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

2,000 阅读13分钟
原文链接: mp.weixin.qq.com

作者 | sunshine8

地址 | http://www.jianshu.com/p/8a3eeeaf01e8

声明 | 本文是 sunshine8 原创,已获授权发布,未经原作者允许请勿转载

引子

这篇文章会告诉你

  • 什么是路由,是为了解决什么问题才产生的

  • 业界现状是怎么样的,我们可以做什么来优化当前的问题

  • 路由设计思路是怎么样的,该怎么设计比较好

  • 如何用注解实现路由表

  • 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

生成

阿里Arouter

生成

Activity Router

生成

OkDeepLink

生成

初始化路由表

汇总路由表

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

DeepLinkDispatch https://github.com/airbnb/DeepLinkDispatch是这么做的

ARouter https://yq.aliyun.com/articles/71687?spm=5176.100240.searchblog.7.8os9Go 是通过类查找,就比较耗时了,所以他又加入了分组的概念,按需加载

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

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

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

天猫 统跳协议 https://yq.aliyun.com/articles/71687?spm=5176.100240.searchblog.7.8os9Go 是最简单的,转发一下Intent就可以,但是这样就没法享受注解的好处了。

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

路由查找

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

路由分发

现在所有路由方案分发都是用 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去转发路由

我选择了第二种

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

结果返回

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

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

然后这样调用

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

动态拦截

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

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

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

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

  1. AsyncSubject具有仅释放Observable释放的最后一个数据的特性,作为路由请求的发送器

  2. BehaviorSubject具有一开始就会释放最近释放的数据的特性,作为路由拦截器的发送器

具体实现看核心代码

方法调用

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

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

譬如把SecondService接口

生成

然后调用

SecondService就生成了。

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

  @Service
  SampleService sampleService;

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

参数获取

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

  1. Hook掉InstrumentationnewActivity方法,注入参数

  2. 注册ActivityLifecycleCallbacks方法,注入参数

  3. Apt生成注入代码,onCreate的时候bind一下

Hook掉InstrumentationnewActivity方法是这么实现的

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

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

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

通过Apt把这段代码

生成

Module接入不同App

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

安全

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

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

签名屏蔽

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

adb打开屏蔽

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

包名过滤

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

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

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

activity的launchMode使用不当会照成闪屏页面打开多次的问题,可以参考我这篇文章。http://www.jianshu.com/p/b202690b7d96

未来展望

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

  • 做一个像Swagger的平台,支持一键导出所有路由、二维码打开路由

  • 注解修改AndroidManifest,不再需要路由表

  • 支持路由方法接收器,Url直接打开某个方法,不再局限Activity已实现

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

参考文献

业界做法

  • airbnb开源的页面路由

  • 阿里开源的页面路由

  • 天猫的统跳协议

  • 蘑菇街的页面路由

  • Google App Link

  • 移动DeepLink的前世今生

设计方案

  • UrlRouter路由框架的设计

  • 移动端路由层设计

  • 客户端路由动态配置

  • 移动端基于动态路由的架构设计

  • Android组件化通信(多进程)

  • iOS 组件化 —— 路由设计思路分析

  • QQ音乐首页Activity的单例实现

个人开发

  • LiteRouter            模仿retrofit,各个业务分根据需求约定好接口,就像一份接口文档一样

  • ActivityRouter

  • ActivityRouter2

  • AndRouter

  • Router

  • Router2

  • router-android

安全讨论

  • 如何在Activity中获取调用者             讨论了android里面原生支持找到路由来源的可能性,分析了referrer是如何产生的

  • LauncherFrom提供了一种hook activitythread找到launchedFromPackage的方法,不过也只支持5.0以上

  • 高效过滤Intents只有包含特定Package URL的 intent 才会唤起页面

今日推荐

code小生-交流圈 开通啦

微信号:code-xiaosheng

公众号

「code小生」