AGP 8.0 路由框架新思路

212 篇文章 8 订阅
21 篇文章 0 订阅

前言

说到路由又是老生长谈了,阿里的 ARouter、美团的WMRouter 这些老牌知名度很高的路由框架。由于 AGP 8.0 以后不能支持,Github 上也有很多人提了PR,Issues 里边也有很多个开发了支持8.0 的插件。

去年我为了支持 AGP 8.0 以及 KSP, 也写了一个路由框架 LRouter。新框架难免会有很多问题,刚好公司有一个新项目要做,我自己在公司的项目中第一个接入的,踩了半年的坑,也基本上稳定了下来。在这里把遇到的一个很致命的编译问题分享一下解决的思路。

问题

AGP 8.0以后移除了 Transform API。官方文档也给出了替代的方式 AsmClassVisitorFactory,但是这种方式我以前的文章也有说过,只适合对已知的类做插脏或者转换。像路由框架是需要对整个项目的类进行扫描的,遍历完成之后拿到了类信息,再进行插桩。所以只能自定义 Task 来实现。 这里是自定义Tssk官方例子

LRouter 第一个版本就是用自定义Task来实现。自定义Task 会对整个项目的类进行处理包括第三方库最终生成一个classes.jar。这样就对编译速度影响很大。大到什么程度呢,拿我接入到公司的项目来举例,看下图:

image.png

这是我只改一行代码的情况下,直接把项目运行起来,要等很久 1 分钟多,项目越大这个时间会越久。

过完年刚来公司还是有些小忙的,没太多时间处理这个问题,我就这样活活的被自己折磨了几个月。

其中有一天,就因为频繁运行项目测试,这个编译慢的问题拖到我晚上10 点才搞完需求。然后10 点下班去骑我的风驰电掣的小电摩,结果电池还被人给偷了 ............................................................。

原因

由于极大的拖慢了编译速度,我开始找原因,发现扫描和插桩的过程其实并不慢,只用了一两秒的时间,真正拖慢编译速度的是 dexBuilderDebug 这个任务,因为插件把所有类包括第三方Lib 全都Copy 到一个 classes.jar 中了,哪怕你只改一行代码最终的这个 classes.jar 都会变,所以每次运行都会全量执行dex。

有时会抛出如下大量警告:

AGPBI: {"kind":"warning","text":"Expected stack map table for method with non-linear control flow.","sources":[{"file":"D:\\Android\\Project\\PicMe\\app\\build\\intermediates\\classes\\devGoogleDebug\\ALL\\classes.jar"}],"tool":"D8"}

第一次尝试解决

知道了原因我也查了相关的资料,对 dexBuilderDebug 相关的优化的文章很少,几乎没有,只有字节和得物的两篇文章有介绍

  1. 字节dexBuilder优化
  2. 得物优化

这两篇文章有同一个特点,就是只介绍没放出来源码。大概思路就是 Hook AGP 的编译流程,因为我们的Android 项目编译的时候 只有 APP 主模块是以目录形式做输入,各个子模块都是以 Jar 包形式做输入的。如果我们子模块有更改,整个子模块编译时输入的Jar 里的类都要重新 dex

引用字节那篇文章的一句话:

jar 输入相比于 目录输入来说增量编译效果非常差,那么可以想到 hook TransformInvocation 中的 input 方法,动态将 project 的 jar 类型输入(JarInput)映射为一个 目录输入(DirectoryInput),那么子模块修改对应代码时,只重新编译目录中被修改的 class 为 dex(而不是原来的整个 jar 内所有 class 重新执行 dex 编译),整体 dex 重新编译的数量将大幅度减少。

这种方案,适合体量非常大的项目,是要入侵到 AGP 的编译流程的。目前只是写一个路由插件,如果要这样搞,成本太高了。只能换其他思路

缩小扫描范围

由于自定义 Task 的时候 forScope() 是声明了ScopedArtifacts.Scope.ALL。

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

这样会对项目所有的依赖都进行处理包括第三方Lib。使用这个的原因是为了尽可能的减少反射,把待插桩的类放在了 Router 模块下,Router 模块是以第三方Lib 依赖到项目的,只有ScopedArtifacts.Scope.ALL 的时候才会去扫描第三方Jar 包。另一个原因是这样可以统一处理,扫描加插桩一气合成。

先尝试着把范围缩小 forScope改成 ScopedArtifacts.Scope.PROJECT 这样就不对依赖的项目生效,只对当前Project生效。

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

由于只对当前依赖的Project生效,所以写好的插件就不能只在 APP 模块依赖了,所有用到框架功能的 项目都要添加插件。

这样修改另外引出了另一个问题,就是没法统一处理了,每个Project都注册了这个 Task 任务,扫描出来的类信息都是单独的,这就要每个模块都写一份缓存文件,每个Project 执行完 Task 就把扫描到的类信息写到本地缓存起来。

要进行插桩的类,只能放在APP模块了,因为APP 下边的所有子模块是先执行编译流程的,等所有子模块都执行完了,APP 模块才会执行,刚好这个时候子模块扫描出来的信息也已经写到了本地缓存,可以直接读取进行插桩操作。

这条 Issues 下边xiaoyvyv 提供的修改建议。就是以上的思路。

https://github.com/aleyn97/router/issues/6

改完之后,编译速度提升很多,修改代码只会影响当前Project。

更换 AsmClassVisitorFactory

既然加了缓存文件了,那理论上直接使用 AsmClassVisitorFactory 来处理的也是行的通的。因为transformXXXClassesWithAsm 这个Task ,APP 主模块也是最后执行的:

variant.instrumentation.transformClassesWith(  
LRouterAsmClassVisitor::class.java,  
InstrumentationScope.PROJECT  
) {}

新创建LRouterAsmClassVisitor 类, 对所有Project 都进行注册。然后在 createClassVisitor 方法里通过 ClassVisitor 来把类信息写到缓存文件中去。

巧用KSP

当我打开缓存文件看的时候,缓存文件中的信息只有类名和优先级这些信息,也就是说使用transformClassesWith对子模块下的类进行遍历,只是拿到了这些信息来做了缓存,并没有使用 ASM 来对类进行修改或者替换,那有没有其他方法提前拿到类信息,不通过 transformClassesWith这样遍历写到缓存中去呢,这样就只用关心主模块下要插桩的类怎么处理。

突然灵光一闪,卧槽,KSP不是就嘛,模板类都是由 KSP 根据注解来生成的,我们所有要扫描的类都是KSP 生成出来的模板类,transformXXXClassesWithAsm 这个Task 也肯定是在 KSP 生成模板类之后执行的。直接拿到所有子模块下 KSP 生成目录里边的模板类不就好了吗。

说干就干,由于之前版本拦截器和初始化相关的注解,都是编译时注解,用ASM在处理 Class 时拿到的类名优先级等信息。首先要改造的就是这里,不能通过 ASM 取信息了,把相关注解换成源码注解,全部通过 KSP 去生成。这样就只用判断类名了。

这个时候,就只用在主模块使用 LRouterAsmClassVisitor来处理要插桩的类就好了,然后把所有子Project 的 ksp 生成目录当参数传递过去,在进行插桩的时候遍历所有目录通过类名取出需要的信息。

首先把所有 KSP 生成目录用 list 集合传递给 LRouterAsmClassVisitor:

androidComponents.onVariants { variant ->  
       // ......
    val generatedDir = "generated/ksp/"  // ksp 生成目录
    variant.instrumentation.transformClassesWith(  
        LRouterAsmClassVisitor::class.java,  
        InstrumentationScope.PROJECT  
    ) { param ->  
        param.genDirName.set(generatedDir)  // 目录名称参数
        val list = project.rootProject.subprojects.plus(project)  
        .map { it.layout.buildDirectory.dir(generatedDir).get() }  // 过滤所有 KSP 生成目录
        param.inputFiles.set(list)  // 设置所有子模块和主模块的生成目录
    }  
    variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)  
    variant.instrumentation.excludes.addAll(  
        "androidx/**",  
        "android/**",  
        "com/google/**",  
    )  
}

当 LRouterAsmClassVisitor 处理到"com.router.LRouterGenerateImpl" 时取出所有 KSP生成目录的路径,返回 InsertCodeVisitor 进行插桩。

internal const val GENERATE_INJECT = "com.router.LRouterGenerateImpl" // 待插桩类

abstract class LRouterAsmClassVisitor : AsmClassVisitorFactory<ParametersImpl> {  

    override fun createClassVisitor(  
        classContext: ClassContext,  
        nextClassVisitor: ClassVisitor  
    ): ClassVisitor {  
        if (classContext.currentClassData.className == GENERATE_INJECT) {  
        val inputFiles = parameters.get().inputFiles.get() //取出所有 KSP 生成目录
        val genDirName = parameters.get().genDirName.get() 
        return InsertCodeVisitor(nextClassVisitor, inputFiles, genDirName)// 插桩操作
        }  
        return nextClassVisitor  
}  

override fun isInstrumentable(classData: ClassData): Boolean {  
return classData.className == "com.router.LRouterGenerateImpl"  
}  
}  


interface ParametersImpl : InstrumentationParameters {  

    @get:Internal  
    val genDirName: Property<String>  

    @get:Internal  
    val inputFiles: ListProperty<Directory>  
}

InsertCodeVisitor 类的代码不贴了,有点多,点链接进去看吧。https://github.com/aleyn97/router/blob/main/plugin/src/main/java/com/aleyn/router/plug/visitor/InsertCodeVisitor.kt

优化结果

贴个优化后的图:

image.png

从 1 分钟多减到 8 秒。

最后也推荐下我这个路由 LRouter,基于KSP和AsmClassVisitorFactory 的路由框架。如果你想在Gradle 高版本使用路由可以考虑下 LRouter。

作者:Aleyn
链接:https://juejin.cn/post/7374677514536812571
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值