TheRouter是货拉拉开源的路由库,也应用在货拉拉的部分产品中:如小拉出行用户端、司机端
Github: github.com/HuolalaTech…
官网: therouter.cn/
为什么需求运用路由结构
路由是现如今 Android 开发中必不可少的功能,尤其是企业级APP,能够用于将 Intent
页面跳转的强依靠联系解耦,同时减少跨团队开发的彼此依靠问题。如今的路由结构现已不只是是为了满足页面之间的跳转问题。
模块化开发的痛点
- 不合理的模块依靠联系,导致模块之间耦合度高,难以保护且编译时间长。
- 模块之间的跳转、彼此拜访艰难问题 。
为什么要运用TheRouter
-
路由思想和路由库现已不是一个新鲜的常识点了,早在有模块化开发架构就现已出现了处理痛点的法子了。现在市面是有许多路由库都能处理模块化问题,但各有优缺陷。咱们为什么选TheRouter,由于他的确有许多优点现现已过检测的。能够下面参阅官网这个表格:
- TheRouter有专门的团队在保护,并且是应用在自己公司的产品的,有问题能得到及时的处理,还能不断的迭代优化,信任这个库会越来越优异。
TheRouter源码剖析
接下来从源码的视点剖析TheRouter怎么处理模块化开发存在的痛点。我选了几个中心的功能,也是最常用的功能,至于TheRouter怎么运用,官网现已有很详细的文档。也能够参阅 货拉拉技能blog /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插桩