美文网首页Android Devandroid干货爱上Android
Android 组件化 —— 路由设计最佳实践

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

作者: 信念着了火 | 来源:发表于2017-06-14 21:02 被阅读14296次

    引子

    这篇文章会告诉你

    • 什么是路由,是为了解决什么问题才产生的
    • 业界现状是怎么样的,我们可以做什么来优化当前的问题
    • 路由设计思路是怎么样的,该怎么设计比较好
    • 如何用注解实现路由表
    • 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 才会唤起页面

    相关文章

      网友评论

      • 04189ac4f18d:博主开源了嘛?
      • 751d6bed8557: 一年过去了,楼主的2.0版本还有可能见天日吗:joy:
        信念着了火:没想到还有人等着呀,抱歉,公司走流程麻烦,没有动力开源了
      • 无聊的ddd:哥们还开源吗
        d5618be267df:@无聊的ddd 推荐一个看看 https://www.jianshu.com/p/4210234cf0a0
      • 黎鑫:请问博主,原理流程图 是用什么工具画的?
      • 宇宙只有巴掌大:库的地址呢?
        iyifei:@宇宙只有巴掌大 https://www.jianshu.com/p/4210234cf0a0
      • Todo2:实现强大而且完美的组件化路由功能,集成简单稳定.
        https://github.com/wenzhonghu/MyRouter
      • a458411bad82:这种方案,在应用市场加固会有问题吗
      • jkyeo:很棒,期待2.0 开源版本。
      • 5ec8c631b04c:你们这个流程有点长啊。半年了,还怎么造福程序员。。。
      • 751d6bed8557:楼主是51的吗?这篇文章和你的一模一样 https://mp.weixin.qq.com/s/J5Ju0kAapYip97kTz7NzRw
        不知道你们的流程什么时候能走完,等了几个月了没能见到楼主的2.0版本:pensive:
        751d6bed8557:@sunshine8 :+1:
        信念着了火:@一万道光 是的,你怎么发现的,至于流程问题快了,等上了和你讲:smiley:
      • BuleRiver:你好,是这个吗?https://github.com/jjerry/OkDeepLink
        信念着了火:这个是被fork的版本,自己的已经删除了,后面2.0版本会放在公司的github上
      • Chauncey_Chen:感谢如此用心的分析和阐述~ 谢谢~
        信念着了火:@Chauncey_Chen 谢谢,有人看到,对别人有帮助我也挺高兴的
      • 047111e8a054:https://github.com/jjerry/OkDeepLink 这个是你开源的?
        信念着了火:@twolight 我是这样做的,至于担心的会不会内存泄露的问题,我已经解决了
        public class LoginInterceptor extends Interceptor {

        public Call call
        @Override
        public void intercept(final Call call) {
        if(!UserInfoManager.isLogin()){//没有登录
        startActvity(context,LoginActivity.class);
        this.call= call;
        }else{
        call.proceed();
        }
        }

        public void onEventMainThread(LoginEvent event) {//接收到登录成功消息
        call.proceed();//接着刚才的操作
        }
        }
        047111e8a054:@sunshine8 “譬如未登录的情况下,打开登录页面,登录成功后接着打开刚才想打开的页面”,这个场景在2.0版本怎么优雅的实现?
        信念着了火:@twolight 这个不是,是以前开源过被fork的,我自己的删除了,被fork的还在,不要用这个库,这个还是1.0版,等最近走完公司流程,会开源一个2.0,里面增加了很多实用的功能
      • 4f7b66344036:可以分享下github地址吗?谢谢
        3230289517@qq.com
      • 格竹子:写的蛮详细,后面我的组件化ui跳转这块可以参考你的方案
        d5618be267df:@格竹子 推荐最佳路由方案 https://www.jianshu.com/p/4210234cf0a0
      • 梁镈岩:等待你的开源地址。。
        信念着了火: @梁镈岩 走流程中,不过我现在已经改了好多地方,功能更完善了
      • 画十:nice!!!
        有1个问题想请教下楼主:如何跨进程路由通信呢?
        我看了一篇文章http://t.cn/RJ9j4hl ,楼主有空评价下这里的实现方式吗?这个框架多进程进行了处理。
        画十:坐等开源~~~
      • 87f0a1e968a9:方便发一下链接吗
        信念着了火:@杨康_a 需要等待公司流程去开源
      • 小枫:代码开源了吗?能否发个Github链接?或者共享一份参考下,谢谢!bbssyyuui@qq.com
      • xiaobinZh:@ 原理分析的很透彻。 但是使用AOP有点重,而且 会导致 InstantRun,失败。
        可以参考下 https://github.com/gybin02/RouterKit。 也是用了 APT,但是参考的是 Google
        auto-service 里面的代码,使用APT直接生成 配置文件。
        19b75e0e4407:楼主,目前这个有github地址吗?
        信念着了火: @xiaobinZh sunshine8: @sunshine8 文章更新了,评论了你们的方案,你可以看看
        信念着了火: @xiaobinZh用aop是为了解决apt只能遍历每个工程,无法收集所有的module的路由,你发的工程解决了吗?
      • azhunchen:开源了,通知一下:smirk:
        87f0a1e968a9:可以发一下链接吗?
      • 我爱吃栗子啊:你好,可以分享的时候麻烦通知我下哈,觉得写的很棒,我也想学习一下。
        我爱吃栗子啊: @sunshine8 好的,期待分享ԅ(¯ㅂ¯ԅ)
        信念着了火:好的,现在优化了好多,后面开源了,通知你
      • jxiang112:何时在上github?或者能否发份源码观摩学习?847710147@qq.com,谢谢!
        64e76cb16368:@sunshine8 请问能不能支持从调用其他组件的Fragment
        jxiang112:@sunshine8 好的,谢谢!加油:+1:
        信念着了火:正在优化中,现在多了个功能 支持路由方法接收,Url直接打开某个方法
        像这样
        @path("/second")
        void startSecondActivity1(Context context, Intent intent) {
        intent.setComponent(new ComponentName(context, SecondActivity.class));
        context.startActivity(intent);
        }
        至于共享的话,要走正规公司流程,后面分享了,我通知你
      • 2b63166665c1:请问支持插件化的是形式吗
        信念着了火:不看懂,形式?
      • 耍耍:github上已经找不到了
        19b75e0e4407:现在是否有地址呀
        24b48ed7247b:@sunshine8 现在是否有地址?
        信念着了火: @耍耍 抱歉了,公司要求,后面会迁移到公司的github上
      • 耍耍:库怎么被删除了?
      • de6d5d54c98d:很给力。。。
      • 恋猫月亮:我比较关心兼容性和稳定性😉,对比ARouter如何?

        然后为什么的像ARouter之类的不使用RX,应该是考虑了接入问题,使用的人多了,很多时候会和使用者本身的如Rx版本冲突,特别如果是rx1和rx2这样的冲突。
        信念着了火:首先很高兴你提出的宝贵问题
        兼容性不是问题,我没用任何反射方法
        稳定性这个需要时间的考验,我会负责维护的
        至于ARouter不使用Rx,有可能是原来的人没想到RxActivityResult这种思路
        Rx版本的问题,倒确实是一个问题,我后面考虑一下如何兼容
      • KingofGlory: 写的很用心。赞一个。另外有个疑问:你在service中用了@activity(XXXActivity.class) 注解,这样两个module 的activity之间通讯不久耦合了吗?a module 必须依赖b module 的activity类。有想过这个问题吗?
        d47bd9ba6e68:@sunshine8 同样的疑问,楼主的这个SampleService接口是放到一个公共的module里面吧,然后其他业务module都会依赖这个module?直接写XXXActivity.class不就耦合了?还是说像LiteRouter一样用一个类的全名string去替代class?
        KingofGlory:@sunshine8 好的。谢谢回答。
        信念着了火: @KingofGlory 写这个注解是为了生成路由表,两个module之间直接写一个不带activity注解的service就可以调用
      • 罗力:可以,这很强!话说怎么发现Qq音乐的方案的呀
        s_x_q: @sunshine8 我之前也有这方面的想法,可是到现在也没找到几篇,能否分享下你搜寻的?
        信念着了火:@罗力 我有个计划,搜录大厂文章的计划,刚好看到了
      • 开发者头条_程序员必装的App:感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/knmo8j 欢迎点赞支持!
        欢迎订阅《Android组件化》https://toutiao.io/subjects/242487

      本文标题:Android 组件化 —— 路由设计最佳实践

      本文链接:https://www.haomeiwen.com/subject/kxvjqxtx.html