抽丝剥茧带你探索 TheRouter

3,040 阅读18分钟

TheRouter是货拉拉开源的路由库,也应用在货拉拉的部分产品中:如小拉出行用户端、司机端

Github: github.com/HuolalaTech…

官网: therouter.cn/

为什么需要使用路由框架

路由是现如今 Android 开发中必不可少的功能,尤其是企业级APP,可以用于将 Intent 页面跳转的强依赖关系解耦,同时减少跨团队开发的互相依赖问题。如今的路由框架已经不仅仅是为了满足页面之间的跳转问题。

模块化开发的痛点

  1. 不合理的模块依赖关系,导致模块之间耦合度高,难以维护且编译时间长。
  1. 模块之间的跳转、相互访问艰难问题 。

为什么要使用TheRouter

  1. 路由思想和路由库已经不是一个新鲜的知识点了,早在有模块化开发架构就已经出现了解决痛点的法子了。目前市面是有很多路由库都能解决模块化问题,但各有优缺点。我们为什么选TheRouter,因为他确实有很多优点已经经过考验的。可以下面参考官网这个表格:

  1. 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 的跳转很简单,不传参数的话,只需要简单两行代码。

  1. 在目标页面添加路由注解 @Route,并设置相应的 path

    @Route(path = BusinessAPathIndex.INJECT_TEST4)
    public class MultiThreadActivity extends AppCompatActivity {
     //...
    }
    
  1. 在需要跳转页面的调用 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文件,一般可以用来生成一些比较固定的模版代码。

  1. 在使用TheRouter的时候需要在目标类加一个注解,并且设置好对应的路由协议。

    @Route(path = BusinessAPathIndex.INJECT_TEST2)
    
  1. 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。

  1. 找到我们刚刚使用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
    }
    
  1. 遍历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是如何传递数据到目标页面的:

  1. 第一步在目标页面定义变量,添加 @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);
         }
     }
    
  1. 在调起跳转的地方则使用 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() ,路由也是不例外的,只是使用模版代码来解决这些固定的操作而已。

  1. 首先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;
             }
          }
       }
    }
    
  1. 之后 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) {
                }
            }
            // 省略代码...
        }
    }
    
  1. 最终我们看看整个注入流程,首先是调用 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?
                }
            }
        } 
     }
  }

跨模块方法调用实现

模块化开发并不是仅仅只需要解决页面的跳转问题,也会存在模块之间业务相关联,需要使用到其他模块现有的能力,这时候跨模块方法调用就非常必要了。

假设我们的项目依赖是下图这样的结构:

因为 module1module2 没有依赖关系,这样 module1module2 则无法相互使用对方提供的能力,解决这样的问题方法很多,可以改变依赖关系,可以把需要被使用的能力都下沉到 base 模块里。但是这样显然不是我们想要的。还有一种解决方案,就是在 base 模块里定义接口,在 module 里实现接口,这样每个 module 因为都依赖了 base 模块,从而可以直接调用相应的接口,这样就变成面向接口了。那么我们只要解决一个问题,就是找到接口的具体实现类。有兴趣的可以了解下 SPI 全称是 Service Provider Interface ,是一种将服务接口与服务实现分离以达到解耦、可以提升程序可扩展性的机制。

下面看下TheRouter是如何解决这个问题的?

TheRouter 跨模块方法调用使用是在 base 模块中定义接口,在具体需要提供能力的 module 中实现并以 @ServiceProvider 注解标记,步骤如下:

  1. base 模块中定义接口。
public interface IUserService {
    String getUserInfo();
}
  1. 在提供能力的模块中实现接口并 @ServiceProvider 注解标记。
/**
 * 方法名不限定,任意名字都行,方法可以写在任何地方
 * 返回值必须是服务接口名,如果是实现了服务的子类,需要加上returnType限定
 * 方法必须加上 public static 修饰,否则编译期就会报错
 */
@ServiceProvider
public static IUserService test() {
    return new IUserService() {
        @Override
        public String getUserInfo() {
            return "这是用户信息";
        }
    };
}
  1. 使用方通过 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插桩