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

背景

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

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

image.png

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

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

什么是Transform

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

image.png

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

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

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

  • Transform Task: 每个 Transform 都对应一个 Task,Transform 的输入和输出可以理解为对应 Transform Task 的输入输出。每个 TransformTask 的输出都分别存储在 app/build/intermediates/transform/[TransformName]/[Variant] 文件夹中

  • 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版本升级之后,应该是为了区分新旧版的Extension,所以在AppExtension的基础上,新增了一个AndroidComponentsExtension出来。

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

TransformClassesWithAsmTask

他有一个即视感非常强烈的前缀,并且其中的成员也让人感觉非常熟悉

abstract val visitorsList: ListProperty<AsmClassVisitorFactory<*>>

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

abstract val inputJarsDir: DirectoryProperty

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

// 一些环境变量和classpath
abstract val runtimeClasspath: ConfigurableFileCollection

abstract val bootClasspath: ConfigurableFileCollection

// 输出,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这个流程了。TransformClassesWithAsmTask如其名所示,相当于强制使用了ASM进行字节码的处理,并且和之前Transform使用Stream的方式不同——Stream每次被消耗后,都会通过io在build/intermediates/transforms文件夹下生成对应的输出目录,然后下一个Transform再使用之前的输出目录作为输入。

TransformClassesWithAsmTask使用了visitorsList这个field维护了一个ClassVistorFactor的列表,进行transform流程时,也只需要依次应用对应的ClassVisitor即可,不需要再为每一次transform准备一个io的输出了

TransformClassesWithAsmTask目前其实并不灵活,绑定了ASM的使用,并且无法像Transform一样预先收集所有的依赖classpath

AsmClassVisitorFactory优点

  • 不用写复杂的增量构建
  • 老版本的Transform每个Transfrom各自独立,如果每个Transform编译构建耗时+10s,各个Transform叠在一起,编译耗时就会呈线性增长

而新版本可以看出我们也没有手动进行IO操作,这是因为AsmInstrumentationManager中已经做了统一处理,只需要进行一次IO操作,然后交给ClassVisitor链表处理,完成后统一交给ClassWriter写入,通过这种方式,可以有效地减少IO操作,这也是新版本API性能提升的原因

AsmClassVisitorFactory缺点

  • 它没有了之前 Transform API 的灵活性,目前看起来它和ASM字节码工具是绑定的,不支持 Javassist和Aspect等。
  • ASM学习成本更高

新API是否稳定

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

google官方给的demo里面 image.png

image.png

ASM简介

image.png

ASM为什么快

AspectJ在java --> class阶段,修改java代码;

Javassist和asm,都是修改的.class
Javassist在复杂的字节码级操作上提供了更高级别的抽象层,使用了反射机制,它比运行时使用Classworking技术的ASM慢

AGP内部的变化

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

为了验证这个猜测,我对比了3.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 除了自定义的Transform,其它的系统Transform全部替换为Task实现,可以大胆语言一下,在AGP8.0中添加自定义Transform的这段代码会被删除
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 插件会根据这些定义生成不同的变体对象,并对应各自的构建任务。这些构建任务的输出会被注册为与任务对应的工件 (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肯定会有一次痛苦的经历

另外吐槽一下AGP适配,Booster适配AGP升级非常快,已经适配了7.2版本,反观Qigsaw和Bytex毫无动静,还停留在4.x阶段原地踏步

总结

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

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

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

个人思考

目的:内部插件开发更加规范,为AGP升级提前扫清障碍

  • 是否需要开发我们自己的插件框架,制定插件开发规范和管理机制?
    项目中虽然已经引入了ByteX,但是ByteX适配AGP的速度堪称龟速

  • 以后编写新插件都基于ByteX或者自研的一套插件框架,对Transform进行隔离?

  • 是否有必要整合现有的内部插件?

参考文章

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…

猜你喜欢

转载自juejin.im/post/7114863832954044446
今日推荐