AGP8.0 ASM 更简单灵活了

前言

在之前 《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"

 更多


  // access flags 0x2
  private Landroid/view/View; rootView

  // access flags 0x2
  private Lhome/smart/fly/animations/customview/CustomImageView; check

  // access flags 0x2
  private Z selected

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 24 L0
    ALOAD 0
    INVOKESPECIAL androidx/fragment/app/Fragment.<init> ()V
   L1
    LINENUMBER 30 L1
    ALOAD 0
    ICONST_0
    PUTFIELD home/smart/fly/animations/ui/activity/multifragments/OneFragment.selected : Z
   L2
    LINENUMBER 26 L2
    RETURN
   L3
    LOCALVARIABLE this Lhome/smart/fly/animations/ui/activity/multifragments/OneFragment; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1
  public onCreateView(Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View;
   L0
    LINENUMBER 36 L0
    LDC "OneFragment"
    LDC "onCreateView: "
    INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L1
    LINENUMBER 38 L1
    ACONST_NULL
    ALOAD 0
    GETFIELD home/smart/fly/animations/ui/activity/multifragments/OneFragment.rootView : Landroid/view/View;
    IF_ACMPEQ L2
   L3
    LINENUMBER 39 L3
    ALOAD 0
    GETFIELD home/smart/fly/animations/ui/activity/multifragments/OneFragment.rootView : Landroid/view/View;
    INVOKEVIRTUAL android/view/View.getParent ()Landroid/view/ViewParent;
    CHECKCAST android/view/ViewGroup
    ASTORE 4
   L4
    LINENUMBER 40 L4
    ACONST_NULL
    ALOAD 4
    IF_ACMPEQ L5
   L6
    LINENUMBER 41 L6
    ALOAD 4
    ALOAD 0
    GETFIELD home/smart/fly/animations/ui/activity/multifragments/OneFragment.rootView : Landroid/view/View;
    INVOKEVIRTUAL android/view/ViewGroup.removeView (Landroid/view/View;)V
   L5
    LINENUMBER 43 L5
   FRAME FULL [home/smart/fly/animations/ui/activity/multifragments/OneFragment android/view/LayoutInflater android/view/ViewGroup android/os/Bundle] []
    GOTO L7
   L2
    LINENUMBER 44 L2
   FRAME FULL [home/smart/fly/animations/ui/activity/multifragments/OneFragment android/view/LayoutInflater android/view/ViewGroup android/os/Bundle] []
    ALOAD 0
    ALOAD 1
    LDC 2131493187
    ALOAD 2
    ICONST_0
    INVOKEVIRTUAL android/view/LayoutInflater.inflate (ILandroid/view/ViewGroup;Z)Landroid/view/View;
    PUTFIELD home/smart/fly/animations/ui/activity/multifragments/OneFragment.rootView : Landroid/view/View;
   L8
    LINENUMBER 47 L8
    ALOAD 0
    INVOKEVIRTUAL home/smart/fly/animations/ui/activity/multifragments/OneFragment.loadData ()V
   L7
    LINENUMBER 52 L7
   FRAME FULL [home/smart/fly/animations/ui/activity/multifragments/OneFragment android/view/LayoutInflater android/view/ViewGroup android/os/Bundle] []
    ALOAD 0
    GETFIELD home/smart/fly/animations/ui/activity/multifragments/OneFragment.rootView : Landroid/view/View;
    ARETURN
   L9
    LOCALVARIABLE parent Landroid/view/ViewGroup; L4 L5 4
    LOCALVARIABLE this Lhome/smart/fly/animations/ui/activity/multifragments/OneFragment; L0 L9 0
    LOCALVARIABLE inflater Landroid/view/LayoutInflater; L0 L9 1
    LOCALVARIABLE container Landroid/view/ViewGroup; L0 L9 2
    LOCALVARIABLE savedInstanceState Landroid/os/Bundle; L0 L9 3
    MAXSTACK = 5
    MAXLOCALS = 5

  // access flags 0x2
  private loadData()V
   L0
    LINENUMBER 56 L0
    LDC "OneFragment"
    LDC "loadData: "
    INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L1
    LINENUMBER 57 L1
    RETURN
   L2
    LOCALVARIABLE this Lhome/smart/fly/animations/ui/activity/multifragments/OneFragment; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1
  public onViewCreated(Landroid/view/View;Landroid/os/Bundle;)V
    // annotable parameter count: 2 (invisible)
    @Landroidx/annotation/NonNull;() // invisible, parameter 0
    @Landroidx/annotation/Nullable;() // invisible, parameter 1
   L0
    LINENUMBER 61 L0
    ALOAD 0
    ALOAD 1
    ALOAD 2
    INVOKESPECIAL androidx/fragment/app/Fragment.onViewCreated (Landroid/view/View;Landroid/os/Bundle;)V
   L1
    LINENUMBER 62 L1
    ALOAD 1
    LDC 2131297671
    INVOKEVIRTUAL android/view/View.findViewById (I)Landroid/view/View;
    CHECKCAST android/webkit/WebView
    ASTORE 3
   L2
    LINENUMBER 63 L2
    ALOAD 3
    INVOKEVIRTUAL android/webkit/WebView.getSettings ()Landroid/webkit/WebSettings;
    ICONST_1
    INVOKEVIRTUAL android/webkit/WebSettings.setDomStorageEnabled (Z)V
   L3
    LINENUMBER 64 L3
    ALOAD 3
    LDC "https://www.baidu.com"
    INVOKEVIRTUAL android/webkit/WebView.loadUrl (Ljava/lang/String;)V
   L4
    LINENUMBER 65 L4
    RETURN
   L5
    LOCALVARIABLE this Lhome/smart/fly/animations/ui/activity/multifragments/OneFragment; L0 L5 0
    LOCALVARIABLE view Landroid/view/View; L0 L5 1
    LOCALVARIABLE savedInstanceState Landroid/os/Bundle; L0 L5 2
    LOCALVARIABLE webView Landroid/webkit/WebView; L2 L5 3
    MAXSTACK = 3
    MAXLOCALS = 4

  // access flags 0x1
  public onStart()V
   L0
    LINENUMBER 69 L0
    ALOAD 0
    INVOKESPECIAL androidx/fragment/app/Fragment.onStart ()V
   L1
    LINENUMBER 70 L1
    LDC "OneFragment"
    LDC "onStart: "
    INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L2
    LINENUMBER 71 L2
    RETURN
   L3
    LOCALVARIABLE this Lhome/smart/fly/animations/ui/activity/multifragments/OneFragment; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1
  public onResume()V
   L0
    LINENUMBER 75 L0
    ALOAD 0
    INVOKESPECIAL androidx/fragment/app/Fragment.onResume ()V
   L1
    LINENUMBER 76 L1
    LDC "OneFragment"
    LDC "onResume: "
    INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L2
    LINENUMBER 77 L2
    RETURN
   L3
    LOCALVARIABLE this Lhome/smart/fly/animations/ui/activity/multifragments/OneFragment; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1
}
 

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_CLASSESStack 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_CLASSESStack 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_METHODSStack 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_FRAMESStack 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 的差异。

TransformAsmClassVisitorFactory

transforms.png

AsmClassVisitorFactory.png

通过对比可以看到,新的 API 其实是将 class 被 ASM 处理时一头一尾的 IO 操作直接封装到了内部,也不用关心什么增量编译之类的,开发者只需要聚焦于实际的 ASM 处理。因此,AGP8 废弃 Transform 的同时也是简化了相关的操作。

代码:GitHub - REBOOTERS/AndroidAnimationExercise: Android 动画各种实现,包括帧动画、补间动画和属性动画的总结分享

参考文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值