欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

Kotlin打造Android路由框架,kotlin打造android,WuRichard 的博

来源: 开发者 投稿于  被查看 45466 次 评论:166

Kotlin打造Android路由框架,kotlin打造android,WuRichard 的博


明天就是周六啦,提前祝大家周末愉快!

本篇来自 WuRichard 的投稿,分享了他自己的路由框架,希望对大家有所帮助。

WuRichard 的博客地址:

https://www.jianshu.com/u/fbeab77ee34b


前言


KRouterhttps://github.com/richardwrq/KRouter )路由框架借助gradle插件、kapt( https://blog.jetbrains.com/kotlin/2015/05/kapt-annotation-processing-for-kotlin )实现了依赖注入、为Android平台页面启动提供路由功能。

源码不复杂,在关键地方也有注释说明,建议打算或正在使用kapt+kotlinpoet遇到坑的同学可以fork一下项目,或许能找到你想要的答案,只要将整个流程了解清楚了,相信你自己也能撸一个轮子出来,目前许多开源框架dagger、butter knife、greendao等实现原理都是一致的。


从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详解

https://blog.csdn.net/hj7jay/article/details/52180023

一小时搞明白注解处理器(Annotation Processor Tool)

https://blog.csdn.net/u013045971/article/details/53509237

这两篇文章,简单来说,APT就是javac提供的一个插件,它会搜集被指定注解所注解的元素(类、方法或者属性),最终将搜集到的这些交给注解处理器Annotation Processor进行处理,注解处理器通常会生成一些新的代码(推荐大名鼎鼎的square团队造的轮子javapoet( https://github.com/square/javapoet ),这个开源库提供了非常友好的API让我们去生成Java代码),这些新生成的代码会与源码一起在同一个编译时期进行编译。

但是Annotation Processor是javac提供的一个插件,也就是说它只认识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当中。生成的代码如下(项目build之后可以在(module)/build/generated/source/kaptKotlin/(buildType)目录下找到这些自动生成的类):

/**
 *    ***************************************************
 *    * 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的名称。这个参数可以在ProcessingEnvironment的getOptions()方法获取的map中取出,RouteInterceptorProvider三者的处理流程大致相同,就不一一赘述了。

在这里提一下关于依赖注入Inject的实现,关于如何对属性进行注入我想了两种解决方案:

   1. 第一种就是通过反射,了解反射的同学都知道可以通过反射获取类的运行时注解,并且通过反射API为类的属性进行赋值,但由于时反射,所以性能上有所损耗,但是可以无视属性的访问权限;

   2. 第二种是生成需要被注入的类的扩展方法,在扩展方法里面对接收者的属性进行赋值,性能更好,但是缺点是无法对private以及protected成员进行赋值。

一开始是希望偷懒,就选择了第一种方案,但是问题来了,我知道Java的反射会有一些性能上的问题,但速度还不至于让用户感知明显,但是当我调用kotlin反射相关API时(最主要是获取Properties相关API),发现第一次调用花费的在4~5s 左右,之后调用速度是毫秒级的,我猜测是第一次调用加载了大量数据,然后将这些数据缓存起来了,但这4~5s的调用时间实在是恶心,所以最终还是决定采用方案2,有兴趣的同学可以查看com/github/richardwrq/krouter/compiler/processor/RouteProcessor.kt,生成的代码如下:

class com_github_richardwrq_krouter_activity_Main2Activity_KRouter_Injector : IInjector {
    override fun inject(any: Any, extras: Bundle?) {
        val bundle = getBundle(any, extras)//getBundle为自动生成的顶层方法
        (any as Main2Activity).exInject(bundle)
    }

    private fun Main2Activity.exInject(bundle: Bundle) {
        person = bundle.get("person") as? Person ?: KRouter.getProvider<Person>("person") ?: parseObject(bundle.getString("person"), object : TypeToken<Person>() {}.getType()) ?: throw java.lang.NullPointerException("Field [person] must not be null in [Main2Activity]!")//parseObject为自动生成的顶层方法
        provider = bundle.get("NoImplProvider") as? NoImplProvider ?: KRouter.getProvider<NoImplProvider>("NoImplProvider") ?: parseObject(bundle.getString("NoImplProvider"), object : TypeToken<NoImplProvider>() {}.getType()) ?: throw java.lang.NullPointerException("Field [provider] must not be null in [Main2Activity]!")
        myProvider = (KRouter.getProvider<MyProvider>("provider/myprovider")) ?: throw java.lang.NullPointerException("Field [myProvider] must not be null in [Main2Activity]!")
    }
}

生成的类路径与扩展方法接收者的类路径相同(解决Java包内访问权限问题),类名命名规则为扩展方法接收者类路径的“.“替换为”_“作为前缀,后缀为”_KRouter_Injector“,比如被被注入的类是com.github.richardwrq.krouter.activity.Main2Activity,那么自动生成的类为com.github.richardwrq.krouter.activity.com_github_richardwrq_krouter_activity_Main2Activity_KRouter_Injector

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功能不够完善,下面就列举一些遇到的问题以及解决方法:

注解处理器Annotation Processor中判断Java或kotlin类

https://www.jianshu.com/p/f1ca4314f804

使用kotlinpoet生成kotlin代码数据类型不正确的问题

https://www.jianshu.com/p/f41d21850cb4


ToDoList


  • 通过gradle插件修改AndroidManifest.xml文件,自动注册路由组件(Activity、Service)

  • 目前尚不支持动态加载的插件的路由注册,但有解决方案,hook classloader装载方法,在加载dex文件时扫描KRouter的路由组件

  • 支持多应用多进程环境下的页面路

用户评论