注册

ARouter原理与缺陷解析

前言


本文主要包括以下内容

1.为什么需要ARouterARouter的基本原理

2.什么是APTARoutr注解是如何生效的?

3.ARouter有什么缺陷?

4.什么是字节码插桩,及如何利用字节码插桩优化ARouter?


为什么需要ARouter


我们知道,传统的Activity之间通信,通过startActivity(intent),而在组件化的项目中,上层的module没有依赖关系(即便两个module有依赖关系,也只能是单向的依赖)

那么如何实现在没有依赖的情况下进行界面跳转呢?

ARoutr帮我们实现了这点


使用ARouter的原因就是为了解耦,即没有依赖时可以彼此跳转


什么是APT


APTAnnotation 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文件的编译过程:

image.png

从上图可以看出,我们可以在 1、2 两处对代码进行改造。

1.在.java文件编译成.class文件时,APTAndroidAnnotation 等就是在此处触发代码生成。

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.什么是APTARoutr注解生成的代码解析

3.ARouter的缺陷在于首次初始化时会通过反射扫描dex,同时将结果存储在SP中,会拖慢首次启动速度

4.ARouter提供了插件实现在编译期实现路由表的自动加载,从而避免启动耗时,其原理是字节码插桩


作者:RicardoMJiang
链接:https://juejin.cn/post/6945610863730491422
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册