ARouter 兄弟版(LRouter)

1,374 阅读6分钟

前言

前不久 Android Studio Giraffe 正式版本发布了,支持新UI,试用了两天感觉不太适应又换回去了,不知道公司灯光的原因还是屏幕原因,看着太暗,代码看的不是很清楚了都。最近两个正式版本都是支持 AGP 8 的。AGP 8 没发正式版本之前就听到好几个人说这次是断代式更新。因为 AGP 到了 8 以上 Transform 废弃了,对于新版本升级,激进派的哥们,要难受了。好多基于 Transform 写的三方库跟着不能用。阿里的 ARouter 正是其中之一。在 issues 中也有很多人 fork 了代码,分享出来了支持 AGP 8的、支持KSP 的版本。我也是最开始只写了支持 AGP 8 的插件,后来又想支持 KSP,最后又想着既然都用 ASM 了,拦截器什么的都可以 ASM 直接插入,模块初始化也可以单独分出来编译期去插入,就这样一步一步的改到这份上了。突然又想着这样改还没有直接重写来的快.....................................

最终版本:LRouter

LRouter 不一定适合大多数人。因为只支持 AGP 7.4 及以上版本,JDK 至少 11。只支持 KSP 版本,不支持KAPT。本身写LRouter 也是因为阿里的 ARouter 官方没有更新的势头。为了给 AGP 高版本来用的。所以就直接放弃了 Transform 的适配。现在Google 的 KSP 也很稳定了,也直接放弃了 KAPT。

下边介绍下 LRouter的实现思路以及与 ARouter 的差别。

注解处理器

ARouter 是 Java 写的,在Kotlin 项目中由于 APT 无法识别 Kotlin 语法,要用 KAPT 插件来处理注解。 KAPT 插件有一个Task 任务叫: KaptGenerateStubsTask。 会把 Kotlin 文件转换成 Stubs 让 APT 可以识别出来。这也是 KAPT 慢的原因之一。

KSP Google 出的轻量级编译器插件,引用官网介绍:

Kotlin 符号处理 ( KSP ) 是一个 API,可用于开发轻量级编译器插件。KSP 提供了一个简化的编译器插件 API,它利用 Kotlin 的强大功能,同时将学习曲线保持在最低限度。与kapt相比,使用 KSP 的注释处理器的运行速度最高可达 2 倍。

KSP 提供的 API 跟反射很像,写起来上手很快,也不依赖 JVM。

LRouter 使用 KSP 后编译速度能感觉到明显的提升。

AGP8

AGP 7 新的 API 已经有了但还保留着Transform 只是不推荐,而AGP 8 则移除了 Transform。现在有很多文章都是介绍 Transform 的替代品是 AsmClassVisitorFactory。其实并不是如此。跟 Transform 比起来,AsmClassVisitorFactory 简化了好多,也不用写增量逻辑,速度上也有提升。因为无论注册多少个 AsmClassVisitorFactory 只执行了一次IO 操作。AsmClassVisitorFactory 都是插在 Read 和 Write 之间的。跟字节开源的 byteX 同一个思路。

但是 LRouter 并没有使用 AsmClassVisitorFactory,原因是 AsmClassVisitorFactory 没有办法根据整个项目来做插入,只适合对已知类进行修改或者插入。我们现在是需要把整个工程中要处理的类先找出来,再统一往待插桩类里插入。比如 AsmClassVisitorFactory 已经处理到待插桩类了,这个时候要插入类信息可能只收集到了一半,还没收集完整。对于一些复杂的功能 AsmClassVisitorFactory 就没办法做到了。就需要自定义 TASK 来处理。


val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)

androidComponents.onVariants { variant ->
    val taskProvider = project.tasks.register(
        "${variant.name}LRouterHandleClasses",
        LRouterClassTask::class.java
    )

    variant.artifacts
        .forScope(ScopedArtifacts.Scope.ALL)
        .use(taskProvider)
        .toTransform(
            ScopedArtifact.CLASSES,
            LRouterClassTask::allJars,
            LRouterClassTask::allDirectories,
            LRouterClassTask::output
        )
}

这是官方的例子:modifyProjectClasses

不同的是 官方用的是 javassist来修改字节码。相对 ASM 来说 javassist API 简单,但是速度慢些。ASM 小而快,API相对复杂。LRouter 还是选用 ASM。

参数注入

ARouter 的参数注入,是在运行时,通过当前类的类名称拼上固定的类后缀名,用反射去创建对应的类,反射调用注入方法。当前类处理完再去对父类执行以上操作。核心代码如下:

private void doInject(Object instance, Class<?> parent) {
    Class<?> clazz = null == parent ? instance.getClass() : parent;

    ISyringe syringe = getSyringe(clazz);
    if (null != syringe) {
        syringe.inject(instance);
    }

    Class<?> superClazz = clazz.getSuperclass();
    // has parent and its not the class of framework.
    if (null != superClazz && !superClazz.getName().startsWith("android")) {
        doInject(instance, superClazz);
    }
}

private ISyringe getSyringe(Class<?> clazz) {
    String className = clazz.getName();

    try {
        if (!blackList.contains(className)) {
            ISyringe syringeHelper = classCache.get(className);
            if (null == syringeHelper) {  // No cache.
                syringeHelper = (ISyringe) Class.forName(clazz.getName() + SUFFIX_AUTOWIRED).getConstructor().newInstance();
            }
            classCache.put(className, syringeHelper);
            return syringeHelper;
        }
    } catch (Exception e) {
        blackList.add(className);    // This instance need not autowired.
    }

    return null;
}

可以看到参数注入完全用的反射。

为了避免使用反射,最开始的方案本来是直接在 Activity 或者Fragment 的 onCreate 方法中插入生成的模板类方法。

override fun onCreate(savedInstanceState: Bundle?) {
    `ParamActivity__LRouter$$Autowired`.autowiredInject(this)
     //...
}

这样做每个要有参数注入的页面,都要经过 ASM 去处理,另外onCreate() 方法,一般都是放在基类里的,如果没有onCreate(),还要用 ASM 生成 onCreate 出来,再把字节码加进去。这样做 ASM 要处理的太多了,又影响了编译速度,然后放弃了。

最后方案还是在一个注入口统一处理,把所有生成的类的方法调用,都通过ASM 添加到一个预留方法里。生成的模板类里边去做条件判断是否是当前页面要注入的参数。

这样搞也有不好的地方就是每一次路由,会把所有生成的参数相关静态类方法执行一遍。

模块初始化和拦截器

LRouter 提供每个Module 单独初始化的功能,类似Google 的 androidx.startup.Initializer 。使用androidx.startup.Initializer时,每个模块的Initializer都需要统一在壳子工程添加。如果模块单独运行的时候,又要单独把运行的Module 的 Initializer 注册在清单文件。另一种方式是在清单文件指定 lib 模块的 Initializer然后用反射添加其他模块。 看下两种方式代码的不同。

  • 在壳子工程添加子Module 方式

class AppInitializer : Initializer<Unit> {

    override fun create(context: Context) {
        // 初始化逻辑...
    }

    override fun dependencies(): List<Class<out Initializer<*>>> {
        return listOf(
            MainInitializer::class.java,
            RoomInitializer::class.java,
            //....
        )
    }
}

在 lib 模块添加 方式:

// 
class BaseInitializer : Initializer<Unit> {

    private val depend = listOf(
        "com.xxx.xxx.base.MainInitializer",
        "com.xxx.xxx.base.RoomInitializer",
        //...
    )

    override fun create(context: Context) {
     // 初始化逻辑...
    }

    override fun dependencies(): List<Class<out Initializer<*>>> {
        val dependencies = ArrayList<Class<out Initializer<*>?>>()
        depend.forEach {
            try {
                val initClass = Class.forName(it) as Class<out Initializer<*>>
                dependencies.add(initClass)
            } catch (e: Exception) {
                Log.d("BaseInitializer", "not found $it")
            }
        }
        return dependencies
    }

第一种没有使用到反射,但是模块单独运行的时候需要手动修改。第二种模块单独运行时不用手动修改,但是使用到了反射。

LRouter 既然用到 ASM 了,那这些问题就简单多了。LRouter 的初始化是注册了一个 ContentProvider 。那么把每个模块要初始化的代码用 ASM 插入到 ContentProvider 的 onCreate 方法中。不就两全其美了吗。

最终实现方式:

实现 LRouterInitializer 接口并添加 @Initializer 注解

@Initializer(priority = 1, async = false)
class AppModelInit : LRouterInitializer {
    override fun create(context: Context) {
        Log.d("AppModelInit", "create: ${context is Application}")
    }
}

可在注解里指定优先级和执行线程,再也不用在 lib 或者 壳子工程里去关联初始化逻辑了。

路由拦截器的添加方式跟初始化器是一样的。最开始是用 KSP 生成了添加的模板代码,找到生成类再执行生成类方法,后来发现有点多此一举了,然后直接通过 ASM 插入。不再用 KSP 去生成模板代码了。

拦截器也是可指定优先级的,可添加多个,当一个拦截器中断路由时,后边的拦截器将不会执行。

路由表生成

路由表的生成跟 ARouter 也是有区别的,ARouter 的路由表是通过跟每个模块生成的模板代码关联起来的,在编译期生成在每个 Module 的 build 目录中。ARouter开启路由表生成后,会禁用增量编译。因为开启增量,生成的文档是不完全的。

LRouter 路由表生成换了另一种思路,把路由表生成用一个单独的 Task 任务来做。用Task 去扫描每个 Module 下的 build 目录中 KSP 生成的代码。因为所有页面都会由 KSP 插件生成注册代码在 build 目录,只要去读取并解析这些生成文件就可以了。这样就可以统一输出整个工程的路由表到一个文件。什么时候需要,什么时候手动执行 Task 任务。

router_task.png

执行前要保证至少进行过一次 Build。如果想集成到打包流程中去,指定 Task 任务依赖关系就可以。

生成路由表Task代码:GenLRouterDocTask

依赖注入

说起依赖注入肯定会想起 Dagger2、Hilt、Koin等。这些都是相当知名的注入库。LRouter 也提供了简单的注入功能。如果项目中使用到了像 Hilt 等注入库,建议不要使用 路由框架的注入,统一使用一个。路由框架提供的只是为了模块间的通信和解耦。

在这里先介绍下KoinKoin 是一个轻量级注入框架。更适合Kotlin 使用。还记得当年刚开始写 Kotlin 的时候,基本上都是用 Java 思想写 KT 代码。后边看了 Koin 的源码后,学了到了很多 KT 的写法,DSL 等。在一句句 “卧槽还能这样写” 中。写 KT 代码风格大变。现在还记忆犹新。

LRouter 的注入功能是参考了 Koin 的注入方式。用 Kion 的几个核心类,二次修改。修改完后 LRouter 注入相关的代码大概只有 300 行左右,然后配合着 KSP 来生成模板代码。还是那句话,只提供简单的注入功能,为了模块间的解耦和通信。如果对注入有更多需求的话就直接使用 专业的注入库 Dagger2、Hilt、Koin等。这些都有很完善的注入功能,作用域管理等。

最后回顾

从最开始写支持 8.0插件,后边又要支持KSP,然后在一直的不满足中,跌跌撞撞的整个重写了。人嘛总是天生的贪。整个写完之后,还是发出了一声感慨。一件事情刚开始的时候很有兴致,等你搞完了之后你会发现也就那么回事(别瞎想我是说代码这事¬_¬)。

最近刚整理出第一版,毕竟是整个重写的,肯定有很多功能要完善的。还望各位感兴趣的大佬多提提意见。

地址:LRouter