现在最流行的路由框架应该是阿里的ARouter,这几乎是组件化应用的必备了。但是ARouter用起来稍微有一点不爽,不爽在以下两点:

  1. 没有一个规范化的api式的调用方式:项目大了,调用路由的方法分布在项目各处,难以查找;
  2. 对startActivityForResult支持不够友好:按照传统方式,在onActivityResult中处理,比较分散。

基于以上问题,闲来无事,手撸一个自己的路由框架IRouter,基本使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface IRouterService {
@RouteTo("topic/detail")
fun topicDetail(@Key("topic") topic: Topic): Navigator
}

val iRouter = IRouter.Builder()
.isDebug(BuildConfig.DEBUG)
.errorActivity(ErrorActivity::class.java)
.build()
.create(IRouterService::class.java)

iRouter.topicDetail(topic).startActivity(this@MainActivity)

// OR

iRouter.topicDetail(topic).startActivityForResult(this, 100) { requestCode, resultCode, data ->
}

具体的配置方式请参考IRouter,本文主要是解析源码。

如此调用方式,很像是Retrofit的方式,打开一个activity就像请求一个api一样。从这里可以体现出解决了上述的两个痛点:

  1. 类似API的调用方式,集中管理路由路径;
  2. startActivityForResult中添加回调,哪里调用,就在哪里处理结果,结构紧凑。

下面进入源码解析。

ARouter源码分析这篇文章一样,我们分析时候要按照时态去分析这个框架在运行时编译时做的事情。

运行时

其实从上述调用的方式,有过热门开源框架源码阅读经验的,都能猜出个大概。

先从IRouter这个类创建IRouterService实例说起。使用过Retrofit的同学都知道,创建一个接口类,通过注解标注方法,不用提供具体的实现流程,就能完成网络请求。其实这并不难,这是通过动态代理实现的。我们来看IRouter.create的代码:

1
2
3
4
5
6
7
8
9
public <T> T create(Class<T> tClass) {
return (T) Proxy.newProxyInstance(tClass.getClassLoader(), new Class[]{tClass},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return parseMethod(method, args);
}
});
}

通过动态代理,我们创建了一个IRouterService的实现类,这个类在调用相关方法的时候,比如topicDetail这个方法,都会经由InvocationHandlerinvoke方法来代理完成。

接下来,从invoke中调用了parseMethod方法,这个方法比较简单,主要是用于解析方法注解和参数注解,取出其中的值,比如跳转路径path和参数的key - value键值对,通过path查询出相对应的Activity的class,这些值最终汇总起来,返回一个Navigator对象,这个就是真正要执行跳转的地方。

上面提到通过path查询出Activity对应的class,既然要查询,肯定要事先存储后才能被查询到。这就涉及到编译时做的工作了。

编译时

一、APT部分

其实读过其他一些开源框架的人对这部分一定不会陌生。

这部分工作与ARouter类似,就是通过**@RoutePath**注解标记目标Activity,然后再通过注解处理器来获取到path - activity.class的对应关系,将这个对应关系,生成成一个类,我们查看一个生成的类的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.github.boybeak.irouter.loader;

import com.github.boybeak.irouter.core.BaseLoader;
import java.lang.Override;
import java.lang.String;

public class V2ex$Topic$Loader extends BaseLoader {
@Override
public String getHeader() {
return "topic";
}

@Override
public void loadIntoMap() {
load("detail", com.v2ex.activity.TopicActivity.class);
}
}

这些所有生成的类在一个包名com.github.boybeak.irouter.loader底下,这很重要,因为我们要在接下来的过程,通过这个包名去筛选生成的loader类。

理解这部分,需要对APT(注解处理器)和java poet比较了解。

二、ASM部分

不太了解ASM的,可以通过这篇文章ASM库介绍与使用来了解,简单来说,ASM就是一款修改class文件的工具。能用来动态生成class文件,也可以修改已经存在的class文件。

有这样的利器,我们能做的事就太多了。

这部分,其实我就是参考了ARouter的做法,改成了自己的一些逻辑。

接下来我们要编写的是一个gradle plugin,我们主要是利用其中的Transform工具,官方解释在这里,其作用就是在编译时,会挨个遍历我们的源码、类库、jar包等。附上一个教程Gradle 学习之 Android 插件的 Transform API

Path - activity.class的对应关系是通过LoaderManager来查询的,我们看一下这个类的代码:

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
public final class LoaderManager {
private static final LoaderManager sManager = new LoaderManager();

public static LoaderManager getInstance() {
return sManager;
}

private final Map<String, DelegateLoader> loadersMap = new HashMap<>();
private boolean isInitialized = false;

private LoaderManager() {
init();
}

private void init() {
if (isInitialized) {
return;
}
load();
isInitialized = true;
}

private void load() {
}

private void loadInto(BaseLoader loader) {
String header = loader.getHeader();
obtainLoader(header).mergeOtherLoaders(loader);
}

private DelegateLoader obtainLoader(String header) {
DelegateLoader delegateLoader = loadersMap.get(header);
if (delegateLoader == null) {
delegateLoader = new DelegateLoader(header);
loadersMap.put(header, delegateLoader);
}
return delegateLoader;
}

public Class<?> get(String path) {
String[] segments = path.split("/");
final String header = segments[0];
final String tail = segments[1];
return loadersMap.get(header).getTargetClass(tail);
}

}

我们需要注意其中的一个方法——load,我们看到,这个类在构建方法里调用了init方法,init里又调用了load方法,但是这个load方法却是留白的。这样调用有什么用呢?

其实这个留白方法是我们为ASM留的一个修改的入口。

1
apply plugin: 'i-router-register'

在app.gradle中,使用这个插件,我们的transform就能顺利运行起来发挥作用了。

我们需要查看一下RegisterTransform的代码了:

1
2
3
4
5
6
7
8
9
10
public class RegisterTransform extends Transform {
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);

scanner.scan(transformInvocation, (loaderManagerJar, loaderManagerEntryName, loaders) -> {
Asm.getInstance().generateCode(loaderManagerJar, loaderManagerEntryName, loaders);
});
}
}

这里使用了Scanner来扫描TransformInvocation类,Scanner是我们自定义的类,主要作用就是通过包名和类名,查找我们的loader类和LoaderManager所在的jar包。我们查看其scan方法:

1
2
3
4
5
6
7
8
9
for (TransformInput input : transformInvocation.getInputs()) {
for (JarInput jarInput : input.getJarInputs()) {
// 这里处理第三方类库,引用的module和jar文件
}
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
// 这里处理应用了这个plugin的module的相关class文件
}
}
onScanFinish.onScanFinish(loaderManagerJar, loaderManagerEntryName, loaderClzList);

通过回调,我们将查找到的loader类和LoaderManager所在jar返回给transform,并交由我们的asm工具来处理。

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 class ASM {
private static class HackMethodVisitor extends MethodVisitor {

private List<String> loaders = null;

public HackMethodVisitor(int api, MethodVisitor methodVisitor, List<String> loaders) {
super(api, methodVisitor);
this.loaders = loaders;
}

@Override
public void visitInsn(int opcode) {
for (String loader : loaders) {
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitTypeInsn(Opcodes.NEW, loader);
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, loader, "<init>", "()V", false);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/github/boybeak/irouter/core/LoaderManager", "loadInto", "(Lcom/github/boybeak/irouter/core/BaseLoader;)V", false);
}

super.visitInsn(opcode);
}
}
}

主要的修改逻辑就在HackMethodVisitor这个类中,注意其中的visitInsn方法,这就是ASM真正发挥作用的地方,这里的逻辑就是为loadInto方法添加加载loader的代码。

以demo中的v2ex为例,修改后的代码load代码如下:

LoaderManager

最后一点细节

到目前为止,主要的流程已经结束了,接下来是一些细节部分。

  • LoaderManager在加载loader的时候,会针对path做归并,比如同为app/main和app/user被归并为一组,这样在使用的时候,可以按组做实际载入。
  • 用于跳转的Navigator是有缓存的,用来减少查询次数,因为path - activity.class的对应关系并不是动态变化的,如果缓存中有已经用过的,则清空其intent的参数部分重复利用即可。
  • 对于startActivityForResult的集中调用,可以参考我的另外一个开源项目——Starter中的SAFR项目。这里在FragmentActivity中,通过一个fragment去代理了startActivityForResult的过程,从而拦截了回调结果;类似的,在非FragmentActivity中,通过了一个全透明的代理了此过程,这里学习了Glide通过一个fragment来探测生命周期的方式。

总结

通过这样一个自己动手的过程,我们熟悉了的编写APT和gradle plugin的过程,学会了ASM的基本用法。

在这个项目中,学习了很多其他优秀开源项目的经验,比如:

  • 主流程是参考了ARouter,但是去掉了应用启动时候,从codeDir找到apk来解析路由路径的过程;
  • 集中的api式调用,参考了Retrofit的动态代理;
  • 通过Fragment拦截startActivityForResult的结果,参考了Glide向宿主activity添加无UI的Fragment的方式。

因此,多读源码可以开拓思维,当自己想开发自己的工具框架时候,就可以信手拈来。

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