ARouter 兄弟版(LRouter)

前言

前不久 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

全部评论

相关推荐

小厂面经,也是我的处女面(30min)1.自我介绍2.spring&nbsp;boot的自动装配原理(好多类和接口的单词都忘了全称是啥了,就说了记得的单词,流程应该说对了吧)3.有用过redis吗?主要是用在实现什么功能(说了技术派用redis的zset来实现排行榜)5.有了解过Redisson吗?讲一下对于分布式锁的了解以及在什么场景下应用(说了秒杀场景)6.对mysql有了解吗?包括它的索引优化和创建(把想起来的全说了)7.了解设计模式吗?比如单例模式,为什么要使用单例模式,它的优点是什么(昨天刚看的设计模式)8.工厂模式有了解吗?主要的使用场景是?(也是昨天刚看的)9.场景题:有7个服务器,需要在早上十点定时的向数据库中的用户表中的用户发短信,如果做到发送的消息不重复,且如果发送失败了需要知道是到哪个用户失败了,这样下次就直接从这个用户开始(我答了用spring&nbsp;task来实现定时,用分布式锁来保证只有一份服务器可以发送消息,用消息队列来存储消息,然后用消息确认机制来保证错误信息的记录,以及在数据库或者业务层面完成消息消费的幂等性)10.场景题:如果在系统启动的时间就将数据库的所有用户相关的信息都读到一个hashmap中(这个没啥思路,没答好)27届的投了一个星期终于有一个面试了,大部分公司都只招26的
inari233:已oc,拒了
查看9道真题和解析
点赞 评论 收藏
分享
Eeeeevans:都是校友,还是同届,我就说直白点,不委婉了,我相信你应该也不是个玻璃心,首先你觉得一个双非的绩点写简历上有用吗?班长职务有用吗?ccf有用吗?企业会关心你高数满分与否吗?第二,第一个项目实在太烂,一眼就能看出是外卖,还是毫无包装的外卖,使用JWT来鉴权,把热点数据放进Redis这两个点居然还能写进简历里,说难听点这两个东西都是学个几十分钟,调用个API就能完成的事情,在双非一本的条件下,这种项目你觉得能拿出手吗,第二个项目你写的东西和你的求职方向有任何的匹配吗?第三,计设那一块毫无价值,如果想突出自己会前端,直接写入专业技能不行吗,最后,专业技能里像深入理解JVM底层原理这种你觉得这句话你自己真的能匹配吗?都是校友加上同届,我措辞直接,但希望能点出你的问题,想进大厂还得继续沉淀项目和学习
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务