前言
在之前 《Gradle Transform + ASM 探索》 一文中讨论了使用 AGP 提供的 Transform 接口自定义 Gradle 插件,通过 ASM 进行代码插桩。实现一些类似方法耗时统计,批量添加点击事件做埋点的功能。
但是随着 AGP8.0 的到来,Transform 接口已经被废弃了,至于废弃的原因官方解释也是很官方了。
The Transform API is being removed to improve build performance. Projects that use the Transform API force AGP to use a less optimized flow for the build that can result in large regressions in build times. It’s also difficult to use the Transform API and combine it with other Gradle features; the replacement APIs aim to make it easier to extend AGP without introducing performance or build correctness issues
说白了,就是不好用,传统的 Transform API 增加编译耗时。同时提供了更好用的 API 来规避一些编译时的问题。下面就来看看在 AGP8.0 中如何替换被废弃的 Transform。
更简单的代码插桩
关于这部分的迁移,其实 Android 官方已经提供了示例代码-testAsmTransformApi。
结合代码总结来说,只需要两步
实现特定的 AsmClassVisitorFactory
abstract class FooClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {
override fun isInstrumentable(classData: ClassData): Boolean {
return classData.className.startsWith("home.smart.fly.animations.ui.activity.multifragments.OneFragment")
}
override fun createClassVisitor(
classContext: ClassContext, nextClassVisitor: ClassVisitor
): ClassVisitor {
return TraceClassVisitor(nextClassVisitor, PrintWriter(System.out))
}
}
实现 AsmClassVisitorFactory 这个接口。并且通过 createClassVisitor
方法,返回一个 ClassVisitor 对象。看到这里的 ClassVisitor ,熟悉 ASM 的同学应该已经明白了,使用 AGP 进行代码插桩更简化了。其实在以往使用 Transform 接口的时候,也是通过 ClassVisitor 接口,通过每一个类的维度来进行处理。
至于这里的isInstrumentable
方法,顾名思义,通过其参数内提供的 class 信息可以决定是否对当前 class 进行处理。
注册 AsmClassVisitorFactory
实现好了进行 ASM 处理的工厂类,接着就是注册了。这里是不是似曾相识,之前是注册 Transform,现在是注册 Factory。
abstract class PhoenixPlugin : Plugin<Project> {
override fun apply(project: Project) {
androidComponents.onVariants { variant ->
variant.instrumentation.transformClassesWith(
FooClassVisitorFactory::class.java, InstrumentationScope.ALL
) {}
variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
}
}
}
代码中相关细节可以暂时忽略,后续会说到
注册的位置和之前一样,也是在自定义 Plugin 的 apply 方法中。
最后在 app 的 build.gradle 文件中导入 apply plugin: 'phoenix-plugin'
插件,就可以了。我们可以看一下效果。
TraceClassVisitor 的结果
// class version 61.0 (61)
// access flags 0x21
public class home/smart/fly/animations/ui/activity/multifragments/OneFragment extends androidx/fragment/app/Fragment {
// compiled from: OneFragment.java
// access flags 0x19
public final static INNERCLASS home/smart/fly/animations/R$layout home/smart/fly/animations/R layout
// access flags 0x19
public final static INNERCLASS home/smart/fly/animations/R$id home/smart/fly/animations/R id
// access flags 0x1A
private final static Ljava/lang/String; TAG = "OneFragment"
TraceClassVisitor 是 ASM 库自带的一个 ClassVisitor,可以实现 class 字节码的打印。从上面 gradle 编译期间打印的日志,可以看到这个功能已经生效了,也就是说我们在 FooClassVisitorFactory
中创建的 TraceClassVisitor 已经并且只对 home.smart.fly.animations.ui.activity.multifragments.OneFragment
这个类生效了。说明我们的 ASM 工厂也已经生效了。
自定义 ClassVisitor
下面通过自定义的 ClassVisitor 再说一下整个流程中的一些细节。这里以之前定义的 TrackClassVisitor
为例。这个 TrackClassVisitor
会对一个类中实现了 Android View.OnClickListener 接口的方法做特定的插桩。插桩的实现细节不再赘述,可以参考之前的《Gradle Transform + ASM 探索》 。这里主要看一下在新的场景中如何使用和适配。
首先还是自定义 Factory
abstract class TrackClassVisitorFactory :
AsmClassVisitorFactory<TrackClassVisitorFactory.TrackParam> {
override fun isInstrumentable(classData: ClassData): Boolean {
return true
}
interface TrackParam : InstrumentationParameters {
@get:Input
val trackOn: Property<Boolean>
}
override fun createClassVisitor(
classContext: ClassContext, nextClassVisitor: ClassVisitor
): ClassVisitor {
val trackOn = parameters.orNull?.trackOn?.get() ?: false
val api = instrumentationContext.apiVersion.get()
return TrackClassVisitor(api, trackOn, nextClassVisitor)
}
}
- 由于事先无法统计哪些类需要处理,因此这里
isInstrumentable()
直接返回 true ,对所有的类进行处理。 - 通过扩展 InstrumentationParameters 这个接口,定义了一个参数
trackOn
表示是否开启插桩功能,可以看到这个参数也变成了工厂的泛型参数。这里还可以基于插桩的需求定义其他的参数。 - 创建 TrackClassVisitor ,这里值得注意的是,在构造函数中特地传递了 api 这个参数。 这个参数有什么用呢?我们可以看一下他的注释
interface InstrumentationContext : Serializable {
/**
* The asm api version to be passed to the [ClassVisitor] constructor.
*
* ```
* | AGP version | Corresponding ASM version |
* |-------------|---------------------------|
* | 4.2.0 - 7.0 | ASM7 |
* | 7.1.0+ | ASM9 |
* ```
*/
@get:Input
val apiVersion: Property<Int>
}
可以看到,这里相当于是约束了 AGP 的版本和 ASM 的版本的依赖关系。我们知道使用 ASM 相关 API 的时候都要指定 ClassVisitor 或者 MethodVisitor 的 ASM 版本,这里通过上下文获取参数,可以更好的进行适配。简单看一下改变之后的 TrackClassVisitor
class TrackClassVisitor(api: Int, private val trackOn: Boolean, classVisitor: ClassVisitor) :
ClassVisitor(api, classVisitor) {
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
className = name
if (trackOn.not()) {
return
}
.....
}
override fun visitMethod(
access: Int,
name: String?,
desc: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
var methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
if (trackOn && hack) {
// println("name is $name, desc is $desc")
if (name.equals("onClick") && desc.equals("(Landroid/view/View;)V")) {
methodVisitor =
TrackMethodVisitor(className, api, methodVisitor, access, name, desc)
}
}
return methodVisitor
}
}
可以看到我们使用 api 这个参数规范了 Visitor 的创建,同时用 trackOn 参数决定是否进行插桩功能的后续流程。 基于同样的原理,我们可以对之前实现过的 CatClassVisitor
和 TigerClassVistor
创建工厂。 最后,我们可以注册这些有不同功能的工厂 。同时为了方便控制插桩行为,我们也可以复用之前创建的 PhoenixExtension 进行功能的配置。
abstract class PhoenixPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.extensions.create("phoenix", PhoenixExtension::class.java, project.objects)
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
val transformExtension = getTransformConfig(project)
androidComponents.onVariants { variant ->
variant.instrumentation.transformClassesWith(
FooClassVisitorFactory::class.java, InstrumentationScope.ALL
) {}
variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
variant.instrumentation.transformClassesWith(
TrackClassVisitorFactory::class.java, InstrumentationScope.PROJECT
) {
it.trackOn.set(transformExtension.trackOn)
}
variant.instrumentation.transformClassesWith(
CatClassVisitorFactory::class.java, InstrumentationScope.ALL
) {
it.catOn.set(transformExtension.catOn)
}
variant.instrumentation.transformClassesWith(
TigerClassVisitorFactory::class.java, InstrumentationScope.ALL
) {
it.tigerOn.set(transformExtension.tigerOn)
}
}
}
private fun getTransformConfig(project: Project): TransformExtension {
val phoenix = project.extensions.findByType(PhoenixExtension::class.java)
if (phoenix == null) {
val transformExtension = TransformExtension()
transformExtension.catOn = false
transformExtension.tigerOn = false
transformExtension.trackOn = false
transformExtension.tigerClassList = HashMap()
return transformExtension
}
return phoenix.transform
}
}
- 创建
phoenix
闭包,并且和 PhonenixExtension 做绑定。 - 在注册工厂之前获取实际配置的 phoenix 参数信息,将这些配置的信息透传到工厂内定义的参数中。比如以 trackOn 参数为例,这样就可以通过外部配置控制内部的插桩行为了。
- transformClassesWith 中的第二个参数,InstrumentationScope
enum class InstrumentationScope {
/**
* Instrument the classes of the current project only.
*
* Libraries that this project depends on will not be instrumented.
*/
PROJECT,
/**
* Instrument the classes of the current project and its library dependencies.
*
* This can't be applied to library projects, as instrumenting library dependencies will have no
* effect on library consumers.
*/
ALL
}
只有两个值 PROJECT 和 ALL ,意思很好理解就不多做解释了。对比 Transform ,也有 ScopeType
这样的枚举,甚至是可以处理 Resource 资源的,相对来说新的 API 在这方面有所收敛。
- setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES) 这个方法比较重要
FramesComputationMode 有四个枚举值
Enum Values | |
---|---|
COMPUTE_FRAMES_FOR_ALL_CLASSES | Stack frames and the maximum stack sizes will be skipped when reading the original classes, and will not be computed by ClassWriter. |
COMPUTE_FRAMES_FOR_INSTRUMENTED_CLASSES | Stack frames and the maximum stack sizes will be skipped when reading the original classes, and will be instead computed from scratch by ClassWriter based on the classpath of the original classes. |
COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS | Stack frames and the maximum stack size will be computed by ClassWriter for any modified or added methods based on the classpath of the original classes. |
COPY_FRAMES | Stack frames and the maximum stack sizes will be copied from the original classes to the instrumented ones, i.e. frames and maxs will be read and visited by the class visitors and then to the ClassWriter where they will be written without modification. |
这个值怎么设置决定了栈帧及其最大值会如何计算。对于插装操作来说,使用 COPY_FRAMES 模式就可以,默认值就是这个,设置多次的话,枚举值高的会覆盖低的。
最后,在 build.gradle 中配置一下插件
apply plugin: 'phoenix-plugin'
phoenix {
transform {
catOn = false
tigerOn = false
trackOn = true
tigerClassList = ["com/bumptech/glide/RequestManager": ["load"]]
}
}
这里配置的参数在 gradle configuration 阶段就可以解析完成,然后在插件注册工厂的时候就可以读取这些外部定义的值了。
编译运行 app 简单验证一下。
27289-27289 0Track home.smart.fly.animations E ┌───────────────────────────────────------───────────────────────────────────------ 27289-27289 1Track home.smart.fly.animations E │ class's name: home/smart/fly/animations/ui/activity/OptionalActivity 27289-27289 2Track home.smart.fly.animations E │ view's id: home.smart.fly.animations:id/elseUse 27289-27289 3Track home.smart.fly.animations E │ view's package name: home.smart.fly.animations 27289-27289 4Track home.smart.fly.animations E └───────────────────────────────────------───────────────────────────────────------ 27289-27289 0Track home.smart.fly.animations E ┌───────────────────────────────────------───────────────────────────────────------ 27289-27289 1Track home.smart.fly.animations E │ class's name: home/smart/fly/animations/ui/activity/OptionalActivity 27289-27289 2Track home.smart.fly.animations E │ view's id: home.smart.fly.animations:id/map_function 27289-27289 3Track home.smart.fly.animations E │ view's package name: home.smart.fly.animations 27289-27289 4Track home.smart.fly.animations E └───────────────────────────────────------───────────────────────────────────------ 27289-27289 0Track home.smart.fly.animations E ┌───────────────────────────────────------───────────────────────────────────------ 27289-27289 1Track home.smart.fly.animations E │ class's name: home/smart/fly/animations/ui/activity/OptionalActivity 27289-27289 2Track home.smart.fly.animations E │ view's id: home.smart.fly.animations:id/elseUse 27289-27289 3Track home.smart.fly.animations E │ view's package name: home.smart.fly.animations 27289-27289 4Track home.smart.fly.animations E └───────────────────────────────────------───────────────────────────────────------ 27289-27289 0Track home.smart.fly.animations E ┌───────────────────────────────────------───────────────────────────────────------ 27289-27289 1Track home.smart.fly.animations E │ class's name: home/smart/fly/animations/ui/activity/OptionalActivity 27289-27289 2Track home.smart.fly.animations E │ view's id: home.smart.fly.animations:id/elseUse 27289-27289 3Track home.smart.fly.animations E │ view's package name: home.smart.fly.animations 27289-27289 4Track home.smart.fly.animations E └───────────────────────────────────------───────────────────────────────────------
可以看到 TrackClassVisitor 代码插桩已经生效了。其他的两个 ClassVisitor 也是生效了,这里就不贴日志了。
更简单的注册方式
关于注册 Factory 的方式,其实还可以更简单,直接在 build.gradle 文件中实现,Factory 所需的参数可以直接传递,更灵活方便。这就意味着 自定义的 Factory 结合 ClassVisitor 可以有更灵活的复用场景,已经可以脱离自定义 Gradle 插件而存在了。毕竟注册都可以脱离 Plugin 了。
import com.engineer.plugin.transforms.FooClassVisitorFactory
import com.engineer.plugin.transforms.track.TrackClassVisitorFactory
import com.engineer.plugin.transforms.tiger.TigerClassVisitorFactory
import com.android.build.api.instrumentation.FramesComputationMode
import com.android.build.api.instrumentation.InstrumentationScope
androidComponents {
onVariants(selector().all(), {
instrumentation.transformClassesWith(FooClassVisitorFactory.class, InstrumentationScope.PROJECT) {}
instrumentation.transformClassesWith(TrackClassVisitorFactory.class,InstrumentationScope.PROJECT) { param ->
param.trackOn = true
}
instrumentation.transformClassesWith(TigerClassVisitorFactory.class,InstrumentationScope.PROJECT) { param ->
param.tigerOn = true
}
instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
})
}
- 在 Factory 里定义了什么参数,这里可以直接传递。
- InstrumentationScope 和 FramesComputationMode 的配置和之前也是一样的规则。
- 和上面的 Plugin 中一样,这里也可以基于 variant ,也就是 flavor 的变体做更灵活的配置了。更多细节可以参考代码 AndroidAnimationExercise
总结
最后,简单对比一下使用新的 API 和之前使用 Transform 的差异。
Transform | AsmClassVisitorFactory |
---|---|
通过对比可以看到,新的 API 其实是将 class 被 ASM 处理时一头一尾的 IO 操作直接封装到了内部,也不用关心什么增量编译之类的,开发者只需要聚焦于实际的 ASM 处理。因此,AGP8 废弃 Transform 的同时也是简化了相关的操作。