TheRouter是货拉拉开源的路由库,也应用在货拉拉的部分产品中:如小拉出行用户端、司机端
Github: github.com/HuolalaTech…
官网: therouter.cn/
为什么需要使用路由框架
路由是现如今 Android 开发中必不可少的功能,尤其是企业级APP,可以用于将 Intent
页面跳转的强依赖关系解耦,同时减少跨团队开发的互相依赖问题。如今的路由框架已经不仅仅是为了满足页面之间的跳转问题。
模块化开发的痛点
- 不合理的模块依赖关系,导致模块之间耦合度高,难以维护且编译时间长。
- 模块之间的跳转、相互访问艰难问题 。
为什么要使用TheRouter
-
路由思想和路由库已经不是一个新鲜的知识点了,早在有模块化开发架构就已经出现了解决痛点的法子了。目前市面是有很多路由库都能解决模块化问题,但各有优缺点。我们为什么选TheRouter,因为他确实有很多优点已经经过考验的。可以下面参考官网这个表格:
- TheRouter有专门的团队在维护,并且是应用在自己公司的产品的,有问题能得到及时的解决,还能不断的迭代优化,相信这个库会越来越优秀。
TheRouter源码分析
接下来从源码的角度分析TheRouter如何解决模块化开发存在的痛点。我选了几个核心的功能,也是最常用的功能,至于TheRouter如何使用,官网已经有很详细的文档。也可以参考 货拉拉技术blog juejin.cn/post/713971…
页面跳转实现
如果没有路由框架,我们要如何实现页面跳转以及跨模块之间的页面跳转呢?通常我们这样做:
Intent intent = new Intent(MainActivity.this,TestActivity.class);
startActivity(intent);
这个使用方式一点问题都没有,但如果是两个没有相互依赖的模块里,这就行不通了,当然也有解决的方案。比如:
try {
Intent intent = new Intent(MainActivity.this,Class.forName("com.therouter.demo.shell.TestActivity"));
startActivity(intent);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//或
Intent intent2 = new Intent();
intent2.setComponent(new ComponentName(MainActivity.this,"com.therouter.demo.shell.TestActivity"));
startActivity(intent2);
这种方式虽然解决了跨模块之间跳转的问题,但是也存在比较明显的缺点,且不说反射的问题,单就通过完整拼写包名类名字符串,后期的维护成本就足够高了,还有可能写的特别零散。
那如果用一个集合把映射关系管理起来呢? 比如:
HashMap<String,String> activityMap = new HashMap<>();
activityMap.put("xx://xxx.xxx.xxx","com.therouter.demo.shell.TestActivity");
Intent intent3 = new Intent();
intent3.setComponent(new ComponentName(MainActivity.this,activityMap.get("xx://xxx.xxx.xxx")));
startActivity(intent3);
虽然我们可以把目标Activity和协议管理起来,但是如果类文件挪个位置,或者我们新增一个 Activity 我们就得不断的维护集合,这是重复且固定工作。而路由库就可以解决这样的问题。
接下来我们来看看TheRouter如何处理这样的问题。TheRouter 的跳转很简单,不传参数的话,只需要简单两行代码。
-
在目标页面添加路由注解
@Route
,并设置相应的path
。@Route(path = BusinessAPathIndex.INJECT_TEST4) public class MultiThreadActivity extends AppCompatActivity { //... }
-
在需要跳转页面的调用
navigation
方法。TheRouter.build(BusinessAPathIndex.INJECT_TEST4).navigation();
使用起来确实非常简洁、方便,那是因为TheRouter帮我们做了很多工作。接下来从源码的角度看看TheRouter如何一步步实现路由跳转的。首先从 navigation
方法入手:
@JvmOverloads
fun navigation(ctx: Context?, fragment: Fragment?, requestCode: Int, ncb: NavigationCallback? = null) {
// 省略代码....
//从集合中找到对应的目标类
var match = matchRouteMap(matchUrl)
if (match != null) {
debug("Navigator::navigation", "NavigationCallback on found")
callback.onFound(this)
routerInterceptor.invoke(match!!) { routeItem ->
val intent = intent ?: Intent()
// 省略代码...
//设置跳转的目标类
intent. component = ComponentName(context!!. packageName , routeItem.className)
context.startActivity(intent)
// 省略代码...
}
callback.onArrival(this)
} else {
callback.onLost(this)
}
}
@Synchronized
fun matchRouteMap(url: String?): RouteItem? {
var path = TheRouter.build(url ?: "").simpleUrl
if (path.endsWith("/")) {
path = path.substring(0, path.length - 1)
}
// copy是为了防止外部修改影响路由表
val routeItem = ROUTER_MAP [path]?.copy()
// 由于路由表中的path可能是正则path,要用入参替换掉
routeItem?.path = path
return routeItem
}
从以上代码可以清楚的看出 navigation
方法通过我们传入的url参数从 ROUTER_MAP
集合查找 RouteItem
对象,并从 RouteItem
对象中获取到目标类的全类名作为 intent
的参数,实现页面跳转。
带着问题看源码
如上提到的 RouteItem
对象是如何生成的?ROUTER_MAP
集合又是如何生成的?
首先TheRouter是使用了APT+ASM插桩技术来解决这个问题,以下会一步步分析TheRouter是如何使用这两个技术来解决问题的。
APT(Annotation Processing Tool)
即注解处理器,在编译的时候可以处理注解然后做一些事情,如在编译时生成一些文件之类的。关于APT如何使用网上有非常多的文章介绍这方面的知识,实现也不难。总结就是一句话:我们提前对一些类设置注解,在编译过程中会扫描到我们需要的注解,通过注解的信息以及我们自己的业务来生成一些class文件,一般可以用来生成一些比较固定的模版代码。
-
在使用TheRouter的时候需要在目标类加一个注解,并且设置好对应的路由协议。
@Route(path = BusinessAPathIndex.INJECT_TEST2)
- 在
TheRouterAnnotationProcessor
中的getSupportedAnnotationTypes
方法添加需要处理的注解类型。以下是APT核心代码,可以看到TheRouterAnnotationProcessor
处理的注解其中就包含@Route
。项目的在编译的时候扫描到的有注解的类、方法的地方都会回调process
方法。这样我们就能在process
方法里获取被我们加了注解的类或方法的信息,根据我们自己的需要做相应的处理。
可以看到 process
方法中的 parseRoute
方法就是对 @Route
注解的信息进行了提取,并把每个注解的信息封装成 RouteItem
对象,最后将所有的信息放到一个集合中。
class TheRouterAnnotationProcessor : AbstractProcessor() {
private var isProcess = false
override fun getSupportedAnnotationTypes(): Set<String> {
val supportTypes: MutableSet<String> = HashSet()
// 省略代码...
supportTypes.add(Route::class.java.canonicalName)
return supportTypes
}
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
override fun process(set: Set<TypeElement?>, roundEnvironment: RoundEnvironment): Boolean {
if (!isProcess) {
isProcess = true
checkSingleton(roundEnvironment)
//处理注解 生成对应的class文件
genRouterMapFile(parseRoute(roundEnvironment))
// 省略代码...
}
return isProcess
}
private fun parseRoute(roundEnv: RoundEnvironment): List<RouteItem> {
val list: MutableList<RouteItem> = ArrayList()
val set = roundEnv.getElementsAnnotatedWith(Route::class.java)
// 省略代码...
if (set != null && set.isNotEmpty()) {
for (element in set) {
require(element.kind == ElementKind.CLASS) { element.simpleName.toString() + " is not class" }
val annotation = element.getAnnotation(Route::class.java)
val routeItem = RouteItem()
routeItem.className = element.toString()
routeItem.path = annotation.path
routeItem.action = annotation.action
routeItem.description = annotation.description
require(annotation.params.size % 2 == 0) { "$element params is not key value pairs" }
var key: String? = null
for (kv in annotation.params) {
if (key == null) {
key = kv
} else {
routeItem.params[key!!] = kv
key = null
}
}
list.add(routeItem)
}
}
return list
}
}
genRouterMapFile
方法则是将 RouteItem
集合根据业务逻辑生成class文件,作者这里使用的字符串拼接的方式,也可以使用 javapoet来制作 github.com/square/java… 。
private fun genRouterMapFile(pageList: List<RouteItem>) {
if (pageList.isEmpty()) {
return
}
val path = processingEnv.filer.createSourceFile(PACKAGE + POINT + PREFIX_ROUTER_MAP + "temp").toUri().toString()
// 确保只要编译的软硬件环境不变,类名就不会改变
val className = PREFIX_ROUTER_MAP + abs(path.hashCode()).toString()
val routePagelist = duplicateRemove(pageList)
val json = gson.toJson(routePagelist)
var ps: PrintStream? = null
try {
val jfo = processingEnv.filer.createSourceFile(PACKAGE + POINT + className)
val genJavaFile = File(jfo.toUri().toString())
if (genJavaFile.exists()) {
genJavaFile.delete()
}
ps = PrintStream(jfo.openOutputStream())
ps.println(String.format("package %s;", PACKAGE))
ps.println()
ps.println("/**")
ps.println(" * Generated code, Don't modify!!!")
ps.println(" * Created by kymjs, and APT Version is ${BuildConfig.VERSION}.")
ps.println(" */")
ps.println("@androidx.annotation.Keep")
ps.println(
String.format(
"public class %s implements com.therouter.router.IRouterMapAPT {",
className
)
)
ps.println()
ps.println("\tpublic static final String TAG = "Created by kymjs, and APT Version is ${BuildConfig.VERSION}.";")
ps.println("\tpublic static final String THEROUTER_APT_VERSION = "${BuildConfig.VERSION}";")
ps.println(String.format("\tpublic static final String ROUTERMAP = "%s";", json.replace(""", "\"")))
ps.println()
ps.println("\tpublic static void addRoute() {")
var i = 0
for (item in routePagelist) {
i++
ps.println("\t\tcom.therouter.router.RouteItem item$i = new com.therouter.router.RouteItem("${item.path}","${item.className}","${item.action}","${item.description}");")
item.params.keys.forEach {
ps.println("\t\titem$i.addParams("$it", "${item.params[it]}");")
}
ps.println("\t\tcom.therouter.router.RouteMapKt.addRouteItem(item$i);")
}
ps.println("\t}")
ps.println("}")
ps.flush()
} catch (e: Exception) {
e.printStackTrace()
} finally {
ps?.close()
}
}
最后产生了一个以 RouterMap__TheRouter_xxx 为前缀拼接数字的class文件,可以在模块对应的 build/generated/source/kapt/a 中查看。这个类只有一个静态方法 addRouter
,而方法内部都是很有规律的代码,都执行了 RouteMapKt.addRouteItem(xx)
。
package a;
/**
* Generated code, Don't modify!!!
* Created by kymjs, and APT Version is 1.1.0.
*/
@androidx.annotation.Keep
public class RouterMap__TheRouter__546452950 implements com.therouter.router.IRouterMapAPT {
public static final String TAG = "Created by kymjs, and APT Version is 1.1.0.";
public static final String THEROUTER_APT_VERSION = "1.1.0";
public static final String ROUTERMAP = "[{"path":"http://kymjs.com/business_a/testinject","className":"com.therouter.demo.shell.TestInjectActivity","action":"","description":"","params":{}},{"path":"http://kymjs.com/business_a/testinject3","className":"com.therouter.demo.shell.TestActivity","action":"","description":"","params":{}},{"path":"http://kymjs.com/business_a/testinject4","className":"com.therouter.demo.shell.MultiThreadActivity","action":"","description":"","params":{}},{"path":"http://kymjs.com/therouter/demo_service_provider","className":"com.therouter.demo.shell.MainActivity","action":"","description":"","params":{}}]";
public static void addRoute() {
com.therouter.router.RouteItem item1 = new com.therouter.router.RouteItem("http://kymjs.com/business_a/testinject","com.therouter.demo.shell.TestInjectActivity","","");
com.therouter.router.RouteMapKt.addRouteItem(item1);
// 省略代码...
}
}
而 RouteMapKt.addRouteItem(xx)
就是将目标类信息存储到路由集合 ROUTER_MAP
里的 。
@Synchronized
fun addRouteItem(routeItem: RouteItem) {
var path = routeItem.path
if (path.endsWith("/")) {
path = path.substring(0, path.length - 1)
}
debug("addRouteItem", "add $path")
ROUTER_MAP [path] = routeItem
onRouteMapChangedListener?.onChanged(routeItem)
}
以上的源码解释了路由信息如何生成,如何添加到路由表里的,但是有个问题:addRoute()
是在什么时候调用的,这里就要使用到另一个技术知识 ASM字节码插桩技术。
回过头来看下TheRouter初始化的时候做了什么事:从初始化的过程中依次调用了
init
-> asyncInitRouteMap
-> initDefaultRouteMap
, 但是 initDefaultRouteMap
是个空方法,
@JvmStatic
fun init(context: Context?) {
if (!inited) {
// 省略代码...
asyncInitRouteMap()
// 省略代码...
}
}
fun asyncInitRouteMap() {
execute {
// 省略代码...
initDefaultRouteMap ()
// 省略代码...
}
}
@file:JvmName("TheRouterServiceProvideInjecter")
package a
import android.content.Context
import com.therouter.flow.Digraph
/**
* Created by ZhangTao on 18/2/24.
*/
fun trojan() {}
fun autowiredInject(obj: Any?) {}
fun addFlowTask(context: Context?, digraph: Digraph) {}
fun initDefaultRouteMap() {}
作者在这里设计几个空方法也是非常巧妙的,因为我们要使用字节码插桩技术,往往需要找到一个合适的点来添加我们的代码。接下来看看作者是如何使用ASM字节码插桩技术来实现路由集合的初始化问题。ASM字节码插桩技术,网上也有非常多的文章,可以自行下学习。
ASM
集成TheRouter需要在App模块中添加插件 apply plugin: 'therouter'
此插件主要就是处理字节码插桩问题。插件的入口在 TheRouterTransform
类中,主要的核心逻辑就遍历所有的jar包和class文件。
@Override
void transform(Context context, Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider,
boolean isIncremental)
throws IOException, javax.xml.crypto.dsig.TransformException, InterruptedException {
theRouterTransform(isIncremental, inputs, outputProvider)
}
private void theRouterTransform(boolean isIncremental, Collection<TransformInput> inputs, outputProvider) {
inputs.each { TransformInput input ->
// 遍历jar包
input.jarInputs.each { JarInput jarInput ->
//...
TheRouterInjects.tagJar(jarInput.file)
//...
}
// 遍历源码
input.directoryInputs.each { DirectoryInput directoryInput ->
//...
TheRouterInjects.tagClass(inputFile.absolutePath)
//...
}
//插入代码
if (theRouterClassOutputFile) {
TheRouterInjects.injectClassCode(theRouterClassOutputFile, isIncremental)
}
}
遍历jar比遍历class文件多一步解包的过程,最终的目的就是遍历所有的class文件并找到我们需要做字节码插桩的class。
-
找到我们刚刚使用APT生成的前缀为RouterMap__TheRouter__xxxx数字的类信息,并保存起来。
private static final PREFIX_ROUTER_MAP = "RouterMap__TheRouter__" public static JarInfo tagJar(File jarFile) { JarInfo jarInfo = new JarInfo() if (jarFile) { def file = new JarFile(jarFile) Enumeration enumeration = file.entries() while (enumeration.hasMoreElements()) { JarEntry jarEntry = (JarEntry) enumeration.nextElement() // 省略代码... if (jarEntry.name.contains(PREFIX_ROUTER_MAP)) { routeSet.add(jarEntry.name) // 省略代码... } } } return jarInfo }
-
遍历jar找到
TheRouterServiceProvideInjecter
类,之后就是对TheRouterServiceProvideInjecter
类的空方法做字节码插桩操作//插入代码 if (theRouterClassOutputFile) { TheRouterInjects.injectClassCode(theRouterClassOutputFile, isIncremental) }
/** * 开始修改 TheRouterServiceProvideInjecter 类 */ static void injectClassCode(File inputJarFile, boolean isIncremental) { // 省略代码... while (enumeration.hasMoreElements()) { JarEntry jarEntry = (JarEntry) enumeration.nextElement(); String entryName = jarEntry.getName() ZipEntry zipEntry = new ZipEntry(entryName) jarOutputStream.putNextEntry(zipEntry) InputStream inputStream = inputJar.getInputStream(jarEntry) byte[] bytes if (entryName.contains("TheRouterServiceProvideInjecter")) { ClassReader cr = new ClassReader(inputStream) ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES) //对TheRouterServiceProvideInjecter类中的方法进行修改 AddCodeVisitor cv = new AddCodeVisitor (cw, serviceProvideMap , autowiredSet , routeSet , isIncremental) cr.accept(cv, ClassReader.SKIP_DEBUG) bytes = cw.toByteArray() } else { bytes = IOUtils.toByteArray(inputStream) } jarOutputStream.write(bytes) jarOutputStream.closeEntry() } // 省略代码... }
AddCodeVisitor
主要做的事就是过滤类的方法,并做一些代码插入操作,具体可以学习ASM字节码插桩知识。此处是查找 TheRouterServiceProvideInjecter
类的 initDefaultRouteMap
方法插入 RouterMap__TheRouter__2107448941.addRoute()
这类型的代码。
public class AddCodeVisitor extends ClassVisitor {
// 省略代码...
@Override
public MethodVisitor visitMethod(int access, String methodName, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, methodName, desc, signature, exceptions);
mv = new AdviceAdapter(Opcodes.ASM7, mv, access, methodName, desc) {
@Override
protected void onMethodEnter() {
super.onMethodEnter();
//不代理构造函数
if (!"<init>".equals(methodName)) {
//...
//这里正是我们刚刚找到RouterMap__TheRouter__xxxx对集合
for (String route : routeList) {
if ("initDefaultRouteMap".equals(methodName)) {
Label tryStart = new Label();
Label tryEnd = new Label();
Label labelCatch = new Label();
Label tryCatchBlockEnd = new Label();
mv.visitTryCatchBlock(tryStart, tryEnd, labelCatch, "java/lang/Exception");
mv.visitLabel(tryStart);
String className = route.replace(".class", "").replace('.', '/');
mv.visitMethodInsn(INVOKESTATIC, className, "addRoute", "()V", false);
mv.visitLabel(tryEnd);
mv.visitJumpInsn(GOTO, tryCatchBlockEnd);
mv.visitLabel(labelCatch);
mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Exception"});
mv.visitVarInsn(ASTORE, 0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Exception", "printStackTrace", "()V", false);
mv.visitLabel(tryCatchBlockEnd);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
}
}
if (!isIncremental) {
mv.visitInsn(RETURN);
}
}
}
};
return mv;
}
}
修改之后,我们可以通过反编译查看修改后的类信息,以下就是字节码插桩之后的代码,可以看到原先的空方法现在多了许多有规则的代码。
public final class TheRouterServiceProvideInjecter {
// 省略代码...
public static final void initDefaultRouteMap() {
try {
RouterMap__TheRouter__2107448941.addRoute();
} catch (Exception e) {
e.printStackTrace();
}
try {
RouterMap__TheRouter__546452950.addRoute();
} catch (Exception e2) {
e2.printStackTrace();
}
}
// 省略代码...
}
至此路由信息如何生成,如何添加到路由集合中,如何被使用从源码中大致就梳理通了。通过以上源码分析我们已经知道了TheRouter是如何通过路由跳转到目标页面的,但是我们的业务往往还需要向目标页面传递参数,接下来我们看看TheRouter是如何处理参数传递的问题的。
数据传递实现
一般我们通过 intent
来携带需要传递数据,然后在目标页面通过 intent
来获取传递的数据,这是非常常规的方式,这种方式实用但是在使用了路由之后就行不通了。
Intent intent = new Intent(MainActivity.this,TestInjectActivity.class);
//传递参数
intent.putExtra("KEY","传递数据");
startActivity(intent);
//获取传递的参数
String data = getIntent().getStringExtra("KEY");
再看看TheRouter是如何传递数据到目标页面的:
-
第一步在目标页面定义变量,添加
@Autowired
,之后调用TheRouter.inject(this)
注入@Route(path = HomePathIndex.HOME) public class NavigatorTargetActivity extends AppCompatActivity { // .withInt("intValue", 12345678) // 测试传 int 值 // .withString("stringIntValue", "12345678")// 测试用 string 传 int 值 // .withString("str_123_Value", "测试传中文字符串")// 测试 string // .withString("boolParseError", "非boolean值") // 测试用 boolean 解析字符串的情况 // .withString("shortParseError", "12345678") // 测试用 short 解析超长数字的情况 // .withBoolean("boolValue", true) // 测试 boolean // .withLong("longValue", 123456789012345L) // 测试 long // .withChar("charValue", 'c') // 测试 char // .withDouble("double", 3.14159265358972)// 测试double,key与关键字冲突 // 测试int值传递 @Autowired int intValue; @Autowired String stringIntValue; @Autowired String str_123_Value; @Autowired boolean boolParseError; @Autowired short shortParseError; @Autowired boolean boolValue; @Autowired Long longValue; @Autowired char charValue; @Autowired(name = "double") double doubleValue; @Autowired float floatValue; @Autowired String strFromAnnotation; // 来自注解设置的默认值,允许路由动态修改 @Autowired(id = R.id.button1) Button button1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.navigator_target); setTitle("导航落地页1"); // Autowired注入,这一行应该写在BaseActivity中 TheRouter.inject(this); } }
-
在调起跳转的地方则使用
with
系列方法设置相应的键值和数据,TheRouter.build(HomePathIndex.HOME) .withInt("intValue", 12345678) // 测试传 int 值 .withString("stringIntValue", "12345678")// 测试用 string 传 int 值 .withString("str_123_Value", "测试传中文字符串")// 测试 string .withString("boolParseError", "非boolean值") // 测试用 boolean 解析字符串的情况 .withString("shortParseError", "12345678") // 测试用 short 解析超长数字的情况 .withBoolean("boolValue", true) // 测试 boolean .withLong("longValue", 123456789012345L) // 测试 long .withChar("charValue", 'c') // 测试 char .withDouble("double", 3.14159265358972)// 测试double,key与关键字冲突 .withFloat("floatValue", 3.14159265358972F)// 测试float,自动四舍五入 .navigation();
简简单单的两步解决了数据传递问题,而且 TheRouter 这个 @Autowired
注解还支持控件初始化和自定义注入规则,这个是真的强大。
Autowired 实现
接下来源码分析看看TheRouter关于 @Autowired
注解的骚操作。 @Autowired
注解也是用到APT技术,APT的主要流程是类似的,可以看到 TheRouterServiceProvideInjecter
类中几个空方法中有一个 autowiredInject
方法。这个就是用来处理参数传递自动注入的问题的,页面的参数传递其原始形式无非就是 intent.putExtra()
、intent.getXX()
,路由也是不例外的,只是使用模版代码来解决这些固定的操作而已。
-
首先TheRouter使用APT技术处理
@Autowired
注解,并提取相关信息封装成AutowiredItem
对象,并存储起来。最后生成XXX__TheRouter__Autowired
的class文件 可以在模块对应的build/generated/source/kapt/a
中查看class TheRouterAnnotationProcessor : AbstractProcessor() { private var isProcess = false override fun getSupportedAnnotationTypes(): Set<String> { val supportTypes: MutableSet<String> = HashSet() // 省略代码... supportTypes.add(Autowired::class.java.canonicalName) // 省略代码... return supportTypes } override fun getSupportedSourceVersion(): SourceVersion { return SourceVersion.latestSupported() } override fun process(set: Set<TypeElement?>, roundEnvironment: RoundEnvironment): Boolean { if (!isProcess) { isProcess = true // 省略代码... val autowiredItems = parseAutowired(roundEnvironment) genAutowiredJavaFile(autowiredItems) // 省略代码... } return isProcess } //处理Autowired注解 封装成AutowiredItem对象并存储起来 private fun parseAutowired(roundEnv: RoundEnvironment): Map<String, MutableList<AutowiredItem>> { val map: MutableMap<String, MutableList<AutowiredItem>> = HashMap() val set = roundEnv.getElementsAnnotatedWith(Autowired::class.java) for (element in set) { require(element.kind == FIELD) { element.simpleName.toString() + " is not field" } val annotation = element.getAnnotation(Autowired::class.java) val autowiredItem = AutowiredItem() autowiredItem.key = annotation.name.trim { it <= ' ' } if (autowiredItem.key == "") { autowiredItem.key = element.toString() } autowiredItem.args = annotation.args autowiredItem.fieldName = element.toString() autowiredItem.required = annotation.required autowiredItem.id = annotation.id autowiredItem.description = annotation.description autowiredItem.type = element.asType().toString() autowiredItem.className = element.enclosingElement.toString() var list = map[autowiredItem.className] if (list == null) { list = ArrayList() } list.add(autowiredItem) list.sort() map[autowiredItem.className] = list } return map } }
-
最终生成的
XX_TheRouter__Autowired
class文件,可以在模块对应的build/generated/source/kapt/a
中查看,如:@androidx.annotation.Keep public class MainActivity__TheRouter__Autowired { public static final String TAG = "Created by kymjs, and APT Version is 1.1.0."; public static final String THEROUTER_APT_VERSION = "1.1.0"; public static void autowiredInject(com.therouter.demo.shell.MainActivity target) { for (com.therouter.router.interceptor.AutowiredParser parser : com.therouter.TheRouter.getParserList()) { Integer variableName0 = parser.parse("int", target, new com.therouter.router.AutowiredItem("int","intValue",0,"","com.therouter.demo.shell.MainActivity","intValue",false,"No desc.")); if (variableName0 != null){ target.intValue = variableName0; } java.lang.String variableName1 = parser.parse("java.lang.String", target, new com.therouter.router.AutowiredItem("java.lang.String","str_123_Value",0,"","com.therouter.demo.shell.MainActivity","str_123_Value",false,"No desc.")); if (variableName1 != null){ target.str_123_Value = variableName1; } } } }
-
之后 TheRouter 通过 ASM 字节码插桩技术将APT生成的
XX_TheRouter__Autowired
类的方法插入到TheRouterServiceProvideInjecter
类中的autowiredInject
方法中,最终得到这样的产物,可以反编译查看:public final class TheRouterServiceProvideInjecter { // 省略代码... public static final void autowiredInject(Object obj) { if ("com.therouter.app.navigator.NavigatorTargetActivity__TheRouter__Autowired".contains(obj.getClass().getName())) { try { NavigatorTargetActivity__TheRouter__Autowired.autowiredInject((NavigatorTargetActivity) obj); } catch (Exception e) { } } // 省略代码... } }
- 最终我们看看整个注入流程,首先是调用
TheRouter.inject(this)
,实际就是调用了autowiredInject
,因为autowiredInject
的方法体被注入了APT生成的代码,所以最后调用了APT生成的 xx_TheRouter__Autowired 类中的autowiredInject
方法。这个时候使用 TheRouter 提供的各种解析器来对参数做赋值操作,TheRouter提供了四种默认的解析器 :DefaultIdParser
、DefaultObjectParser
、DefaultServiceParser
、DefaultUrlParser
, 其中DefaultUrlParser
就是对路由跳转携带的数据做赋值用的 。
TheRouter 在跳转的时候将数据通过 with
系列方法存储到 extras
中,最终在执行 navigation
是设置到 intent
里。
//设置要携带的数据
fun withString(key: String?, value: String?): Navigator {
extras.putString(key, value)
return this
}
@JvmOverloads
fun navigation(ctx: Context?, fragment: Fragment?, requestCode: Int, ncb: NavigationCallback? = null) {
// 省略代码...
match?.getExtras()?.putAll(extras)
// reset callback
TheRouterLifecycleCallback.setActivityCreatedObserver {}
if (match != null) {
debug("Navigator::navigation", "NavigationCallback on found")
callback.onFound(this)
routerInterceptor.invoke(match!!) { routeItem ->
with(routeItem.getExtras()) {
val bundle: Bundle? = getBundle(KEY_BUNDLE)
if (bundle != null) {
remove(KEY_BUNDLE)
intent.putExtra(KEY_BUNDLE, bundle)
}
intent.putExtras(this)
}
}
callback.onArrival(this)
} else {
callback.onLost(this)
}
}
在变量注入的时候从解析器中获取值并对字段赋值,而解析器 DefaultUrlParser
也是从 intent
中获取值的。
TheRouter.inject(this);
@JvmStatic
fun inject(any: Any?) {
autowiredInject(any)
}
@androidx.annotation.Keep
public class MainActivity__TheRouter__Autowired {
public static final String TAG = "Created by kymjs, and APT Version is 1.1.0.";
public static final String THEROUTER_APT_VERSION = "1.1.0";
public static void autowiredInject(com.therouter.demo.shell.MainActivity target) {
for (com.therouter.router.interceptor.AutowiredParser parser : com.therouter.TheRouter.getParserList()) {
Integer variableName0 = parser.parse("int", target, new com.therouter.router.AutowiredItem("int","intValue",0,"","com.therouter.demo.shell.MainActivity","intValue",false,"No desc."));
//赋值操作
if (variableName0 != null){
target.intValue = variableName0;
}
java.lang.String variableName1 = parser.parse("java.lang.String", target, new com.therouter.router.AutowiredItem("java.lang.String","str_123_Value",0,"","com.therouter.demo.shell.MainActivity","str_123_Value",false,"No desc."));
if (variableName1 != null){
target.str_123_Value = variableName1;
}
}
}
}
class DefaultUrlParser : AutowiredParser {
override fun <T> parse(type: String?, target: Any?, item: AutowiredItem?): T? {
if (item?.id != 0) {
return null
}
if ("java.lang.String".equals(type, ignoreCase = true) || "String".equals(type, ignoreCase = true)) {
when (target) {
is Activity -> {
//从intent中取值
return target.intent?.extras?.get(item.key)?.toString() as T?
}
is Fragment -> {
return target.arguments?.get(item.key)?.toString() as T?
}
is androidx.fragment.app.Fragment -> {
return target.arguments?.get(item.key)?.toString() as T?
}
}
}
}
}
跨模块方法调用实现
模块化开发并不是仅仅只需要解决页面的跳转问题,也会存在模块之间业务相关联,需要使用到其他模块现有的能力,这时候跨模块方法调用就非常必要了。
假设我们的项目依赖是下图这样的结构:
因为 module1
和 module2
没有依赖关系,这样 module1
和 module2
则无法相互使用对方提供的能力,解决这样的问题方法很多,可以改变依赖关系,可以把需要被使用的能力都下沉到 base
模块里。但是这样显然不是我们想要的。还有一种解决方案,就是在 base
模块里定义接口,在 module
里实现接口,这样每个 module
因为都依赖了 base
模块,从而可以直接调用相应的接口,这样就变成面向接口了。那么我们只要解决一个问题,就是找到接口的具体实现类。有兴趣的可以了解下 SPI
全称是 Service Provider Interface
,是一种将服务接口与服务实现分离以达到解耦、可以提升程序可扩展性的机制。
下面看下TheRouter是如何解决这个问题的?
TheRouter 跨模块方法调用使用是在 base 模块中定义接口,在具体需要提供能力的 module
中实现并以 @ServiceProvider
注解标记,步骤如下:
- 在
base
模块中定义接口。
public interface IUserService {
String getUserInfo();
}
- 在提供能力的模块中实现接口并
@ServiceProvider
注解标记。
/**
* 方法名不限定,任意名字都行,方法可以写在任何地方
* 返回值必须是服务接口名,如果是实现了服务的子类,需要加上returnType限定
* 方法必须加上 public static 修饰,否则编译期就会报错
*/
@ServiceProvider
public static IUserService test() {
return new IUserService() {
@Override
public String getUserInfo() {
return "这是用户信息";
}
};
}
- 使用方通过
TheRouter.get
获取实现类
TheRouter.get(IUserService.class).getUserInfo()
从前面的路由跳转和参数传递可以看到都用到了注解,那么老思路,既然用注解标记了,那么APT必然就有一个收集注解信息的过程,就从这里入手。
从APT源码中可以看到,思路和上面的两个篇章是一样的。提取注解信息,构建class文件
override fun process(set: Set<TypeElement?>, roundEnvironment: RoundEnvironment): Boolean {
if (!isProcess) {
isProcess = true
// 省略代码...
//收集ServiceProvider注解的信息
val providerItemList = parseServiceProvider(roundEnvironment)
// 省略代码...
//生成对应的class文件
genJavaFile(providerItemList, flowTaskList)
}
return isProcess
}
接下来看看生成的class文件,生成的文件是以 ServiceProvider__TheRouter_xx
开头的,可以在模块对应的 build/generated/source/kapt/a
中查看。
@androidx.annotation.Keep
public class ServiceProvider__TheRouter__526662154 implements com.therouter.inject.Interceptor {
public static final String TAG = "Created by kymjs, and APT Version is 1.1.0.";
public static final String THEROUTER_APT_VERSION = "1.1.0";
public static final String FLOW_TASK_JSON = "{"businessB_interceptor":"TheRouter_activity_splash"}";
public <T> T interception(Class<T> clazz, Object... params) {
T obj = null;
// 省略代码...
if (com.therouter.demo.di.IUserService.class.equals(clazz) && params.length == 0 ) {
// 加上编译期的类型校验,防止方法实际返回类型与注解声明返回类型不匹配
com.therouter.demo.di. IUserService retyrnType = com.therouter.demo.b.Test. test ();
obj = (T) retyrnType;
}
// 省略代码...
return obj;
}
// 省略代码...
}
可以看到最终创建了一个实现 com.therouter.inject.Interceptor
拦截器的类,并且在实现类中通过类型判断找到接口的实现类,以上以 IUserService
为例。
既然找到接口的实现类,那必然有个地方需要调用到 interception
方法,我们从使用方的角度来看看使用方是从什么地方调用这个方法的。首先看看 TheRouter.get
做了什么
TheRouter.get(IUserService.class).getUserInfo()
@JvmStatic
fun <T> get(clazz: Class<T>, vararg params: Any?): T? {
return routerInject.get(clazz, *params)
}
operator fun <T> get(clazz: Class<T>, vararg params: Any?): T? {
//...
if (temp == null) {
temp = createDI(clazz, *params)
if (temp != null) {
temp = mRecyclerBin.put(clazz, temp, *params)
}
}
return temp
}
可以看到在 createDI
方法中从拦截器的集合中去寻找对应的目标类
private fun <T> createDI(tClass: Class<T>, vararg params: Any?): T? {
var t: T? = null
//查找自定义拦截器
for (f in mCustomInterceptors) {
t = f.interception(tClass, *params)
}
//查找 ServiceProvider
//首先保证不会在读取的时候另一个线程不会对集合有增删操作
mInterceptors.readLock().lock()
for (f in mInterceptors) {
t = f.interception(tClass, *params)
}
// 省略代码...
return t
}
接下来只要找到拦截器集合是如何初始化的,如何把我们APT生成的拦截器添加进去的。我们在来看看TheRouter初始化的时候做的事。
@JvmStatic
fun init(context: Context?) {
if (!inited) {
// 省略代码...
routerInject.asyncInitRouterInject(context)
// 省略代码...
}
}
可以看到13行代码 routerInject.asyncInitRouterInject(context)
fun asyncInitRouterInject(context: Context?) {
// 省略代码...
execute {
trojan ()
}
// 省略代码...
}
不用多说了,trojan
又是作者预先提供的空方法,肯定是和字节码插桩相关了,源码参考插件的 AddCodeVisitor
类,思路参考路由跳转篇。
我们看看最终的产物,可以反编译查看。
@Metadata(d1 = {"\u0000\u001e\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\u001a\u0018\u0010\u0000\u001a\u00020\u00012\b\u0010\u0002\u001a\u0004\u0018\u00010\u00032\u0006\u0010\u0004\u001a\u00020\u0005\u001a\u0010\u0010\u0006\u001a\u00020\u00012\b\u0010\u0007\u001a\u0004\u0018\u00010\b\u001a\u0006\u0010\t\u001a\u00020\u0001\u001a\u0006\u0010\n\u001a\u00020\u0001¨\u0006\u000b"}, d2 = {"addFlowTask", "", "context", "Landroid/content/Context;", "digraph", "Lcom/therouter/flow/Digraph;", "autowiredInject", "obj", "", "initDefaultRouteMap", "trojan", "router_release"}, k = 2, mv = {1, 5, 1}, xi = 48)
/* loaded from: classes.dex */
public final class TheRouterServiceProvideInjecter {
public static final void trojan() {
try {
TheRouter.getRouterInject().privateAddInterceptor(new ServiceProvider__TheRouter__183396771());
} catch (Exception e) {
e.printStackTrace();
}
try {
TheRouter.getRouterInject().privateAddInterceptor(new ServiceProvider__TheRouter__1995163322());
} catch (Exception e2) {
e2.printStackTrace();
}
// 省略代码...
}
}
方法很清晰,就是把APT生成的 ServiceProvider__TheRouter_XX
拦截器添加到拦截器 mInterceptors
集合中。
@Keep
fun privateAddInterceptor(factory: Interceptor) {
mInterceptors.add(factory)
}
至此TheRouter的跳转,数据传递,跨模块方法调用思路源码分析就差不多了。
总结一下整体的思路:注解标记,APT技术收集对应的注解信息,生成模版代码,利用ASM字节码插桩技术在合适的地方插入代码打通整个流程。
TheRouter在最新的版本中已经支持了KSP去做注解处理,效率更高,但整体的实现逻辑依然是:注解标记->KSP解析->生成模板代码->ASM插桩