刚学会Transform,你告诉我就要被移除了

9,610 阅读10分钟

背景

我们工程中使用的AGP是4.0.2版本,在去年升级的,目前最新版本是AGP7.2.1,已经落后官方5个大版本了 gradle升级是必然的,只是时候未到而已。同时在去年也进行了AGP7.0升级调研,由于种种原因,按下了暂停键。但是这不妨碍我们与时俱进,去学习新的知识。

image.png 今天给大家介绍我们未来会使用到的一个新特性,先看下面这张图

image.png

从 AGP1.5 一直存在的Transform API在AGP7.0被标记为废弃了,AGP8.0会直接移除掉,连AGP中最稳定的Transform API都被废弃了,所以技术的更新迭代,没有什么是不可能的,只有变化才能适用发展

根据Google的计划AGP8.0在今年年中就会正式发布

什么是Transform

已经2022年了,作为一名Android工程师,我相信大家都知道Transform是什么,如果有人不知道,也没关系,因为Transform已经快过时了,按照惯例,这里我还是要介绍一下Transform

image.png

AGP包含了一个Transform API, 它允许第三方插件在编译后的class文件转换为dex文件之前做处理操作. 而使用Transform API, 我们完全可以不用去关注相关task的生成与执行流程, 我们可以只聚焦在如何对输入的class文件进行处理

  • 工作时机 :Transform工作在Android构建中由Class → Dex 的节点

  • 处理对象: 处理对象包括Javac编译后的Class文件、Java标准 resource 资源、本地依赖和远程依赖的 JAR/AAR

  • Transform Task: 每个Transform都对应一个Task,Transform 的输入和输出可以理解为对应 Transform Task 的输入输出

  • Transform 链: TaskManager 会将每个TransformTask 串联起来,前一个Transform的输出会作为下一个 Transform 的输入

Transform使用场景

  • 对编译class文件做自定义的处理
  • 读取编译产生的class文件,做一些其他事情,但是不需要修改它。

Transform替代API

image.png 官方解释
Transform API被移除的原因:为了提高构建性能,使用Transform API很难与其它Gradle特性结合使用

Transform API 没有单一的替代 API,每个case都会有新的针对性 API。所有替代 API 都位于 androidComponents {} 代码块中,在 AGP 7.2 中均有提供。

Transform替代API示例

先贴一段代码,这段代码就是后面要讲到的内容

友情提示:后文代码较多,代码密集恐惧症患者可以直接看注释和文字

class ExamplePlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)

        androidComponents.onVariants { variant ->
            //基于不同的变种增加对应的适配工作
            println("variant.name--${variant.name}")
            //最终会在transformClassesWithAsm这个task里面执行
            variant.instrumentation.transformClassesWith(
                ExampleClassVisitorFactory::class.java,
                InstrumentationScope.ALL//扫描范围
            ) {
                it.writeToStdout.set(true)
            }
            variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
        }
    }

    interface ExampleParams : InstrumentationParameters {
        @get:Input
        val writeToStdout: Property<Boolean>
    }
    //AsmClassVisitorFactory根据官方说法,编译速度会有提升,大概18%左右
    abstract class ExampleClassVisitorFactory : AsmClassVisitorFactory<ExampleParams> {
        
        override fun createClassVisitor(
            classContext: ClassContext,
            nextClassVisitor: ClassVisitor
        ): ClassVisitor { //ClassNode和ClassVisitor
            return if (parameters.get().writeToStdout.get()) {
                TraceClassVisitor(nextClassVisitor, PrintWriter(System.out))
            } else {
                TraceClassVisitor(nextClassVisitor, PrintWriter(File("trace_out")))
            }
        }

        //判断当前类是否要进行扫描,ClassData则包含了类的一些信息
        override fun isInstrumentable(classData: ClassData): Boolean {
            return classData.className.startsWith("com.wuba.instrument")
        }
    }
}

网上有人说Transform Action是替代方案,但是从官方的demo看来,官方推荐的应该是AsmClassVisitorFactory

AsmClassVisitorFactory

A factory to create class visitor objects to instrument classes.

The implementation of this interface must be an abstract class where the parameters and instrumentationContext are left unimplemented. The class must have an empty constructor which will be used to construct the factory object.

AsmClassVisitorFactory根据官方说法,编译速度会提升大概18%左右,同时可以减少约5倍代码

image.png

interface AsmClassVisitorFactory<ParametersT : InstrumentationParameters> : Serializable {

    
    @get:Nested
    val parameters: Property<ParametersT>

  
    @get:Nested
    val instrumentationContext: InstrumentationContext

    //我们要做的就是在这个方法中返回一个ClassVisitor
    fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor

    //判断当前类是否要进行扫描,可以通过这个方法过滤掉很多我们不需要扫描的类
    fun isInstrumentable(classData: ClassData): Boolean
interface ClassData {
    /**
     * Fully qualified name of the class.
     */
    val className: String

    /**
     * List of the annotations the class has.
     */
    val classAnnotations: List<String>

    /**
     * List of all the interfaces that this class or a superclass of this class implements.
     */
    val interfaces: List<String>

    /**
     * List of all the super classes that this class or a super class of this class extends.
     */
    val superClasses: List<String>

ClassData并不是ASM的API,所以其中包含的内容相对来说比较少,但是应该也勉强够用,成员变量都比较容易理解,这里就不过多解释了

新的Extension

AGP版本升级7.0之后,应该是为了区分新旧版的Extension,所以在AppExtension的基础上,新增了一个AndroidComponentsExtension出来。

我们的transformClassesWith方法就需要注册在这个上面。这个需要考虑到变种(variant),和之前的Transform还是有比较大的区别的,这样我们就可以基于不同的变种增加对应的适配工作了。查看源码发现AsmClassVisitorFactory 最终是运行在TransformClassesWithAsmTask里面

TransformClassesWithAsmTask

image.png

他有一个即视感非常强烈的前缀,并且其中的成员和Transform中的成员也比较相似

//维护了一个ClassVistorFactory的列表
abstract val visitorsList: ListProperty<AsmClassVisitorFactory<*>>

//下面两个field标记输入(有.class文件和.jar文件),Transform中对应的就是transformInvocation.inputs
abstract val inputClassesDir: ConfigurableFileCollection

abstract val inputJarsDir: DirectoryProperty

// 这个field用于确定输出目录
abstract val inputJarsWithIdentity: JarsClasspathInputsWithIdentity

// 输出,Transform中对应的是transformInvocation.outputProvider
abstract val classesOutputDir: DirectoryProperty

abstract val jarsOutputDir: DirectoryProperty

inputClassesDir和inputJarsDir的赋值也和TransformManager添加Stream时指定的FileCollection相同

task.inputClassesDir.from(
    creationConfig.artifacts.getAll(MultipleArtifact.ALL_CLASSES_DIRS)
)

task.inputJarsWithIdentity.inputJars.from(
    creationConfig.artifacts.getAll(MultipleArtifact.ALL_CLASSES_JARS)
)

由此可见旧版的Transform和新增的TransformClassesWithAsmTask处理的源文件基本是一样的

不同的点就在于transform这个流程。

Transform使用Stream的方式是每次被消耗后,都会通过在build/intermediates/transforms文件夹下生成对应的输出目录,然后下一个Transform再使用之前的输出目录作为输入,多次IO叠加在一起,编译耗时呈线性增长

image.png

TransformClassesWithAsmTask,相当于强制使用了ASM进行字节码的处理,使用visitorsList这个field维护了一个ClassVistorFactory的列表,进行transform流程时,只需要依次应用对应的ClassVisitor即可,不需要再为每一次transform准备一个IO的输出。

AsmClassVisitorFactory优点

  • 不用写复杂的增量构建 TransformClassesWithAsmTask继承自NewIncrementalTask,已经处理了增量逻辑,不需要我们再手动处理

  • 构建性能大幅提升

在AsmInstrumentationManager中做统一处理,只进行一次IO操作,可以有效地减少IO操作,这是新版本API构建性能提升的主要原因

ASM相比于AspectJ和Javassist有明显的性能优势,强制使用ASM,这是构建性能提升的次要原因(占比较小)

AsmClassVisitorFactory缺点

  • 没有之前Transform API的灵活性,它和ASM字节码工具是绑定的,不支持其他字节码修改工具。
  • 对于一些复杂的修改,可能需要使用Gradle Task来进行实现
  • ASM学习成本更高

新API是否稳定

AsmClassVisitorFactory拥有更好的性能,更容易适配Gradle的新特性,目前AGP 7.2.1版本已经达到稳定阶段,可以作为生产环境使用,AGP8.0会正式移除Transform ,有图为证

Google官方给的demo里面 image.png

image.png

AGP内部Transform变化

网上很多人提到 “自定义Transform的执行时机早于系统内置Transform”,但从AGP 7.2.1源码看,并不存在系统 Transform。猜测是新版本AGP将系统内置Transform修改为Task直接实现,毕竟从AGP7.0开始Transform标记为过时了

为了验证这个猜测,我对比了AGP3.5.3、4.0.2和7.2.1版本的代码,下面我们一起来看下AGP系统内置Transform的发展变化

ApplicationTaskManager类 Transform=>Task代码变迁

  1. AGP 3.5.3 先添加自定义Transform,再添加系统Transform
public void createPostCompilationTasks(
        @NonNull final VariantScope variantScope) {
    TransformManager transformManager = variantScope.getTransformManager();
    // ----- External Transforms -----  自定义Transform添加
    // apply all the external transforms.
    List<Transform> customTransforms = extension.getTransforms();
    List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies();

    for (int i = 0, count = customTransforms.size(); i < count; i++) {
        Transform transform = customTransforms.get(i);

        List<Object> deps = customTransformsDependencies.get(i);
        transformManager.addTransform(...)
    }

    // 系统Tranform
    if (variantData.getType().isFeatureSplit() || variantScope.consumesFeatureJars()) {
        createMergeClassesTransform(variantScope);
    }
    // ----- Android studio profiling transforms 系统Tranform
    if (appliesCustomClassTransforms(variantScope, projectOptions)) {
        for (String jar : getAdvancedProfilingTransforms(projectOptions)) {
            if (jar != null) {
                transformManager.addTransform(...);
            }
        }
    }
    // ----- Minify next -----  系统Tranform
    CodeShrinker shrinker = maybeCreateJavaCodeShrinkerTransform(variantScope);
    if (shrinker == CodeShrinker.R8) {
        maybeCreateResourcesShrinkerTransform(variantScope);
        maybeCreateDexSplitterTransform(variantScope);
        return;
    }
    //系统Tranform
    maybeCreateResourcesShrinkerTransform(variantScope);
    //系统Tranform
    maybeCreateDexSplitterTransform(variantScope);
}
  1. AGP 4.0.2 部分系统Transform替换为Task实现
public void createPostCompilationTasks(
        @NonNull final VariantScope variantScope) {

    TransformManager transformManager = variantScope.getTransformManager();

    // ----- External Transforms ----- 自定义Tranform
    // apply all the external transforms.
    List<Transform> customTransforms = extension.getTransforms();
    List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies();

    boolean registeredExternalTransform = false;
    for (int i = 0, count = customTransforms.size(); i < count; i++) {
        Transform transform = customTransforms.get(i);

        List<Object> deps = customTransformsDependencies.get(i);
        registeredExternalTransform |=
                transformManager.addTransform(...)        
    }

    // ----- Android studio profiling transforms 系统Tranform
    if (appliesCustomClassTransforms(variantScope, projectOptions)) {
        for (String jar : getAdvancedProfilingTransforms(projectOptions)) {
            if (jar != null) {
                transformManager.addTransform(...);
            }
        }
    }

    // ----- Minify next -----  系统实现方式从Tranform变为Task
    maybeCreateCheckDuplicateClassesTask(variantScope);
    CodeShrinker shrinker = maybeCreateJavaCodeShrinkerTask(variantScope);
    if (shrinker == CodeShrinker.R8) {
        maybeCreateResourcesShrinkerTasks(variantScope);
        maybeCreateDexDesugarLibTask(variantScope, false);
        return;
    }
    //系统实现方式从Tranform变为Task
    maybeCreateResourcesShrinkerTasks(variantScope);
    //系统实现方式从Tranform变为Task
    maybeCreateDexSplitterTask(variantScope);
}
  1. AGP7.2.1 有意思的是,Google的开发者把Transforms的变量名都修改了,从customTransforms改为LegacyTransforms。除了遗留的Transform,其它的系统Transform全部替换为Task实现,可以大胆预测一下,在AGP8.0中添加遗留Transforms的这段代码会被删除掉
fun createPostCompilationTasks(creationConfig: ApkCreationConfig) {
    Preconditions.checkNotNull(creationConfig.taskContainer.javacTask)
    val transformManager = creationConfig.transformManager
    taskFactory.register(MergeGeneratedProguardFilesCreationAction(creationConfig))

    // ----- External Transforms ----- 遗留的transform,大胆预测一下,AGP8.0会把这段代码删掉
    val registeredLegacyTransform = addExternalLegacyTransforms(transformManager, creationConfig)

    // New gradle-transform jacoco instrumentation support.
    if (isTestCoverageEnabled && jacocoTransformEnabled) {
        if (registeredLegacyTransform) {
            createJacocoTaskWithLegacyTransformSupport(creationConfig)
        } else {
            createJacocoTask(creationConfig)
        }
    }
    //上面提到的TransformClassesWithAsmTask
    maybeCreateTransformClassesWithAsmTask(
        creationConfig as ComponentImpl,
        isTestCoverageEnabled
    )

    // Produce better error messages when we have duplicated classes.
    maybeCreateCheckDuplicateClassesTask(creationConfig)

    // Resource Shrinking
    maybeCreateResourcesShrinkerTasks(creationConfig)

    // Code Shrinking
    // Since the shrinker (R8) also dexes the class files, if we have minifedEnabled we stop
    // the flow and don't set-up dexing.
    maybeCreateJavaCodeShrinkerTask(creationConfig)
    if (creationConfig.minifiedEnabled) {
        maybeCreateDesugarLibTask(creationConfig, false)
        return
    }
    .....
}

新的Variant API

image.png build 类型 (buildTypes) 和产品变种 (productFlavors) 都是项目的 build.gradle 文件中的概念。Android Gradle 插件会根据这些定义生成不同的变体对象,并对应各自的构建任务(task)。这些构建任务的输出会被注册为与任务对应的工件 (artifact),并且根据需要被分为公有工件和私有工件。

早期版本的 AGP API 允许访问这些构建任务,但是这些 API 并不稳健,因为每个任务的具体实现细节是会发生改变的。Android Gradle 插件在 7.0 版本中引入了新的 API,可以访问到这些变体对象和一些中间工件。 新的 Variant 接口、Extension 接口公开的 API 比之前少了,但更加规范

Artifacts API

主要负责将我们的Task,插入到编译流程内,让我们尽量少的关注到输入和输出,Artifact APIs 规范了一套标准操作,使得我们可以简易地和已有的数据、中间产物进行交互

image.png

// buildSrc/src/main/kotlin/ToyPlugin.kt,官方的一个示例
abstract class ToyPlugin: Plugin<Project> {
  override fun apply(project: Project) {
    val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
 
    androidComponents.onVariants { variant ->
      val taskProvider = 
        project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {
          it.content.set("foo")
        }
 
      // 核心部分
      variant.artifacts
        .use(taskProvider)
        .wireWith(AddAssetTask::outputDir)
        .toAppendTo(MultipleArtifact.ASSETS)
    }
  }
}

对于AGP7.x Variant部分,恕我太菜,还没有进行比较深入的研究,大家有兴趣可以自行研究一下

业界插件架构

滴滴Booster和字节ByteX实现机制都是基于一个BaseTransform,但是Booster和ByteX这两个流行的开源框架,其实都是对Transform有所隔离的, 这么抽象的好处就是当发生了这种AGP的API过期,适配调整的时候,就可以避免所有写了Transform的插件一起调整了。只需要底层进行对应的适配工作,上层不需要做过多修改。

现在我们项目中使用的内部插件,基本上还是各自为营,后续升级AGP肯定会有一次痛苦的经历

总结

  1. Transform API在AGP7.0已标记为废弃,并且将在AGP8.0(2022年中会正式发布)中移除,我们是时候了解一下如何迁移Transform API了,尤其是经常开发公共插件的同学

  2. AsmClassVisitorFactory相比Transform API,使用起来更加简单,不需要手动处理增量逻辑,可以专注于字节码插桩操作。通过减少IO的方式,编译速度可以提升18%左右

  3. Transform的替换肯定会成为后续升级AGP的一个坎,可能类似当初开启R8编译一样,升上去了编译速度就会得到质的飞越,升不上去就只能止步不前,当然了这一切离我们还很遥远,毕竟我们现在用的还是AGP4.0.2

参考文章

本文借鉴了很多大佬优秀的文章,感谢下面的文章作者,如有遗漏,请及时联系我
juejin.cn/post/701614…
juejin.cn/post/705639…
juejin.cn/post/709875…
blog.csdn.net/qq_33902817…
developer.android.com/studio/buil…
juejin.cn/post/710592…
nebulae-pan.github.io/2021/12/25/…