Android 组件化 —— 路由设计最佳实践

引子

这篇文章会告诉你

  • 什么是路由,是为了解决什么问题才产生的
  • 业界现状是怎么样的,我们可以做什么来优化当前的问题
  • 路由设计思路是怎么样的,该怎么设计比较好
  • 如何用注解实现路由表
  • URL的参数如何依赖注入到Activity、Fragement
  • 如何HookOnActivityResult,不需要再进行requstCode判断
  • 如何异步拦截路由,实现线程切换,不阻塞页面跳转
  • 如何用Apt实现Retrofit接口式调用
  • 如何找到Activity的调用方
  • 如何实现路由的安全调用
  • 如何避开Apt不能汇总所有Module路由的问题

前言

当前Android的路由库实在太多了,刚开始的时候想为什么要用路由表的库,用Android原生的Scheme码不就好了,又不像iOS只能类依赖,后面越深入就越发现当时想的太简单了,后面看到Retrofit和OkHttp,才想到页面请求本质和网络请求不是一样吗,终于业界最简单高效的路由方案1.0出来了
开源的库后面会放在公司github地址上面

背景

什么是路由

根据路由表页面请求分发到指定页面

使用场景

  1. App接收到一个通知,点击通知打开App的某个页面
  2. 浏览器App中点击某个链接打开App的某个页面
  3. 运营活动需求,动态把原生的页面替换成H5页面
  4. 打开页面需要某些条件,先验证完条件,再去打开那个页面
  5. 不合法的打开App的页面被屏蔽掉
  6. H5打开链接在所有平台都一样,方便统一跳转
  7. App存在就打开页面,不存在就去下载页面下载,只有Google的App Link支持

为什么要有路由

Android原生已经支持AndroidManifest去管理App跳转,为什么要有路由库,这可能是大部分人接触到Android各种Router库不太明白的地方,这里我讲一下我的理解

  • 显示Intent:项目庞大以后,类依赖耦合太大,不适合组件化拆分
  • 隐式Intent:协作困难,调用时候不知道调什么参数
  • 每个注册了Scheme的Activity都可以直接打开,有安全风险
  • AndroidMainfest集中式管理比较臃肿
  • 无法动态修改路由,如果页面出错,无法动态降级
  • 无法动态拦截跳转,譬如未登录的情况下,打开登录页面,登录成功后接着打开刚才想打开的页面
  • H5、Android、iOS地址不一样,不利于统一跳转

怎么样的路由才算好路由

路由说到底还是为了解决开发者遇到的各种奇葩需求,使用简单、侵入性低、维护方便是首要条件,不影响你原来的代码,写入代码也很少,这里就要说说我的OkDeepLink的五大功能了,五大功能瞬间击中你的各种痛点,早点下班不是梦。

  • 编译时注解,实现静态路由表,不再需要在臃肿的AndroidManifest中找到那个Actvity写Scheme和Intent Filter
  • 异步拦截器,实现动态路由,安全拦截、动态降级难不倒你
  • 模仿Retrofit接口式调用,实现方式用apt,不耗性能,参数调用不再是问题
  • HookOnActivityResult,支持RxJava响应式调用,不再需要进行requestCode判断
  • 参数依赖注入,自动保存,不再需要手动写onSaveInstanceonCreate(SaveInstace)onNewIntent(Intent)getQueryParamer
注册路由
路由结构图

详细比较

大部分路由库都用Apt(编译时注解)生成路由表,然后用路由表转发到指定页面

方案对比 OkDeepLink Airbnb DeepLinkDispatch 阿里 ARouter 天猫 统跳协议 ActivityRouter
路由注册 注解式接口注册 每个module都要手动注册 每个module的路由表都要类查找 AndroidManiFest配置 每个module都要手动注册
路由查找 路由表 路由表 路由表 系统Intent 路由表
路由分发 Activity转发 Activity转发 Activity转发 Activity转发 Activity转发
动态替换 Rxjava实现异步拦截器 不支持 线程等待 不支持 不支持
动态拦截 Rxjava实现异步拦截器 不支持 线程等待 不支持 主线程
安全拦截 Rxjava实现异步拦截器 不支持 线程等待 不支持 主线程
方法调用 接口 手动拼装 手动拼装 手动拼装 手动拼装
参数获取 Apt依赖注入,支持所有类型,不需要在Activity的onCreate中手动调用get方法 参数定义在path,不利于多人协作 Apt依赖注入,但是要手动调用get方法 手动调用 手动调用
结果返回 Rxjava回调 onActivityResult onActivityResult onActivityResult onActivityResult
Module接入不同App 支持 不支持 支持 不支持 支持

其实说到底,路由的本质就是注册再转发,围绕着转发可以进行各种操作,拦截,替换,参数获取等等,其他Apt、Rxjava说到底都只是为了方便使用出现的,这里你会发现各种路由库反而为了修复各种工具带来的问题,出现了原来没有的问题,譬如DeepLinkDispatch为了解决Apt没法汇总所有Module路由,每个module都要手动注册,ARouter为了解决Apt没法汇总所有Module路由,通过类操作耗时,才出现分组的概念。

原理分析

原理流程图

定义路由

路由定义

对应路由的定义,业界有两种做法

  1. 参数放在path里面
  2. 参数放在query里面

参数定义在path里面的做法,有不需要额外传参数的好处,但是没有那么灵活,调试起来也没有那么方便。

路由注册

AndroidManifest里面的acitivity声明scheme码是不安全的,所有App都可以打开这个页面,这里就产生有三种方式去注册,

  • 注解产生路由表,通过DispatchActivity转发
  • AndroidManifest注册,将其export=fasle,但是再通过DispatchActivity转发Intent,天猫就是这么做的,比上面的方法的好处是路由查找都是系统调用,省掉了维护路由表的过程,但是AndroidManifest配置还是比较不方便的
  • 注解自动修改AndroidManifest,这种方式可以避免路由表汇总的问题,方案是这样的,用自定义Lint扫描出注解相关的Activity,然后在processManifestTask后面修改Manifest

我现在还是采用了注解,第三种不稳定

生成路由表

思路都是用Apt生成URL和activity的对应关系

Airbnb

@DeepLink("foo://example.com/deepLink/{id}")
public class MainActivity extends Activity {
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
}

生成

public final class SampleModuleLoader implements Parser {
  public static final List<DeepLinkEntry> REGISTRY = Collections.unmodifiableList(Arrays.asList(
    new DeepLinkEntry("foo://example.com/deepLink/{id}", DeepLinkEntry.Type.METHOD, MainActivity.class, null)
    ));

  @Override
  public DeepLinkEntry parseUri(String uri) {
    for (DeepLinkEntry entry : REGISTRY) {
      if (entry.matches(uri)) {
        return entry;
      }
    }
    return null;
  }
}

阿里Arouter

@Route(path = "/deepLink")
public class MainActivity extends Activity {
 @Autowired
    String id;
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
}

生成


public class ARouter$$Group$$m2 implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/deepLink", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/deepLink", null, null, -1, -2147483648));
  }
}

Activity Router

@Router("deeplink")
public class ModuleActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

生成

public final class RouterMapping_sdk {
  public static final void map() {
    java.util.Map<String,String> transfer = null;
    com.github.mzule.activityrouter.router.ExtraTypes extraTypes;

    transfer = null;
    extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
    extraTypes.setTransfer(transfer);
    com.github.mzule.activityrouter.router.Routers.map("deeplink", ModuleActivity.class, null, extraTypes);

  }
}

OkDeepLink

public interface SampleService {


    @Path("/main")
    @Activity(MainActivity.class)
    void startMainActivity(@Query("key") String key);
}

生成

@After("execution(* okdeeplink.DeepLinkClient.init(..))")
  public void init() {
    DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
  }

初始化路由表

汇总路由表

这里就要提一下使用Apt会造成每个module都要手动注册,因为APT是在javacompile任务前插入了一个task,所以只对自己的moudle处理注解

DeepLinkDispatch是这么做的

@DeepLinkModule
public class SampleModule {
}
@DeepLinkHandler({ SampleModule.class, LibraryDeepLinkModule.class })
public class DeepLinkActivity extends Activity {
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    DeepLinkDelegate deepLinkDelegate = new DeepLinkDelegate(
        new SampleModuleLoader(), new LibraryDeepLinkModuleLoader());
    deepLinkDelegate.dispatchFrom(this);
    finish();
  }
}

ARouter是通过类查找,就比较耗时了,所以他又加入了分组的概念,按需加载

/**
     * 通过指定包名,扫描包下面包含的所有的ClassName
     *
     * @param context     U know
     * @param packageName 包名
     * @return 所有class的集合
     */
    public static List<String> getFileNameByPackageName(Context context, String packageName) throws PackageManager.NameNotFoundException, IOException {
        List<String> classNames = new ArrayList<>();
        for (String path : getSourcePaths(context)) {
            DexFile dexfile = null;

            try {
                if (path.endsWith(EXTRACTED_SUFFIX)) {
                    //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
                    dexfile = DexFile.loadDex(path, path + ".tmp", 0);
                } else {
                    dexfile = new DexFile(path);
                }
                Enumeration<String> dexEntries = dexfile.entries();
                while (dexEntries.hasMoreElements()) {
                    String className = dexEntries.nextElement();
                    if (className.contains(packageName)) {
                        classNames.add(className);
                    }
                }
            } catch (Throwable ignore) {
                Log.e("ARouter", "Scan map file in dex files made error.", ignore);
            } finally {
                if (null != dexfile) {
                    try {
                        dexfile.close();
                    } catch (Throwable ignore) {
                    }
                }
            }
        }

        Log.d("ARouter", "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
        return classNames;
    }

ActivityRouter就比较巧妙了,通过Stub项目,其他地方都是provide的,只有主工程里面用Apt生成RouterInit类,虽然还是要写module的注解

        // RouterInit
        if (hasModules) {
            debug("generate modules RouterInit");
            generateModulesRouterInit(moduleNames);
        } else if (!hasModule) {
            debug("generate default RouterInit");
            generateDefaultRouterInit();
        }

美柚路由是通过生成每个module的路由表,然后复制到app的assets目录,运行的时候遍历asset目录,反射对应的activity

//拷贝生成的 assets/目录到打包目录
android.applicationVariants.all { variant ->
    def variantName = variant.name
    def variantNameCapitalized = variantName.capitalize()
    def copyMetaInf = tasks.create "copyMetaInf$variantNameCapitalized", Copy
    copyMetaInf.from project.fileTree(javaCompile.destinationDir)
    copyMetaInf.include "assets/**"
    copyMetaInf.into "build/intermediates/sourceFolderJavaResources/$variantName"
    tasks.findByName("transformResourcesWithMergeJavaResFor$variantNameCapitalized").dependsOn copyMetaInf
}

Metis是一个android中解决服务发现的库,他是这么解决的,在app主工程中transfomer的时候去扫描所有modlue和jar带注解的文件去生成路由表,然后把这个java文件编译,但是这种方式需要扫描整个app会慢一点,而且手动去编译java感觉不太稳定的感觉

 def destDir
        List<String> classpaths = new ArrayList<>()
        transformInvocation.inputs.each { input ->

            input.jarInputs.each { jarInput ->

                def jarName = jarInput.name
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }

                def dest = transformInvocation.outputProvider.getContentLocation(jarName, jarInput.contentTypes, jarInput.scopes, Format.JAR)

                classpaths.add(dest)
                mAction.loadJar(new JarFile(jarInput.file), jarInput.status)
                FileUtils.copyFile(jarInput.file, dest)

                mProject.logger.info("scan file:\t ${jarInput.file} status:${jarInput.status}")
            }

            input.directoryInputs.each { dirInput ->

                // 测试发现: 如果目录下的文件没有任何改变,不会进入到这个 transform
                Map<File, Status> changedFiles = dirInput.changedFiles
                if (changedFiles == null || changedFiles.isEmpty()) {
                    // clean 后进入, changed 为空
                    mAction.loadDirectory(dirInput.file)
                    mProject.logger.info("scan dir:\t ${dirInput.file}")
                } else {
                    mAction.loadChangedFiles(changedFiles)
                }

                destDir = transformInvocation.outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)
                classpaths.add(destDir)
                FileUtils.copyDirectory(dirInput.file, destDir)
            }
        }

天猫 统跳协议 是最简单的,转发一下Intent就可以,但是这样就没法享受注解的好处了。

而OkDeepLink用aspectj解决了这个问题,会自动汇总所有module的路由省略了这些多余的代码。

@After("execution(* okdeeplink.DeepLinkClient.init(..))")
  public void init() {
    DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
  }

路由查找

路由查找就是查找路由表对应的页面,值得提起的就是因为要适应Module接入不同App,Scheme要自动适应,路由表其实是Path---》Activity,这样的话内部跳转的时候ARouterUri是没有的。而我这边是有的,我组装了一个内部的Uri,这样拦截器不会有影响。

public Request buildRequest(Intent sourceIntent) {
        if (sourceIntent == null) {
            return null;
        }
        Intent newIntent = new Intent(sourceIntent);
        Uri uri = newIntent.getData();

        addNewTaskFlag(newIntent);

        if (uri != null) {
            addBundleQuery(newIntent, uri);

            Address entry = new DeepLinkClient(context).matchUrl(uri.toString());
            if (entry == null || entry.getActivityClass() == null) {
                return new Request(newIntent, this).setDeepLink(false);
            }
            newIntent.setComponent(new ComponentName(context, entry.getActivityClass()));

            return new Request(newIntent, this);
        }
        return new Request(newIntent, this).setDeepLink(false);

    }

路由分发

现在所有路由方案分发都是用Activity做分发的,这样做会有这几个缺点

  1. 每次都要启动一个Activity,而Activity就算不写任何代码启动都要0.1秒
  2. 如果是异步等待的话,Activiy要在合适时间finish,不然会有一层透明的页面阻挡操作

对于第一个问题,有两个方法

  1. QQ音乐是把DispatchActivity设为SingleInstacne,但是这样的话,动画会奇怪,堆栈也会乱掉,后退会有一层透明的页面阻挡操作
  2. DispatchActivity只在外部打开的时候调用

我选择了第二种

对于第二个问题,有两个方法

  1. DispatchActivity再把Intent转发到Service,再finish,这种方法唯一的缺陷是拦截器里面的context是Servcie的activity,就没发再拦截器里面弹出对话框了。
  2. DispatchActivity在打开和错误的时候finish,如果activity已经finish了,就用application的context去转发路由

我选择了第二种

  public void dispatchFrom(Intent intent) {
        new DeepLinkClient(this)
                .buildRequest(intent)
                .dispatch()
                .subscribe(new Subscriber<Request>() {
                    @Override
                    public void onCompleted() {
                        finish();
                    }

                    @Override
                    public void onError(Throwable e) {
                        finish();
                    }

                    @Override
                    public void onNext(Request request) {
                        Intent dispatchIntent = request.getIntent();
                        startActivity(dispatchIntent);
                    }
                });
    }

其实处理透明Activity阻挡操作可以采用取消所有事件变成无感页面的方法
我找到一种方式解决这个问题解决透明Activity点击不影响用户操作

结果返回

这里我封装了一个库RxActivityResult去捕获onActivityResult,这样能保正流式调用

譬如拍照可以这样写,先定义一个接口

    public interface ImageCaptureService {


    @Action(MediaStore.ACTION_IMAGE_CAPTURE)
    Observable<Response> startImageCapture();
}

然后这样调用

public class MainActivity extends AppCompatActivity {

    @Service
    ImageCaptureService imageCaptureService;
  
    public void captureImage(){
        imageCaptureService
                .startImageCapture()
                .subscribe(new Action1<Response>() {
                    @Override
                    public void call(Response response) {
                        Intent data = response.getData();
                        int resultCode = response.getResultCode();
                        if (resultCode == RESULT_OK) {
                            Bitmap imageBitmap = (Bitmap) data.getExtras().get("data");
                        }
                    }
                });
    }
}
}

是不是很简单,原理是这样的,通过封装一个RxResultHoldFragment去处理onActivityResult

 private IActivityObservable buildActivityObservable() {

            T target = targetWeak.get();

            if (target instanceof FragmentActivity) {
                FragmentActivity activity = (FragmentActivity) target;
                android.support.v4.app.FragmentManager fragmentManager = activity.getSupportFragmentManager();
                IActivityObservable activityObservable = RxResultHoldFragmentV4.getHoldFragment(fragmentManager);
                return activityObservable;
            }

            if (target instanceof Activity) {
                Activity activity = (Activity) target;
                FragmentManager fragmentManager = activity.getFragmentManager();
                IActivityObservable activityObservable = RxResultHoldFragment.getHoldFragment(fragmentManager);
                return activityObservable;
            }
            if (target instanceof Context) {
                final Context context = (Context) target;
                IActivityObservable activityObservable = new RxResultHoldContext(context);
                return activityObservable;
            }

            if (target instanceof Fragment) {
                Fragment fragment = (Fragment) target;
                FragmentManager fragmentManager = fragment.getFragmentManager();
                if (fragmentManager != null) {
                    IActivityObservable activityObservable = RxResultHoldFragment.getHoldFragment(fragmentManager);
                    return activityObservable;
                }
            }
            if (target instanceof android.support.v4.app.Fragment) {
                android.support.v4.app.Fragment fragment = (android.support.v4.app.Fragment) target;
                android.support.v4.app.FragmentManager fragmentManager = fragment.getFragmentManager();
                if (fragmentManager != null) {
                    IActivityObservable activityObservable = RxResultHoldFragmentV4.getHoldFragment(fragmentManager);
                    return activityObservable;
                }
            }
            return new RxResultHoldEmpty();
        }

动态拦截

拦截器是重中之重,有了拦截器可以做好多事情,可以说之所以要做页面路由,就是为了要实现拦截器。ARouter是用线程等待实现的,但是现在有Rxjava了,可以实现更优美的方式。
先来看一下我做的拦截器的效果.

@Intercept(path = "/second")
public class SecondInterceptor extends Interceptor {
    @Override
    public void intercept(final Call call) {

        Request request = call.getRequest();
        final Intent intent = request.getIntent();
        Context context = request.getContext();

        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("Intercept\n");
        stringBuffer.append("URL: " + request.getUrl() + "\n");

        AlertDialog.Builder builder = new AlertDialog.Builder(context,R.style.Theme_AppCompat_Dialog_Alert);
        builder.setTitle("Notice");
        builder.setMessage(stringBuffer);
        builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                call.cancel();
            }
        });
        builder.setPositiveButton("ok", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                intent.putExtra("key1", "value3");
                call.proceed();
            }
        });
        builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                call.cancel();
            }
        });
        builder.show();
    }
}

是不是很简单,参考了部分OkHttp的实现思路,加入Rxjava,实现异步拦截。

首先将请求转换成责任链模式RealCallChain,RealCallChain的call方法实际不会执行路由跳转,只有Interceptor里面调用了call.proceed或者call.cancel才会执行.

    private Observable<Request> buildRequest() {
        RealCallChain chain = new RealCallChain(interceptors, 0, request);
        chain.setTimeout(interceptTimeOut);
        chain.call();
        return chain
                .getRequestObservable()
                .map(new Func1<Request, Request>() {
                    @Override
                    public Request call(Request request) {
                        if (interceptors != null) {
                            for (Interceptor interceptor : interceptors) {
                                interceptor.onCall(request);
                            }
                        }
                        return request;
                    }
                });
    }

接着处理异步的问题,这里用到了Rxjava的AsyncSubject和BehaviorSubject,

  1. AsyncSubject具有仅释放Observable释放的最后一个数据的特性,作为路由请求的发送器
  2. BehaviorSubject具有一开始就会释放最近释放的数据的特性,作为路由拦截器的发送器

具体实现看核心代码

    @Override
    public void proceed() {


        if (index >= interceptors.size()) {
            realCall();
            return;
        }
        final Interceptor interceptor = interceptors.get(index);
        Observable
                .just(1)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {
                        interceptor.intercept(RealCallChain.this);
                    }
                });

        interceptorSubject.onNext(interceptor);
        index = index + 1;
    }

方法调用

大部分路由库都是手动拼参数调用路由的,这里模仿了Retrofit接口式调用,受了LiteRouter的启发,不过Retrofit使用了动态代理,我使用的Apt没有性能损耗。

通过Apt生成每个接口的实际方法

譬如把SecondService接口

public interface SecondService {

    @Path("/second")
    @Activity(SecondActivity.class)
    void startSecondActivity();
}

生成

@Aspect
public final class SecondService$$Provider implements SecondService {
  public DeepLinkClient deepLinkClient;

  public SecondService$$Provider(DeepLinkClient deepLinkClient) {
    this.deepLinkClient= deepLinkClient;
  }
  @Override
  public void startSecondActivity() {
    Intent intent = new Intent();
    intent.setData(Uri.parse("app://deeplink/second"));
    Request request = deepLinkClient.buildRequest(intent);
    if (request != null) {
      request.start();
    }
  }
  
  @Around("execution(* okdeeplink.DeepLinkClient.build(..))")
  public Object aroundBuildMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    DeepLinkClient target = (DeepLinkClient)joinPoint.getTarget();
    if (joinPoint.getArgs() == null || joinPoint.getArgs().length != 1) {
      return joinPoint.proceed();
    }
    Object arg = joinPoint.getArgs()[0];
    if (arg instanceof Class) {
      Class buildClass = (Class) arg;
      if (buildClass.isAssignableFrom(getClass())) {
        return new SecondService$$Provider(target);
      }
    }
    return joinPoint.proceed();
  }
}

然后调用

SecondService secondServicenew = DeepLinkClient(target).build(SecondService.class);

SecondService就生成了。
为了调用方便,直接在Activity或者fragement写这段代码,sampleServive就自动生成了

  @Service
  SampleService sampleService;

但是如果用到MVP模式,不是在Activity里面调用路由,后面会支持在这些类里面自动注入SampleService,现在先用java代码build

参数获取

大部分路由库都是手动获取参数的,这样还要传入参数key比较麻烦,有三种做法

  1. Hook掉InstrumentationnewActivity方法,注入参数
  2. 注册ActivityLifecycleCallbacks方法,注入参数
  3. Apt生成注入代码,onCreate的时候bind一下

Hook掉InstrumentationnewActivity方法是这么实现的

@Deprecated
public class InstrumentationHook extends Instrumentation {
    /**
     * Hook the instrumentation's newActivity, inject
     * <p>
     * Perform instantiation of the process's {@link Activity} object.  The
     * default implementation provides the normal system behavior.
     *
     * @param cl        The ClassLoader with which to instantiate the object.
     * @param className The name of the class implementing the Activity
     *                  object.
     * @param intent    The Intent object that specified the activity class being
     *                  instantiated.
     * @return The newly instantiated Activity object.
     */
    public Activity newActivity(ClassLoader cl, String className,
                                Intent intent)
            throws InstantiationException, IllegalAccessException,
            ClassNotFoundException {

//        return (Activity)cl.loadClass(className).newInstance();

        Class<?> targetActivity = cl.loadClass(className);
        Object instanceOfTarget = targetActivity.newInstance();

        if (ARouter.canAutoInject()) {
            String[] autoInjectParams = intent.getStringArrayExtra(ARouter.AUTO_INJECT);
            if (null != autoInjectParams && autoInjectParams.length > 0) {
                for (String paramsName : autoInjectParams) {
                    Object value = intent.getExtras().get(TextUtils.getLeft(paramsName));
                    if (null != value) {
                        try {
                            Field injectField = targetActivity.getDeclaredField(TextUtils.getLeft(paramsName));
                            injectField.setAccessible(true);
                            injectField.set(instanceOfTarget, value);
                        } catch (Exception e) {
                            ARouter.logger.error(Consts.TAG, "Inject values for activity error! [" + e.getMessage() + "]");
                        }
                    }
                }
            }
        }

        return (Activity) instanceOfTarget;
    }
}

业界的统一做法都是用apt,其他方式不稳定,ARouterandroidannotationsJet, 思路都是一样的,这里拿ARouter的代码说明一下是怎么实现的

Autowired生成Test1Activity$$ARouter$$Autowired类,用inject方法找到AutowiredServiceImpl方法,AutowiredServiceImpl调用到Test1Activity$$ARouter$$Autowired

@Route(path = "/test/activity1")
public class Test1Activity extends AppCompatActivity {

    @Autowired
    String name;
     @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test1);

        ARouter.getInstance().inject(this);
        }
    
    }

@Route(path = "/arouter/service/autowired")
public class AutowiredServiceImpl implements AutowiredService {
    private LruCache<String, ISyringe> classCache;
    private List<String> blackList;

    @Override
    public void init(Context context) {
        classCache = new LruCache<>(66);
        blackList = new ArrayList<>();
    }

    @Override
    public void autowire(Object instance) {
        String className = instance.getClass().getName();
        try {
            if (!blackList.contains(className)) {
                ISyringe autowiredHelper = classCache.get(className);
                if (null == autowiredHelper) {  // No cache.
                    autowiredHelper = (ISyringe) Class.forName(instance.getClass().getName() + SUFFIX_AUTOWIRED).getConstructor().newInstance();
                }
                autowiredHelper.inject(instance);
                classCache.put(className, autowiredHelper);
            }
        } catch (Exception ex) {
            blackList.add(className);    // This instance need not autowired.
        }
    }
}
public class Test1Activity$$ARouter$$Autowired implements ISyringe {

  @Override
  public void inject(Object target) {
    Test1Activity substitute = (Test1Activity)target;
    substitute.name = substitute.getIntent().getStringExtra("name");
  }
}

OkDeepLink这里模仿了ARouter,不过支持类型更全一些,支持Bundle支持的所有类型,而且不需要在Acitivty的onCreate调用获取代码。
通过Apt把这段代码

public class MainActivity extends AppCompatActivity {

    @Query("key")
    String key;
}

生成


@Aspect
public class MainActivity$$Injector {
  @Around("execution(* okdeeplink.sample.MainActivity.onCreate(..))")
  public void onCreate(ProceedingJoinPoint joinPoint) throws Throwable {
    MainActivity target = (MainActivity)joinPoint.getTarget();
    Bundle dataBundle = new Bundle();
    Bundle saveBundle = (Bundle)joinPoint.getArgs()[0];
    Bundle targetBundle = BundleCompact.getSupportBundle(target);
    if(targetBundle != null) {
      dataBundle.putAll(targetBundle);
    }
    if(saveBundle != null) {
      dataBundle.putAll(saveBundle);
    }
    try {
      target.key= BundleCompact.getValue(dataBundle,"key",String.class);
    } catch (Exception e) {
      e.printStackTrace();
    }
    joinPoint.proceed();
  }

  @After("execution(* okdeeplink.sample.MainActivity.onSaveInstanceState(..))")
  public void onSaveInstanceState(JoinPoint joinPoint) throws Throwable {
    MainActivity target = (MainActivity)joinPoint.getTarget();
    Bundle saveBundle = (Bundle)joinPoint.getArgs()[0];
    Intent intent = new Intent();
    intent.putExtra("key",target.key);
    saveBundle.putAll(intent.getExtras());
  }

  @Around("execution(* okdeeplink.sample.MainActivity.onNewIntent(..))")
  public void onNewIntent(ProceedingJoinPoint joinPoint) throws Throwable {
    MainActivity target = (MainActivity)joinPoint.getTarget();
    Intent targetIntent = (Intent)joinPoint.getArgs()[0];
    Bundle dataBundle = targetIntent.getExtras();
    try {
      target.key= BundleCompact.getValue(dataBundle,"key",String.class);
    } catch (Exception e) {
      e.printStackTrace();
    }
    joinPoint.proceed();
  }
}

Module接入不同App

这里是参考ARouter把path作为key对应activity,这样接入到其他app中,就自动替换了scheme码

DeepLinkClient.addAddress(new Address("/main", MainActivity.class));

安全

现在有好多人用脚本来打开App,然后干坏事,其实时可以用路由来屏蔽掉.

有三种方法供君选择,不同方法适合不同场景

签名屏蔽

就是把所有参数加密成一个数据作为sign参数,然后比对校验,但是这要求加密方法不变,要不然升级了以前的app就打不开了

adb打开屏蔽

在android5.1手机上,用adb打开的app它的mReferrer为空

 public boolean isStartByAdb(android.app.Activity activity){
        if (Build.VERSION.SDK_INT >= 22) {
            android.net.Uri uri = ActivityCompat.getReferrer(activity);
            return uri == null | TextUtils.isEmpty(uri.toString()) ;
        }
        return false;
    }

包名过滤

在Android 4.4手机上, 写了android:ssp的组件,只有特定应用可以打开

<activity
            android:name="okdeeplink.DeepLinkActivity"
            android:noHistory="true"
            android:theme="@android:style/Theme.Translucent.NoTitleBar">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:ssp="com.app.test"
                    android:host="app"
                    android:scheme="odl" />
            </intent-filter>
        </activity>

这三种方法,比较适合的还是签名校验为主,adb过滤为副

如何解决路由造成的Activity堆栈错乱的问题

activity的launchMode使用不当会照成闪屏页面打开多次的问题,可以参考我这篇文章

未来展望

路由是一个基础模块,技术难度虽然不是很大,但是如果每个开发都重新踩一遍,性价比就比较低,我希望能把路由相关的所有链路都替你弄好,你可以留着时间去干其他更重要的事情,譬如陪陪家人,逗逗狗什么的。
接下来我会在这几个方面努力,把整条链路补全。

  • 做一个像Swagger的平台,支持一键导出所有路由、二维码打开路由
  • 注解修改AndroidManifest,不再需要路由表
  • 支持路由方法接收器,Url直接打开某个方法,不再局限Activity已实现

如果大家有意见,欢迎联系我kingofzqj@gmail.com

参考文献

业界做法

设计方案

个人开发

安全讨论

  • 如何在Activity中获取调用者 讨论了android里面原生支持找到路由来源的可能性,分析了referrer是如何产生的
  • LauncherFrom
    提供了一种hook activitythread找到launchedFromPackage的方法,不过也只支持5.0以上
  • 高效过滤Intents
    只有包含特定Package URL的 intent 才会唤起页面
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容