KRouter(https://github.com/richardwrq/KRouter)路由框架借助gradle插件、kapt实现了依赖注入、为Android平台页面启动提供路由功能。 ####从startActivity开始说起 一直在想应该怎么样为这篇博客起个头,后面在洗澡的时候突然灵光一闪,干脆就从最开始编写这个框架的起因开始写起吧。 在组件化开发的实践过程中,当我完成一个模块的开发后(比如说这个模块中有一个Activity或者Service供调用者调用),其他模块的开发者要启动我这个模块中的Activity的代码我们再熟悉不过了:

val intent = Intent(this, MainActivity::class.java)
intent.putExtra("param1", "1")
intent.putExtra("param2", "2")
startActivity(intent)
复制代码

当然,其他模块的开发人员需要知道我们这个Activity的类名以及传入的参数对应的key值(上面的param1和param2),这时候我就想,在每一个需要启动这个页面的地方都存在着类似的样板代码,而且被启动的Activity在取出参数对字段进行赋值时的代码也比较繁琐,于是在网上查找相关资料了解到目前主流的路由框架(ARouter、Router等)都支持这些功能,秉着尽量不重复造轮子的观念我fork了ARouter项目,但是阅读源码后发现其暂时不支持Service的启动,而我负责的项目里面全是运行在后台的Service。。。 紧接着也大概了解了一下其他一些框架,都存在一些不太满意的地方,考虑再三,干脆自己撸一个轮子出来好了。

首先来看一段最简单的发起路由请求的代码(Java调用):

KRouter.INSTANCE.create("krouter/main/activity?test=32")
                .withFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
                .withString("test2", "this is test2")
                .request();
复制代码

其中krouter/main/activity?test=32为对应的路由路径,可以使用类似http请求的格式,在问号后紧接着的是请求参数,这些参数最终会自动包装在intent的extras中,也可以通过调用with开头的函数来配置请求参数。 上面的代码执行后最终会启动一个Activity,准确来说是一个带有@Route注解的Activity,它长这样:

@Route(path = "krouter/main/activity")
public class MainActivity extends Activity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getIntent().getIntExtra("test", -1);//这里可以获取到请求参数test
    }
    ...
}
复制代码

这是一个最基本的功能,怎么样,看起来还不错吧?跟大部分路由框架的调用方式差不多。现在主流的路由框架是怎么做到的呢?下面就看我一一道来。


在使用KRouter的API前首先需要为一些类添加注解:

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/2
 * Time: 上午10:53
 * Version: v1.0
 * Description:用于标记可路由的组件
 */
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Route(
        /**
         * Path of route
         */
        val path: String,
        /**
         * PathPrefix of route
         */
        val pathPrefix: String = "",
        /**
         * PathPattern of route
         */
        val pathPattern: String = "",
        /**
         * Name of route
         */
        val name: String = "undefined",
        /**
         * Priority of route
         */
        val priority: Int = -1)

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/2
 * Time: 上午10:53
 * Version: v1.0
 * Description:用于拦截路由的拦截器
 */
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Interceptor(
        /**
         * Priority of interceptor
         */
        val priority: Int = -1,
        /**
         * Name of interceptor
         */
        val name: String = "DefaultInterceptor")

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/2
 * Time: 上午10:53
 * Version: v1.0
 * Description:属性注入
 */
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class Inject(
        /**
         * Name of property
         */
        val name: String = "",
        /**
         * If true, app will be throws NPE when value is null
         */
        val isRequired: Boolean = false,
        /**
         * Description of the field
         */
        val desc: String = "No desc.")

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/2
 * Time: 上午10:53
 * Version: v1.0
 * Description:Provider
 */
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Provider(/**
                           * Path of Provider
                           */
                          val value: String)
复制代码

被注解的元素的信息最终被保存在对应的数据类中:

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/4
 * Time: 上午10:46
 * Version: v1.0
 * Description:Route元数据,用于存储被[com.github.richardwrq.krouter.annotation.Route]注解的类的信息
 */
data class RouteMetadata(
        /**
         * Type of Route
         */
        val routeType: RouteType = RouteType.UNKNOWN,
        /**
         * Priority of route
         */
        val priority: Int = -1,
        /**
         * Name of route
         */
        val name: String = "undefine",
        /**
         * Path of route
         */
        val path: String = "",
        /**
         * PathPrefix of route
         */
        val pathPrefix: String = "",
        /**
         * PathPattern of route
         */
        val pathPattern: String = "",
        /**
         * Class of route
         */
        val clazz: Class<*> = Any::class.java)
/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/1/8
 * Time: 下午10:46
 * Version: v1.0
 * Description:Interceptor元数据,用于存储被[com.github.richardwrq.krouter.annotation.Interceptor]注解的类的信息
 */
data class InterceptorMetaData(
        /**
         * Priority of Interceptor
         */
        val priority: Int = -1,
        /**
         * Name of Interceptor
         */
        val name: String = "undefine",
        /**
         * Class desc of Interceptor
         */
        val clazz: Class<*> = Any::class.java)

/**
 * User: WuRuiqiang(263454190@qq.com)
 * Date: 18/3/14
 * Time: 上午1:28
 * Version: v1.0
 * Description:Injector元数据,用于存储被[com.github.richardwrq.krouter.annotation.Inject]注解的类的信息
 */
data class InjectorMetaData(
        /**
         * if true, throw NPE when the filed is null
         */
        val isRequired: Boolean = false,
        /**
         * key
         */
        val key: String = "",
        /**
         * field name
         */
        val fieldName: String = "")
复制代码

其中被**@Route注解的类是Android中的四大组件和Fragment或者它们的子类(目前尚不支持Broadcast以及ContentProvider),被@Route**注解的对象目前有3种处理方式:

  1. 若被注解的类是Activity的子类,那么最终的处理方式是startActivity;
  2. 若被注解的类是Service的子类,最终的处理方式有两种,也就 是Android中启动Service的两种方式,使用哪种启动方式取决于是否调用了withServiceConn函数添加了ServiceConnection;
  3. 若被注解的类是Fragment的子类,最终的处理方式是调用无参构造函数构造出这个类的实例,并调用setArguments(Bundle args)将请求参数传入Fragment的bundle中,最后返回该实例

被**@Interceptor注解的类需实现IRouteInterceptor接口,这些类主要处理是否拦截路由的逻辑,比如某些需要登录才能启动的组件,就可以用到拦截器 @Inject用于标记需要被注入的字段 被@Provider注解的类最终可以调用KRouter.getProvider(path: String)方法获取该类的对象,如果该类实现了IProvider**接口,那么init(context: Context)方法将被调用 这些注解最终都不会被编译进class文件中,在编译时期这些注解会被收集起来最终交由不同的Annotation Processor去处理。

KRouter路由框架分为3个模块:

  • KRouter-api模块,作为SDK提供API供应用调用,调用KRouter-compiler模块生成的类中的方法加载路由表,处理路由请求
  • KRouter-compiler模块,各种注解对应的Processor的集合,编译期运行,负责收集路由组件,并生成kotlin代码
  • KRouter-gradle-plugin模块,自定义gradle插件,在项目构建时期添加相关依赖以及相关参数的配置

#####KRouter-compiler 在介绍该模块之前如果有同学不知道Annotation Processor的话建议先阅读 Annotation Processing-Tool详解一小时搞明白注解处理器(Annotation Processor Tool)这两篇文章,简单来说,APT就是javac提供的一个插件,它会搜集被指定注解所注解的元素(类、方法或者字段),最终将搜集到的这些交给注解处理器Annotation Processor进行处理,注解处理器通常会生成一些新的代码(推荐大名鼎鼎的square团队造的轮子**javapoet,这个开源库提供了非常友好的API让我们去生成Java代码),这些新生成的代码会与源码一起在同一个编译时期进行编译。 但是Annotation Processorjavac提供的一个插件,也就是说它只认识Java代码,它压根不知道kotlin是什么,所以如果是用kotlin编写的代码文件最终将会被javac给忽略,所幸的是JetBrains在2015年就推出了kapt来解决这一问题。而且既然有javapoet,那square那么牛逼的团队肯定也会造一个生成kotlin代码的轮子吧,果不其然,在github一搜kotlinpoet,还真有,所以最终决定KRouter-compiler模块使用kapt**+**kotlinpoet**来自动生成代码(kotlinpoet文档过于简单了,建议使用该库的同学通过它的测试用例或者参照Javapoet文档了解API的调用)。

开头的例子中我们可以看到使用KRouter启动一个Activity只需要知道该Activity的路径即可,并不需要像Android原生的启动方式一样传入Class<*>或者Class Name,那么KRouter是怎么做到的呢? 原理很简单,KRouter-compiler模块生了初始化路由表的代码,这些路由表内部其实就是一个个map,这些map以路径path作为key,数据类作为value(比如RouteMetadata),SDK内部会通过path获取到数据类,像开头启动Activity的例子中,SDK就通过path获取到一个RouteMetadata对象,在这个对象中取出被注解的类的Class<*>,有了这个Class<*>就可以完成启动Activity的操作。 接下来说说路由表初始化代码生成之后是怎么被执行的,首先我定义了这样一些接口:

/**
 * 加载路由
 *
 * @author: Wuruiqiang <a href="mailto:263454190@qq.com">Contact me.</a>
 * @version: v1.0
 * @since: 18/1/4 下午6:38
 */
interface IRouteLoader {
    fun loadInto(map: MutableMap<String, RouteMetadata>)
}

/**
 * 加载拦截器
 *
 * @author: Wuruiqiang <a href="mailto:263454190@qq.com">Contact me.</a>
 * @version: v1.0
 * @since: 18/1/5 上午9:12
 */
interface IInterceptorLoader {
    fun loadInto(map: TreeMap<Int, InterceptorMetaData>)
}

/**
 * 加载Provider
 *
 * @author: Wuruiqiang <a href="mailto:263454190@qq.com">Contact me.</a>
 * @version: v1.0
 * @since: 18/1/5 上午9:12
 */
interface IProviderLoader {
    fun loadInto(map: MutableMap<String, Class<*>>)
}
复制代码

以**@Route注解为例,在KRouter-compiler中定义了一个继承自AbstractProcessor的类RouteProcessor**,在编译期间编译器会收集**@Route注解的元素的信息然后交由RouteProcessor处理,RouteProcessor会生成一个实现了IRouteLoader接口的类,在loadInto方法中把注解中的元数据与被注解的元素的部分信息存到RouteMetadata**对象,然后将注解的路径path作为key,RouteMetadata对象作为value保存在一个map当中。代码如下:

/**
 *    ***************************************************
 *    * THIS CODE IS GENERATED BY KRouter, DO NOT EDIT. *
 *    ***************************************************
 */
class KRouter_RouteLoader_app : IRouteLoader {
    override fun loadInto(map: MutableMap<String, RouteMetadata>) {
        map["krouter/sample/MainActivity"] = RouteMetadata(RouteType.ACTIVITY, -1, "undefined", "krouter/sample/MainActivity", "", "", MainActivity::class.java)
        map["myfragment"] = RouteMetadata(RouteType.FRAGMENT_V4, -1, "undefined", "myfragment", "", "", MainActivity.MyFragment::class.java)
        map["krouter/sample/fragment1"] = RouteMetadata(RouteType.FRAGMENT, -1, "undefined", "krouter/sample/fragment1", "", "", Fragment1::class.java)
        map["krouter/sample/fragment2"] = RouteMetadata(RouteType.FRAGMENT, -1, "undefined", "krouter/sample/fragment2", "", "", Fragment2::class.java)
        map["krouter/sample/Main2Activity"] = RouteMetadata(RouteType.ACTIVITY, -1, "undefined", "krouter/sample/Main2Activity", "", "", Main2Activity::class.java)
        map["krouter/sample/Main3Activity"] = RouteMetadata(RouteType.ACTIVITY, -1, "undefined", "krouter/sample/Main3Activity", "", "", Main3Activity::class.java)
    }
}
复制代码

代码生成之后,我们需要执行loadInto方法才算是把数据存入到map中去,我们可以通过Class.forName(ClassName).newInstance()获取该类实例,然后将其强制转换为IRouteLoader类型,接着调用loadInto方法传入map即可,现在问题来了,加载一个类我们需要知道这个类的路径和名称:com.x.y.ClassA,但是SDK并不知道KRouter-compiler会生成哪些类。 为此我准备了两种解决方案:

  1. 类似ARouter的做法,扫描所有dex文件,找出实现了ARouter接口的类,然后将这些类的ClassName缓存至本地,下次应用启动时如果存在缓存且没有新增文件则读取缓存内容即可;
  2. 第二种是生成的类及其路径遵循一定的规则,比如由RouteProcessor生成的类路径规定为com.github.richardwrq.krouter,类名规定以“KRouter_RouteLoader_”作为开头然后拼接上Module名称(以Module名称作为后缀是避免在不同的Module下生成类名一样的类,导致编译时出现类重复定义异常),所以RouteProcessor名称为app的Module下生成的类就是com.github.richardwrq.krouter.KRouter_RouteLoader_app,在程序运行的时候,我们的SDK只需要获取项目中所有Module的名称,然后依次加载它们并执行loadInto方法即可。

基于性能考虑我采取了第二种方案,这就需要解决一个问题,因为RouteProcessor是无法知道当前是处于哪个Module的,所以我们需要在Module的build.gradle做如下配置:

kapt {
    arguments {
        arg("moduleName", project.getName())
    }
}
复制代码

这样我们就配置了一个名为“moduleName”的参数,它的值就是当前Module的名称。这个参数可以在ProcessingEnvironmentgetOptions()方法获取的map中取出, RouteInterceptorProvider三者的处理流程大致相同,就不一一赘述了。 #####KRouter-api 该模块其实就是提供API给用户调用的SDK 上面提到SDK需要执行KRouter-compiler模块类的代码才能真正完成路由表初始化的工作,由于最终编译器会将所有Module打包成一个apk,所以在APP运行时是不存在Module的概念的,但是按照解决方案2各Module生成的类会以Module名称作为后缀,因此必须想办法让SDK获取到项目中所有Module的名称,考虑再三,我采取的解决方案是从assets目录入手,在项目构建时期创建一个task,这个task会在Module的src/main/assets目录下生成一个“KRouter_ModuleName”的文件,在SDK初始化的时候只需要列出assets目录下所有"KRouter_"开头的文件并截取下划线“_”后的内容,即可得到一个包含所有Module名称的列表。 下面给出SDK的类图,同学们可以对照源码参考

#####KRouter-gradle-plugin 完成上述两个模块后其实 KRouter框架已经可以正常使用了,引用方式如下: 在各Module的 build.gradle加入下面的代码

kapt {
    arguments {
        arg("moduleName", project.getName())
    }
}
dependencies {
    implementation 'com.github.richardwrq:krouter-api:x.y.z’
    kapt 'com.github.richardwrq:krouter-compiler:x.y.z'
}
afterEvaluate {
    //在assets目录创建文件的task
    ...
}
复制代码

当项目中Module较多时,手动在每一个Module加入这些配置未免有些蠢。。所以我写了一个gradle插件用来自动完成这些配置工作,具体实现参考源码,逻辑非常简单,最后使用引用方式变成下面这样: 在项目根目录build.gradle文件加入如下配置

buildscript {

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:x.y.z"
        classpath "com.github.richardwrq:krouter-gradle-plugin:x.y.z"
    }
}
复制代码

然后在各Module的build.gradle文件加入如下配置

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: "com.github.richardwrq.krouter"
复制代码

到这里KRouter路由框架就粗略的介绍了一遍,由于kapt仍在不断完善,所以使用过程中难免碰到一些坑或者本身API功能不够完善,下面就列举一些遇到的问题以及解决方法:

#####ToDoList

  • 通过gradle插件修改AndroidManifest.xml文件,自动注册路由组件(Activity、Service)
  • 目前尚不支持动态加载的插件的路由注册,但有解决方案,hook classloader装载方法,在加载dex文件时扫描KRouter的路由组件
  • 支持多应用多进程环境下的页面路由

转载于:https://juejin.im/post/5aabb207518825558001ffe0

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐