ARouter原理与缺陷解析
前言
本文主要包括以下内容
1.为什么需要ARouter
及ARouter
的基本原理
2.什么是APT
及ARoutr
注解是如何生效的?
3.ARouter
有什么缺陷?
4.什么是字节码插桩,及如何利用字节码插桩优化ARouter
?
为什么需要ARouter
我们知道,传统的Activity
之间通信,通过startActivity(intent)
,而在组件化的项目中,上层的module
没有依赖关系(即便两个module
有依赖关系,也只能是单向的依赖)
那么如何实现在没有依赖的情况下进行界面跳转呢?
ARoutr帮我们实现了这点
使用ARouter的原因就是为了解耦,即没有依赖时可以彼此跳转
什么是APT
APT
是Annotation Processing Tool
的简称,即注解处理工具。
它是在编译期对代码中指定的注解进行解析,然后做一些其他处理(如通过javapoet
生成新的Java
文件)。
我们常用的ButterKnife
,其原理就是通过注解处理器在编译期扫描代码中加入的@BindView
、@OnClick
等注解进行扫描处理,然后生成XXX_ViewBinding
类,实现了view
的绑定。
ARouter
中使用的注解处理器就是javapoet
(1)JavaPoet是square推出的开源java代码生成框架
(2)简洁易懂的API,上手快
(3)让繁杂、重复的Java文件,自动化生成,提高工作效率,简化流程
(4) 相比原始APT方法,JavaPoet是OOP的
ARoutr
的注解是如何生效的?
我们在使用ARouter
时都会在Activity
上添加注解
@Route(path = "/kotlin/test")
class KotlinTestActivity : Activity() {
...
}
@Route(path = "/kotlin/java")
public class TestNormalActivity extends AppCompatActivity {
...
}
复制代码
这些注解在编译时会被arouter-compiler
处理,使用JavaPoet在编译期生成类文件
生成的文件如下所示:
public class ARouter$$Group$$kotlin implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/kotlin/java", RouteMeta.build(RouteType.ACTIVITY, TestNormalActivity.class, "/kotlin/java", "kotlin", null, -1, -2147483648));
atlas.put("/kotlin/test", RouteMeta.build(RouteType.ACTIVITY, KotlinTestActivity.class, "/kotlin/test", "kotlin", new java.util.HashMap<String, Integer>(){{put("name", 8); put("age", 3); }}, -1, -2147483648));
}
}
public class ARouter$$Root$$modulekotlin implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("kotlin", ARouter$$Group$$kotlin.class);
}
}
复制代码
如上所示,将注解的key
与类的路径通过一个Map
关联起来了
只要我们拿到这个Map
,即可在运行时通过注解的key
拿到类的路径,实现在不依赖的情况下跳转
如何拿到这个Map呢?
ARouter
缺陷
ARouter
的缺陷就在于拿到这个Map
的过程
我们在使用ARouter
时都需要初始化,ARouter
所做的即是在初始化时利用反射扫描指定包名下面的所有className
,然后再添加map
中
源码如下
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
//load by plugin first
loadRouterMap();
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
Set<String> routerMap;
// It will rebuild router map every times when debuggable.
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
// These class was generated by arouter-compiler.
//反射扫描对应包
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
if (!routerMap.isEmpty()) {
//
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}
PackageUtils.updateVersion(context); // Save new version name when router map update finishes.
} else {
logger.info(TAG, "Load router map from cache.");
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}
....
}
}
复制代码
如上所示:
1.初次打开时会利用ClassUtils.getFileNameByPackageName
来扫描对应包下的所有className
2.在初次扫描后会存储在SharedPreferences
中,这样后续就不需要再扫描了,这也是一个优化
3.以上两个过程都是耗时操作,即是ARouter
初次打开时可能会造成慢的原因
4.那有没有办法优化这个过程,让第一次打开也不需要扫描呢?
利用字节码插桩优化ARouter
首次启动耗时
我们再看看上面的代码
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
//load by plugin first
loadRouterMap();
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
....
}
}
private static void loadRouterMap() {
//registerByPlugin一直被置为false
registerByPlugin = false;
}
复制代码
在初始化时,会在扫描之前,判断registerByPlugin
,如果我们需要的map
已经被插件注册了,那也就不需要进行下面的耗时操作了
但是我们可以看到在loadRouterMap
中,registerByPlugin
一直被设为false
那registerByPlugin
是不是一直没有生效?
这里面其实用到了字节码插桩来在loadRouterMap
方法中插入代码
什么是编译插桩?
顾名思义,所谓编译插桩就是在代码编译期间修改已有的代码或者生成新代码。实际上,我们项目中经常用到的 Dagger、ButterKnife 甚至是 Kotlin 语言,它们都用到了编译插桩的技术。
理解编译插桩之前,需要先回顾一下Android
项目中.java
文件的编译过程:
从上图可以看出,我们可以在 1、2 两处对代码进行改造。
1.在.java
文件编译成.class
文件时,APT
、AndroidAnnotation
等就是在此处触发代码生成。
2.在.class
文件进一步优化成.dex
文件时,也就是直接操作字节码文件,这就是字码码插桩
ARouter
注解生成用了第一种方法,而启动优化则用了第二种方法ASM
是一个十分强大的字节码处理框架,基本上可以实现任何对字节码的操作,也就是自由度和开发的掌控度很高.
但是其相对来说比AspectJ
上手难度要高,需要对Java
字节码有一定了解.
不过ASM
为我们提供了访问者模式来访问字节码文件,这种模式下可以比较简单的做一些字节码操作,实现一些功能。
同时ASM
可以精确的只注入我们想要注入的代码,不会额外生成一些包装代码,所以性能上影响比较微小。
关于ASM
使用的具体细节可以参见:深入探索编译插桩技术(四、ASM 探秘)
字节码插桩对ARouter
具体做了什么优化?
//源码代码,插桩前
private static void loadRouterMap() {
//registerByPlugin一直被置为false
registerByPlugin = false;
}
//插桩后反编译代码
private static void loadRouterMap() {
registerByPlugin = false;
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi");
}
复制代码
1.插桩前源码与插桩后反编译代码如上所示
2.插桩后代码即在编译期在loadRouterMap
中插入了register
代码
3.通过这种方式即可避免在运行时通过反射扫描className
,优化了启动速度
插件使用
使用Gradle
插件实现路由表的自动加载
apply plugin: 'com.alibaba.arouter'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "com.alibaba:arouter-register:?"
}
}
复制代码
1.可选使用,通过ARouter
提供的注册插件进行路由表的自动加载
2.默认通过扫描dex
的方式进行加载,通过gradle
插件进行自动注册可以缩短初始化时间,同时解决应用加固导致无法直接访问dex
文件,初始化失败的问题
3.需要注意的是,该插件必须搭配api 1.3.0
以上版本使用!
4.ARouter
插件基于AutoRegister进行开发,关于其原理的更多介绍可见:AutoRegister:一种更高效的组件自动注册方案
总结
本文主要讲述了
1.使用ARouter
的根本原因是为在互相不依赖的情况下进行页面跳转以实现解藕
2.什么是APT
及ARoutr
注解生成的代码解析
3.ARouter
的缺陷在于首次初始化时会通过反射扫描dex,同时将结果存储在SP
中,会拖慢首次启动速度
4.ARouter
提供了插件实现在编译期实现路由表的自动加载,从而避免启动耗时,其原理是字节码插桩
作者:RicardoMJiang
链接:https://juejin.cn/post/6945610863730491422
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。