前语

前不久 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 使命。

ARouter 兄弟版(LRouter)

履行前要保证至少进行过一次 Build。假如想集成到打包流程中去,指定 Task 使命依靠关系就能够。

生成路由表Task代码:GenLRouterDocTask

依靠注入

说起依靠注入肯定会想起 Dagger2、Hilt、Koin等。这些都是适当知名的注入库。LRouter 也提供了简略的注入功用。假如项目中运用到了像 Hilt 等注入库,建议不要运用 路由结构的注入,一致运用一个。路由结构提供的仅仅为了模块间的通讯和解耦。

在这里先介绍下Koin。Koin 是一个轻量级注入结构。更适合Kotlin 运用。还记得当年刚开端写 Kotlin 的时分,基本上都是用 Java 思维写 KT 代码。后边看了 Koin 的源码后,学了到了许多 KT 的写法,DSL 等。在一句句 “卧槽还能这样写” 中。写 KT 代码风格大变。现在还浮光掠影。

LRouter 的注入功用是参考了 Koin 的注入办法。用 Kion 的几个中心类,二次修正。修正完后 LRouter 注入相关的代码大约只要 300 行左右,然后配合着 KSP 来生成模板代码。仍是那句话,只提供简略的注入功用,为了模块间的解耦和通讯。假如对注入有更多需求的话就直接运用 专业的注入库 Dagger2、Hilt、Koin等。这些都有很完善的注入功用,效果域办理等。

最终回顾

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

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

ARouter 兄弟版(LRouter)

地址:LRouter