前语

在之前 《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 参数决议是否进行插桩功用的后续流程。 根据相同的原理,咱们能够对之前完成过的 CatClassVisitorTigerClassVistor 创立工厂。 最后,咱们能够注册这些有不同功用的工厂 。一起为了便利操控插桩行为,咱们也能够复用之前创立的 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
AGP8.0 ASM 更简单灵活了
AGP8.0 ASM 更简单灵活了

经过比照能够看到,新的 API 其实是将 class 被 ASM 处理时一头一尾的 IO 操作直接封装到了内部,也不用关怀什么增量编译之类的,开发者只需求聚集于实践的 ASM 处理。因而,AGP8 抛弃 Transform 的一起也是简化了相关的操作。

参阅文档

  • Android Gradle plugin API updates
  • FramesComputationMode