在阅读源码前,请先下载源码:ARouter

最近阅读ARouter源码,发现这真的是一个非常优秀的框架。激发出兴趣来读一下他的源码,实际上,这个框架的结构非常简单。这个框架可以分为主流程辅助流程来拆开分析。

主流程包含编译时运行时两个部分,其中编译时主要做的是路由路径表的构建,运行时主要做的是路由路径表的加载;

辅助流程主要就是做启动优化

主流程

1. 编译时

这部分主要涉及到的是路由路径表的构建,其实现原理是APT,即注解处理器

使用ARouter时候,需要在目标类上,通过**@Route注解进行标记,注解处理器处理的就是这个注解。打开源码路径下的arouter-compiler这个module,找到RouteProcessor,这个类就是用来处理@Route**注解的类。这里需要了解的知识,除了APT,还有java-poet,请自行了解这些。

Processor类的入口方法是process方法,这个方法返回true,则这个处理器已经完成了自己的任务,不会被重复调用。其他比较重要的方法有getSupportedSourceVersiongetSupportedAnnotationTypes等。

1
Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);

通过这个方法,获取所有被**@Route**标记的元素。

获取到routeElements后,在parseRoutes方法进行处理。我们以最常用的Activity为例,进行分析。

1
2
3
4
5
6
7
rootMap.clear();	//用来分类存储标记元素

// 用来检测元素是否为对应的类型,通过Types.isSubtype()方法来检测。
TypeMirror type_Activity = elementUtils.getTypeElement(ACTIVITY).asType();
TypeMirror type_Service = elementUtils.getTypeElement(SERVICE).asType();
TypeMirror fragmentTm = elementUtils.getTypeElement(FRAGMENT).asType();
TypeMirror fragmentTmV4 = elementUtils.getTypeElement(Consts.FRAGMENT_V4).asType();

最后将分类号的元素信息,存储在成员变量groupMap中去。

1
private Map<String, Set<RouteMeta>> groupMap = new HashMap<>();

然后再通过这个groupMap,借助java-poet,来生成真实的类。如下:

1
2
3
4
5
6
7
8
9
10
11
public class ARouter$$Group$$test implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test/activity1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/activity1", "test", new java.util.HashMap<String, Integer>(){{put("ser", 9); put("ch", 5); put("fl", 6); put("dou", 7); put("boy", 0); put("url", 8); put("pac", 10); put("obj", 11); put("name", 8); put("objList", 11); put("map", 11); put("age", 3); put("height", 3); }}, -1, -2147483648));
atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(){{put("key1", 8); }}, -1, -2147483648));
atlas.put("/test/activity3", RouteMeta.build(RouteType.ACTIVITY, Test3Activity.class, "/test/activity3", "test", new java.util.HashMap<String, Integer>(){{put("name", 8); put("boy", 0); put("age", 3); }}, -1, -2147483648));
atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648));
atlas.put("/test/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, "/test/fragment", "test", new java.util.HashMap<String, Integer>(){{put("obj", 11); put("name", 8); }}, -1, -2147483648));
atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, "/test/webview", "test", null, -1, -2147483648));
}
}

2. 运行时

这部分主要做的是,在*ARouter.init()*时候,将上过程生成的路径表加载到内存中。

如果你以官方demo程序验证这一步,需要将app/build.gradle中的apply plugin: 'com.alibaba.arouter'这一行代码注释掉。

我们以ARouter.init方法为入口,实际上最终实现init流程的是LogisticsCenter类的init方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
...
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
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);

我们查看这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
final Set<String> classNames = new HashSet<>();

List<String> paths = getSourcePaths(context);
final CountDownLatch parserCtl = new CountDownLatch(paths.size());

for (final String path : paths) {
Log.v(TAG, "getFileNameByPackageName path=" + path);
DefaultPoolExecutor.getInstance().execute(new Runnable() {
@Override
public void run() {
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.startsWith(packageName)) {
Log.v(TAG, "find CLASS NAME " + className);
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) {
}
}

parserCtl.countDown();
}
}
});
}

parserCtl.await();

Log.d(Consts.TAG, "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
return classNames;
}

注意其中的getSourcePaths方法,这是从代码目录,来获取所有代码目录,然后在

getFileNameByPackageName找出以com.alibaba.android.arouter.routes为开头包名的类,这些就是我们在步骤1中生成的辅助类。

这个方法结束后,回到LogisticsCenter#init方法,接下来要做的就是,把加载到的辅助类,通过反射生成对象,再调用其loadTo方法,将路由路径表加载到Warehouse类中去,方便以后的查询。

辅助流程

在以上的流程中,有一个严重的问题,那就是执行ARouter#init方法的时间过长,以源码的demo为例,在InstantRun的情况下,OnePlus5T需要100多毫秒才能初始化完,这对于程序启动优化来说,是一个不可忽视的时间了。那么如何解决这个问题呢?

1
MainActivity: init cost 134

这就是在上一步中,要求你注释掉的代码起作用了,将apply plugin: 'com.alibaba.arouter'解除注释,让其发挥作用。

这里需要关注的module是arouter-gradle-plugin

先来看一下,使用了apply plugin: 'com.alibaba.arouter'的神奇效果。

1
MainActivity: init cost 19

通过优化,让ARouter#init消耗时间直接降低了一个数量级,那么arouter-gradle-plugin是怎么做到的呢?

这需要你先了解一下[ASM](/android/ASM.md)。简单来说,这是一种字节码编程技术,通过修改编译后的字节码的方式,来对原始逻辑增强。

我们再来看ARouter#init的最终实现类和方法LogisticsCenter#init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
....
try {
long startInit = System.currentTimeMillis();
//billy.qi modified at 2017-12-06
//load by plugin first
loadRouterMap();
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
....
}
.....
} catch (Exception e) {
throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
}
}

我们可以看到,当registerByPlugin为true的时候,则只是打印了一句日志,我们再看loadRouterMap()这个方法:

1
2
3
4
5
6
7
private static void loadRouterMap() {
registerByPlugin = false;
//auto generate register code by gradle plugin: arouter-auto-register
// looks like below:
// registerRouteRoot(new ARouter..Root..modulejava());
// registerRouteRoot(new ARouter..Root..modulekotlin());
}

这里的逻辑非常简单,到底在哪里去加载的路由路径表呢?我们看这个方法的注释,发现,这个方法是被arouter-auto-register自动生成的。

我们打开arouter-gradle-plugin/resources/META-INF/gradle-plugins这个目录,可以看到,有一个com.alibaba.arouter.properties文件,查看其内容:

1
implementation-class=com.alibaba.android.arouter.register.launch.PluginLaunch

这个PluginLaunch便是此gradle plugin的入口类。查看此类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class PluginLaunch implements Plugin<Project> {

@Override
public void apply(Project project) {
def isApp = project.plugins.hasPlugin(AppPlugin)
//only application module needs this plugin to generate register code
if (isApp) {
Logger.make(project)

Logger.i('Project enable arouter-register plugin')

def android = project.extensions.getByType(AppExtension)
def transformImpl = new RegisterTransform(project)

//init arouter-auto-register settings
ArrayList<ScanSetting> list = new ArrayList<>(3)
list.add(new ScanSetting('IRouteRoot'))
list.add(new ScanSetting('IInterceptorGroup'))
list.add(new ScanSetting('IProviderGroup'))
RegisterTransform.registerList = list
//register this plugin
android.registerTransform(transformImpl)
}
}

}

我们查看其代码,可以发现,这里一共做了三件事:

  1. 判断是否为app module,如果不是,则不做任何事,在app module下做2和3两步;
  2. 生成了一个RegisterTransform,并为其静态变量registerList赋值,注意此处赋值的registerList中包含的三个对象
  3. 注册此RegisterTransform。

接下来,就轮到RegisterTransform来执行了。

Transform类,简单来说,就是可以在编译时,扫描所有的jar和class,包括引用类库中的。在扫描过程中,就可以借助ASM技术对目标类进行更改。

我们看其入口方法transform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
inputs.each { TransformInput input ->
// scan all jars
input.jarInputs.each { JarInput jarInput ->
...
if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
ScanUtil.scanJar(src, dest)
}
...
}
}
input.directoryInputs.each { DirectoryInput directoryInput ->
...
directoryInput.file.eachFileRecurse { File file ->
...
if(file.isFile() && ScanUtil.shouldProcessClass(path)){
ScanUtil.scanClass(file)
}
}
...
}
}

省去了一些细节,只保留了主线逻辑,我们可以看到,其扫描到的jar和class都经过了ScanUtils的方法来处理,我们继续跟踪下去,会发现,scanJar也是循环调用的scanClass,这样我们直接看scanClass方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static void scanClass(InputStream inputStream) {
ClassReader cr = new ClassReader(inputStream)
ClassWriter cw = new ClassWriter(cr, 0)
ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
cr.accept(cv, ClassReader.EXPAND_FRAMES)
inputStream.close()
}

static class ScanClassVisitor extends ClassVisitor {

ScanClassVisitor(int api, ClassVisitor cv) {
super(api, cv)
}

void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
RegisterTransform.registerList.each { ext ->
if (ext.interfaceName && interfaces != null) {
interfaces.each { itName ->
if (itName == ext.interfaceName) {
//fix repeated inject init code when Multi-channel packaging
if (!ext.classList.contains(name)) {
ext.classList.add(name)
}
}
}
}
}
}
}

这里就又涉及到了ASM的知识,ScanClassVisitor是访问某个类的内部结构。

version:类的版本;

access:表示类的访问权限,public,private,protected等;

name:类的名字;

signature:有无泛型;

superName:其父类;

interfaces:其实现的接口;

ScanClassVisitor中,并没有对类做修改,只是从遍历过的类中,把我们关心的类挑出来。那么,我们关心哪些类呢?

PluginLaunch类中,我们注册了三个ScanSettings类,分别是IRouteRootIInterceptorGroupIProviderGroup,也就是说,我们把实现了这三个接口的类,挑出来,加入到各自对应的ScanSettings类中记录起来。这三个接口是不是很熟悉?就是通过APT生成的用来记录路由路径表的类。

等收集好了这些记录的路径表信息后,就可以对LogisticsCenter通过ASM进行修改了。我们接着看RegisterTransform#transform方法中剩下的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (fileContainsInitClass) {
registerList.each { ext ->
Logger.i('Insert register code to file ' + fileContainsInitClass.absolutePath)

if (ext.classList.isEmpty()) {
Logger.e("No class implements found for interface:" + ext.interfaceName)
} else {
ext.classList.each {
Logger.i(it)
}
RegisterCodeGenerator.insertInitCodeTo(ext)
}
}
}

注意此处的insertInitCodeTo方法,这就是ASM修改的入口了。这里不对修改过程进行详细解释了。我们直接对比看LogisticsCenter修改前后关键代码的对比。

app/build目录下,找到生成的apk文件,通过AndroidStudio来查看其中的class,找到关键LogisticsCenter关键方法loadRouterMap

具体过程如下:

app/build/outputs/apk/debug/app-debug.apk -> classes.dex(双击) -> 找到LogisticsCenter#loadRouterMap方法 -> 右键: show Bytecode。

1
2
3
4
5
6
7
8
9
10
11
12
// 不使用apply plugin: 'com.alibaba.arouter'
.method private static loadRouterMap()V
.registers 1

.line 64
const/4 v0, 0x0

sput-boolean v0, Lcom/alibaba/android/arouter/core/LogisticsCenter;->registerByPlugin:Z

.line 69
return-void
.end method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 使用apply plugin: 'com.alibaba.arouter'
.method private static loadRouterMap()V
.registers 1

.line 64
const/4 v0, 0x0

sput-boolean v0, Lcom/alibaba/android/arouter/core/LogisticsCenter;->registerByPlugin:Z

.line 69
const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava"

invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin"

invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi"

invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$app"

invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava"

invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Interceptors$$app"

invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava"

invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin"

invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi"

invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$app"

invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

return-void
.end method

对比发现,使用apply plugin: 'com.alibaba.arouter'后,这个方法增加了很多代码,基本上就是在加载路由路径表。使用这个gradle插件的基本思想就是,将查找路由路径表的过程,从运行时提前到了编译时,这算是一种AOT(Ahead of time)思想。

将最耗时的查找过程提前,也就解决了ARouter初始化时间过长的问题。

本文采用CC-BY-SA-3.0协议,转载请注明出处
Author: boybeak