本库是一个安卓平台上的基于 ASM 实现的轻量级高性能字节码全量操作平台。目前已经兼容 AGP 8+。
此外,本库从设计之初就考虑了 Transform 过程对构建时间的影响,因此特别优化了整体构建流程,能更好的利用缓存,增量,过滤机制,从而大幅减少构建所需的时间。
关于构建性能可以直接看文章末尾。
- v2 版已经支持
AGP 7.4 ~ AGP 8+|(v1 老版本是基于AGP4的Transform接口) - 完全基于原生
AGP和Gradle公开接口开发,没有Hook因此不会受到未来AGP接口变更影响。 - 支持根据
variant进行不同的配置和处理。 - 支持
dexBuilder任务的增量编译。 - 支持非全量
Transform,可通过 filter 控制需要进行处理的 class。 - 基于
asm-tree实现的接口更加易于修改字节码。 - 插件平台内部多线程并行 IO 处理,提升编译速度。
- 插件平台支持挂载多个自定义的字节码修改器,使用多个修改器也只会有一次 IO 产生。
- 提供了
plugin-api供开发者实现自定义的字节码修改器,甚至不需要再去写Gradle插件。
-
modifier-aspect :用于实现 AOP 的字节码修改器。
- PermissionMonitor:Android 隐私政策敏感权限监控。
-
modifier-component-di :用于实现组件依赖注入的字节码修改器。
-
StablePlugin:用于实现插件化。
-
BytecodeUtil
app用于演示buildSrc管理 maven 发布和版本控制项目统一配置repos的本地 maven 仓库,便于开发时调试lib_bcu_plugin插件工程编译时修改字节码在这里实现plugin-api插件对外提供的 API,基于此可实现自定义的字节码修改器
-
注意:在构建前先在项目根目录下执行该命令来生成插件后重新 sync 项目
gradlew publishAllPublicationsToLocalRepository
- v2 版本(推荐使用)
- 基于
AGP 8+的variant#artifacts接口开发 min AGP 7.4~max AGP 8+- 当使用
AGP 7.4时最低应使用Gradle 7.5不能使用Gradle 8 - 当使用
AGP 8+时最低应使用Gradle 8
- 基于
- v1 版本(已经不维护)
- 基于
AGP 4的Transform接口开发现在不推荐使用了 min AGP 4.1.3~max AGP 7.3
- 基于
-
项目已经发到
jitpack.io仓库,在项目根build.gradle.kts中配置如下// Top-level build file buildscript { repositories { maven { setUrl("https://jitpack.io") } } dependencies { // BCU 插件依赖 classpath("com.github.Ysj001.BytecodeUtil:plugin:<lastest-version>") } } subprojects { repositories { maven { setUrl("https://jitpack.io") } } }
-
在
app模块的build.gradle.kts中的配置如下plugins { id("com.android.application") id("org.jetbrains.kotlin.android") // 添加 bcu 插件 id("bcu-plugin") } // 插件扩展 bcu { config { variant -> // 设置插件日志级别 (0 ~ 5) loggerLevel = 2 // 挂载你所需的修改器,可以挂载多个,插件内部按顺序执行 modifiers = arrayOf( // Your Modifier ) } filterNot = { variant, entryName -> // 自定义过滤规则 // 该配置会极大影响编译性能,具体见后面性能对比 // 返回 true 表示该 class 不进行 transform 处理 false } } -
BCU会将需要保留的目标加上BCUKeep注解,因此只需如下配置即可-keepclassmembers class * { @com.ysj.lib.bytecodeutil.plugin.api.BCUKeep <methods>; }
推荐直接查看 app 模块,或者查看基于 BCU 实现的库。
开发并使用一个自定义的 Modifier 非常简单,主要为以下几个步骤:
-
创建一个用于开发
Modifier的java模块,并依赖plugin-api和gradleApiplugins { id("java-library") id("kotlin") } dependencies { implementation(gradleApi()) implementation("com.github.Ysj001.BytecodeUtil:plugin-api:<lastest-version>") } -
继承
IModifier接口并重写对应方法class CustomModifier( // 注意:executor , allClassNode 顺序不能变 override val executor: Executor, override val allClassNode: Map<String, ClassNode>, ) : IModifier { private val logger = YLogger.getLogger(javaClass) override fun initialize(project: Project, variant: com.android.build.api.variant.Variant) { super.initialize(project) // 初始化阶段,可以通过 project 拿到所需的配置参数 logger.lifecycle("step1:initialize. variant=${variant.name}") // 演示获取自定义参数 logger.lifecycle(project.properties["modifier.custom"].toString()) } override fun scan(classNode: ClassNode) { // 扫描阶段,该阶段可以获取到所有过滤后需要处理的 class logger.lifecycle("step2:scan -->$classNode") // 你可以在这里收集需要处理的 class // 注意:该方法非多线程安全,内部处理记得按需加锁 } override fun modify() { // 处理阶段,该阶段是最后一个阶段,用于修改 scan 阶段收集的 class logger.lifecycle("step3:modify") } }
-
在
app模块中使用bcu-plugin插件并添加这个自定义的Modifierplugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("bcu-plugin") } bcu { config { variant -> // 在这里配置你的 Modifier 实现,多个 Modifier 会按顺序依次执行 modifiers = arrayOf( // 将 CustomModifier 添加到 bcu 中 CustomModifier::class.java, ) } } // 演示给 CustomModifier 传递自定义参数 ext["modifier.custom"] = "这是自定义的参数"
了解 BCU 的日志系统有助于帮助你开发出更高性能的 Modifier ,它能够直观的告诉你各个不同阶段的耗时,好让你便于进行优化。
如下图所示 BCU 在运行过程中会执行 2 个任务。
分别为 <variant>BCUTransformTask 和 <variant>BCUAppendTask 。
BCUAppendTask 负责处理增量而 BCUTransformTask 负责处理整个修改字节码的过程,因此下面的参数解析主要针对该任务中的 log。
log 对应的具体含义:
| log key | 对应含义 |
|---|---|
| >>> loggerLevel | 当前 BCU 的日志等级(0~5) |
| >>> variant | 当前变体名称 |
| >>> apply modifier | 添加进 BCU 的 Modifier |
| >>> xxxModifier initialize time | xxxModifier 在 initialize 阶段的耗时 |
| >>> bcu scan time | BCU 在 scan 阶段的总耗时 |
| >>> xxxModifier process time | xxxModifier 在 modify 阶段的耗时 |
| >>> bcu modify time | BCU 在 modify 阶段的总耗时 |
| >>> bcu transform output time | BCU 将 transform 结果输出到 jar file 的耗时 |
| >>> total process time | BCUTransformTask 处理过程的总耗时 |
在聊性能前,我们需要先了解下新的 ScopedArtifactsOperation#toTransform 接口造成的问题。
在新 transform 接口中你的输出只能是一个 jar 文件,这会导致如下耗时问题:
- 将
class打进jar的操作只能是个单线程的IO操作,性能利用率极低。 jar本质是个zip包,默认不配置压缩等级时在class文件打入过程还会有额外的压缩计算。dexBuilderXXX任务无法处理 jar 输入的增量编译,使得任意代码改动都会造成该任务的全量编译。
本库为了解决这些问题做了如下处理:
- 设置
jar的压缩等级为不压缩,缩短transform时的压缩时间和dexBuilderXXX时的解压时间。 - 提供
filter接口,减少需要进入transform的class数量,加快输出jar的速度,且减少由于jar导致的dexBuilderXXX任务缓存大面积失效问题。 - 由于
filter接口缩减了transform输出的源码,因此这部分源码需要借由ScopedArtifactsOperation#toAppend接口输出,而基于append接口的Task本身可以支持增量,并且且输出为直接的class文件目录,而这部分输出对dexBuilderXXX任务来说是也支持增量的,因此最后所有filter出去的class就都支持增量编译了。
在了解了本库对提升构建性能所做的处理后,相信你也能更加理解合理配置 fliter 的重要性。
此外由于 filter 后存在额外的计算(使用增量后 gradle 本身会对增量做计算),因此首次编译时构建速度会比不使用 filter 还要慢,但在此之后的编译就能体验到增量的超快速度了,相信这能极大提升你的代码调试效率。
下面是在实际项目上的性能的对比测试,供你参考:
| 全量 | 增量 |
|---|---|
- 了解 Gradle 和 Transform 点这(文章基于 AGP4)
- 如果本项目给予了你帮助那就给个 start 吧。
- 如果对本项目有疑问欢迎提 issues。
- 如果想聊聊本库相关问题可加 Q 群 732198194(注明来源)