作者 | sunshine8
地址 | http://www.jianshu.com/p/8a3eeeaf01e8
声明 | 本文是 sunshine8 原创,已获授权发布,未经原作者允许请勿转载
引子
这篇文章会告诉你
-
什么是路由,是为了解决什么问题才产生的
-
业界现状是怎么样的,我们可以做什么来优化当前的问题
-
路由设计思路是怎么样的,该怎么设计比较好
-
如何用注解实现路由表
-
URL的参数如何依赖注入到Activity、Fragement
-
如何Hook
OnActivityResult
,不需要再进行requstCode判断 -
如何异步拦截路由,实现线程切换,不阻塞页面跳转
-
如何用Apt实现
Retrofit
接口式调用 -
如何找到
Activity
的调用方 -
如何实现路由的
安全
调用 -
如何避开
Apt
不能汇总所有Module路由的问题
前言
当前 Android 的路由库实在太多了,刚开始的时候想为什么要用路由表的库,用 Android 原生的 Scheme 码不就好了,又不像 iOS 只能类依赖,后面越深入就越发现当时想的太简单了,后面看到 Retrofit 和OkHttp,才想到页面请求本质和网络请求不是一样吗,终于业界最简单高效的路由方案 1.0 出来了
开源的库后面会放在公司github地址上面
背景
什么是路由
根据路由表
将页面请求
分发到指定页面
使用场景
-
App接收到一个通知,点击通知打开App的某个页面
-
浏览器App中点击某个链接打开App的某个页面
-
运营活动需求,动态把原生的页面替换成H5页面
-
打开页面需要某些条件,先验证完条件,再去打开那个页面
-
不合法的打开App的页面被屏蔽掉
-
H5打开链接在所有平台都一样,方便统一跳转
-
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
,不耗性能,参数调用不再是问题 -
Hook
OnActivityResult
,支持RxJava响应式调用,不再需要进行requestCode判断 -
参数依赖注入,自动保存,不再需要手动写
onSaveInstance
、onCreate(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 路由,通过类操作耗时,才出现分组的概念。
原理分析
定义路由
对应路由的定义,业界有两种做法
-
参数放在path里面
-
参数放在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
做分发的,这样做会有这几个缺点
-
每次都要启动一个Activity,而Activity就算不写任何代码启动都要0.1秒
-
如果是异步等待的话,Activiy要在合适时间
finish
,不然会有一层透明的页面阻挡操作
对于第一个问题,有两个方法
-
QQ音乐是把
DispatchActivity
设为SingleInstacne
,但是这样的话,动画会奇怪,堆栈也会乱掉,后退会有一层透明的页面阻挡操作 -
DispatchActivity
只在外部打开的时候调用
我选择了第二种
对于第二个问题,有两个方法
-
DispatchActivity
再把Intent转发到Service
,再finish,这种方法唯一的缺陷是拦截器里面的context是Servcie的activity,就没发再拦截器里面弹出对话框了。 -
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,
-
AsyncSubject具有仅释放Observable释放的最后一个数据的特性,作为路由请求的发送器
-
BehaviorSubject具有一开始就会释放最近释放的数据的特性,作为路由拦截器的发送器
具体实现看核心代码
方法调用
大部分路由库都是手动拼参数调用路由的,这里模仿了Retrofit
接口式调用,受了LiteRouter的启发,不过Retrofit
使用了动态代理,我使用的Apt
没有性能损耗。
通过Apt生成每个接口的实际方法
譬如把SecondService
接口
生成
然后调用
SecondService就生成了。
为了调用方便,直接在Activity
或者fragement
写这段代码,sampleServive就自动生成了
@Service
SampleService sampleService;
但是如果用到MVP
模式,不是在Activity
里面调用路由,后面会支持在这些类里面自动注入SampleService,现在先用java代码build
参数获取
大部分路由库都是手动获取参数的,这样还要传入参数key比较麻烦,有三种做法
-
Hook掉
Instrumentation
的newActivity
方法,注入参数 -
注册
ActivityLifecycleCallbacks
方法,注入参数 -
Apt
生成注入代码,onCreate
的时候bind一下
Hook掉Instrumentation
的newActivity
方法是这么实现的
业界的统一做法都是用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-xiaosheng
公众号
「code小生」