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

Github: github.com/HuolalaTech…

官网: therouter.cn/

为什么需求运用路由结构

路由是现如今 Android 开发中必不可少的功能,尤其是企业级APP,能够用于将 Intent 页面跳转的强依靠联系解耦,同时减少跨团队开发的彼此依靠问题。如今的路由结构现已不只是是为了满足页面之间的跳转问题。

模块化开发的痛点

  1. 不合理的模块依靠联系,导致模块之间耦合度高,难以保护且编译时间长。
  1. 模块之间的跳转、彼此拜访艰难问题 。

为什么要运用TheRouter

  1. 路由思想和路由库现已不是一个新鲜的常识点了,早在有模块化开发架构就现已出现了处理痛点的法子了。现在市面是有许多路由库都能处理模块化问题,但各有优缺陷。咱们为什么选TheRouter,由于他的确有许多优点现现已过检测的。能够下面参阅官网这个表格:

    抽丝剥茧带你探索 TheRouter

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

  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?
                }
            }
        } 
     }
  }

跨模块办法调用完成

模块化开发并不是只是只需求处理页面的跳转问题,也会存在模块之间业务相关联,需求运用到其他模块现有的才能,这时分跨模块办法调用就十分必要了。

假设咱们的项目依靠是下图这样的结构:

抽丝剥茧带你探索 TheRouter

由于 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插桩