专业的编程技术博客社区

网站首页 > 博客文章 正文

Transform 被废弃,TransformAction 了解一下

baijin 2024-08-28 11:24:53 博客文章 3 ℃ 0 评论

前言

Transform APIAGP1.5 就引入的特性,主要用于在 Android 构建过程中,在 ClassDex 的过程中修改 Class 字节码。利用 Transform API ,我们可以拿到所有参与构建的 Class 文件,然后可以借助 ASM 等字节码编辑工具进行修改,插入自定义逻辑。

国内很多团队都或多或少的用 AGPTransform API 来搞点儿黑科技,比如无痕埋点,耗时统计,方法替换等。但是在 AGP7.0Transform 已经被标记为废弃了,并且将在 AGP8.0 中移除。

Transrom 被废弃之后,它的代替品则是 Transform Action ,它是由 Gradle 提供的产物变换 API

Transform API 是由 AGP 提供的,而 Transform Action 则是由Gradle提供。不光是 AGP 需要TransformJava 也需要,所以由 Gradle 来提供统一的 Transform API 也合情合理。

当然如果你只是想利用 ASM 对字节码插桩, AGP 提供了对基于 TransformActionASM 插桩的封装,只需要使用 AsmClassVisitorFactory 即可.

而本文主要包括以下内容:

  1. TransformAction 是什么?
  2. 如何自定义 TransformAction
  3. TransformActionAGP 中的应用

TransformAction是什么?

简单来说, TransformAction 就是 Gradle 提供的产物转换 API ,可以注册两个属性间的转换Action ,将依赖从一个状态切换到另一个状态

我们在项目中的依赖,可能会有多个变体,例如,一个依赖可能有以下两种变体: classesorg.gradle.usage=java-api , org.gradle.libraryelements=classes )或 JARorg.gradle.usage=java-api, org.gradle.libraryelements=jar )

它们的主要区别就在于,一个的产物是 jar ,一个则是 classes (类目录)

Gradle 解析配置时,解析的配置上的属性将确定请求的属性,并选中匹配属性的变体。例如,当配置请求 org.gradle.usage=java-api, org.gradle.libraryelements=classes 时,就会选择 classes 目录作为输入。

但是如果依赖项没有所请求属性的变体,那么解析配置就会失败。有时我们可以将依赖项的产物转换为请求的变体。

例如,解压缩 JarTranformAction 会将 java-api , jars 转换为 java-api,classes 变体。

这种转换可以对依赖的产物进行转换,所以称为“产物转换” 。 Gradle 允许注册产物转换,并且当依赖项没有所请求的变体时, Gradle 将尝试查找一系列产物转换以创建变体。

TransformAction选择和执行逻辑

如上所述,当 Gradle 解析配置并且配置中的依赖关系不具有带有所请求属性的变体时, Gradle 会尝试查找一系列 TransformAction 以创建变体。

每个注册的转换都是从一组属性转换为一组属性。例如,解压缩转换可以从 org.gradle.usage=java-api, org.gradle.libraryelements=jars 转换至 org.gradle.usage=java-api, org.gradle.libraryelements=classes

为了找到一条这样的链, Gradle 从请求的属性开始,然后将所有修改某些请求的属性的 TransformAction 视为通向那里的可能路径。

例如,考虑一个 minified 属性,它有两个值: truefalseminified 属性表示依赖项的变体,表示删除了不必要的类文件。

如果我们的依赖只有 minified=false 的变体,并且我们的配置中请求了 minified=true 的属性,如果我们注册了 minify 的转换,那么它就会被选中

在找到的所有变换链中, Gradle 尝试选择最佳的变换链:

  1. 如果只有一个转换链,则选择它。
  2. 如果有两个变换链,并且一个是另一个的后缀,则将其选中。
  3. 如果存在最短的变换链,则将其选中。
  4. 在所有其他情况下,选择将失败并报告错误。

同时还有两个特殊情况:

  1. 当已经存在与请求属性匹配的依赖项变体时, Gradle 不会尝试选择产物转换。
  2. artifactType 属性是特殊的,因为它仅存在于解析的产物上,而不存在于依赖项上。因此任何只变换 artifactTypeTransformAction ,只有在使用ArtifactView时才会考虑使用

自定义 TransformAction

下面我们就以自定义一个 MinifyTransform 为例,来看看如何自定义 TransformAction ,主要用于过滤产物中不必要的文件

定义 TransformAction

abstract class Minify : TransformAction<Minify.Parameters> {   // (1)
    interface Parameters : TransformParameters {               // (2)
        @get:Input
        var keepClassesByArtifact: Map<String, Set<String>>

    }

    @get:PathSensitive(PathSensitivity.NAME_ONLY)
    @get:InputArtifact
    abstract val inputArtifact: Provider<FileSystemLocation>

    override
    fun transform(outputs: TransformOutputs) {
        val fileName = inputArtifact.get().asFile.name
        for (entry in parameters.keepClassesByArtifact) {      // (3)
            if (fileName.startsWith(entry.key)) {
                val nameWithoutExtension = fileName.substring(0, fileName.length - 4)
                minify(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}-min.jar"))
                return
            }
        }
        println("Nothing to minify - using ${fileName} unchanged")
        outputs.file(inputArtifact)                            // (4)
    }

    private fun minify(artifact: File, keepClasses: Set<String>, jarFile: File) {
        println("Minifying ${artifact.name}")
        // Implementation ...
    }
}
复制代码

代码很简单,主要分为以下几步:

TransformAction
transform

其实一个 TransformAction 主要就是输入,输出,变换逻辑三个部分

注册 TransformAction

接下来就是注册了,您需要注册 TransformAction ,并在必要时提供参数,以便在解析依赖项时可以选择它们

val artifactType = Attribute.of("artifactType", String::class.java)
val minified = Attribute.of("minified", Boolean::class.javaObjectType)

val keepPatterns = mapOf(
    "guava" to setOf(
        "com.google.common.base.Optional",
        "com.google.common.base.AbstractIterator"
    )
)

dependencies {
    attributesSchema {
        attribute(minified)                      // <1>
    }
    artifactTypes.getByName("jar") {
        attributes.attribute(minified, false)    // <2>
    }
}

configurations.all {
    afterEvaluate {
        if (isCanBeResolved) {
            attributes.attribute(minified, true) // <3>
        }
    }
}

dependencies {                                 // (4)
    implementation("com.google.guava:guava:27.1-jre")
    implementation(project(":producer"))
}

dependencies {
    registerTransform(Minify::class) { // <5>
        from.attribute(minified, false).attribute(artifactType, "jar")
        to.attribute(minified, true).attribute(artifactType, "jar")

        parameters {
            keepClassesByArtifact = keepPatterns
            // Make sure the transform executes each time
            timestamp = System.nanoTime()
        }
    }
}

tasks.register<Copy>("resolveRuntimeClasspath") { 
    from(configurations.runtimeClasspath)
    into(layout.buildDirectory.dir("runtimeClasspath"))
}
复制代码

注册 TransformAction 也分为以下几步:

  1. 添加 minified 属性
  2. 将所有JAR文件的 minified 属性设置为 false
  3. 在所有可解析的配置上设置请求的属性为 minified=true
  4. 添加将要转换的依赖项
  5. 注册 Transformaction ,设置 fromto 的属性,并且传递自定义参数

运行 TransformAction

在定义与注册了 TransformAction 之后,下一步就是运行了

上面我们自定义了 resolveRuntimeClasspathTask , Minify 转换会在我们请求 minified=true 的变体时调用

当我们运行 gradle resolveRuntimeClasspath 时就可以得到如下输出

> Task :resolveRuntimeClasspath
Nothing to minify - using jsr305-3.0.2.jar unchanged
Minifying guava-27.1-jre.jar
Nothing to minify - using failureaccess-1.0.1.jar unchanged
Nothing to minify - using listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar unchanged
Nothing to minify - using j2objc-annotations-1.1.jar unchanged
Nothing to minify - using checker-qual-2.5.2.jar unchanged
Nothing to minify - using error_prone_annotations-2.2.0.jar unchanged
Nothing to minify - using animal-sniffer-annotations-1.17.jar unchanged
复制代码

可以看出,当我们执行 task 的时候, gradle 自动调用了 TransformAction ,对 guava.jar进行了变换,并将结果存储在 layout.buildDirectory.dir("runtimeClasspath")

变换 ArtifactType的 TransformAction

上文提到, artifactType 属性是特殊的,因为它仅存在于解析的产物上,而不存在于依赖项上。因此任何只变换 artifactTypeTransformAction ,只有在使用ArtifactView时才会考虑使用

其实在 AGP 中,相当一部分自定义 TransformAction 都是属于只变换 ArtifactType 的,下面我们来看下如何自定义一个这样的 TransformAction

class TransformActionPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.run {
            val artifactType = Attribute.of("artifactType", String::class.java)
            dependencies.registerTransform(MyTransform::class.java) { // 1
                it.from.attribute(artifactType, "jar")
                it.to.attribute(artifactType, "my-custom-type")
            }
            val myTaskProvider = tasks.register("myTask", MyTask::class.java) { 
                it.inputCount.set(10)
                it.outputFile.set(File("build/myTask/output/file.jar"))
            }
            val includedConfiguration = configurations.create("includedConfiguration") // 2
            dependencies.add(includedConfiguration.name, "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10")

            val combinedInputs = project.files(includedConfiguration, myTaskProvider.map { it.outputFile })
            val myConfiguration = configurations.create("myConfiguration")
            dependencies.add(myConfiguration.name, project.files(project.provider { combinedInputs }))

            tasks.register("consumerTask", ConsumerTask::class.java) { // 3
                it.artifactCollection = myConfiguration.incoming.artifactView {viewConfiguration ->
                    viewConfiguration.attributes.attribute(artifactType, "my-custom-type")
                }.artifacts
                it.outputFile.set(File("build/consumerTask/output/output.txt"))
            }
        }
    }
}
复制代码

主要分为以下几步:

  1. 声明与注册自定义 Transform ,指定输入与输出的 artifactType
  2. 创建自定义的 configuration ,指定输入的依赖是什么(当然也可以直接用 AGP 已有的 configuration )
  3. 在使用时,通过自定义 configurationartifactView ,获取对应的产物
  4. ConsumerTask 中消费自定义 TransformAction 的输出产物

然后我们运行 ./gradlew consumerTask 就可以得到以下输出

> Task :app:consumerTask
Processing ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.7.10/bac80c520d0a9e3f3673bc2658c6ed02ef45a76a/kotlin-stdlib-common-1.7.10.jar. File exists = true
Processing ~/AndroidProject/2022/argust/GradleTutorials/app/build/myTask/output/file.jar. File exists = true
Processing ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.7.10/d70d7d2c56371f7aa18f32e984e3e2e998fe9081/kotlin-stdlib-jdk8-1.7.10.jar. File exists = true
Processing ~/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar. File exists = true
Processing ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.7.10/d2abf9e77736acc4450dc4a3f707fa2c10f5099d/kotlin-stdlib-1.7.10.jar. File exists = true
Processing ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.7.10/1ef73fee66f45d52c67e2aca12fd945dbe0659bf/kotlin-stdlib-jdk7-1.7.10.jar. File exists = true
复制代码

可以看出,当运行 consumerTask 时,执行了 MyTransform ,并将 jar 类型的产物转化成了 my-custom-type

TransformAction在 AGP中的应用

现在 AGP 中的 Transform 已经基本上都改成 TransformAction 了,我们一起来看几个例子

AarTransform

Android ARchive ,也就是 .aar 后缀的资源包, gradle 是如何使用它的呢?

如果有同学尝试过就知道,如果是默认使用 java-libray 的工程,肯定无法依赖并使用 aar的,引入时会报 Could not resolve ${dependencyNotation} ,说明在 Android Gradle Plugin当中,插件对 aar 包的依赖进行了处理,只有通过了插件处理,才能正确使用 aar 内的资源。那就来看看 AGP 是如何在 TransformAction 的帮助下做到这点的

Aar 转换的实现就是 AarTransform ,我们一起来看下源码:

// DependencyConfigurator.kt
for (transformTarget in AarTransform.getTransformTargets()) {
    registerTransform(
        AarTransform::class.java,
        AndroidArtifacts.ArtifactType.EXPLODED_AAR,
        transformTarget
    ) { params ->
        params.targetType.setDisallowChanges(transformTarget)
        params.sharedLibSupport.setDisallowChanges(sharedLibSupport)
    }
}

public abstract class AarTransform implements TransformAction<AarTransform.Parameters> {

    @NonNull
    public static ArtifactType[] getTransformTargets() {
        return new ArtifactType[] {
            ArtifactType.SHARED_CLASSES,
            ArtifactType.JAVA_RES,
            ArtifactType.SHARED_JAVA_RES,
            ArtifactType.PROCESSED_JAR,
            ArtifactType.MANIFEST,
            ArtifactType.ANDROID_RES,
            ArtifactType.ASSETS,
            ArtifactType.SHARED_ASSETS,
            ArtifactType.JNI,
            ArtifactType.SHARED_JNI,
            // ...
        };
    }

    @Override
    public void transform(@NonNull TransformOutputs transformOutputs) {
        // 具体实现
    }        
复制代码

代码也比较简单,主要做了下面几件事:

  1. DependencyConfigurator 中注册 Aar 转换成各种类型资源的 TransformAction
  2. AarTransform 中根据类型将 aar 包中的文件解压到输出到各个目录

JetifyTransform

Jetifier 也是在迁移到 AndroidX 之后的常用功能,它可以将引用依赖内的 android.support.* 引用都替换为对 androidx 的引用,从而实现对 support 包的兼容

下面我们来看一下 JetifyTransform 的代码

// com.android.build.gradle.internal.DependencyConfigurator

if (projectOptions.get(BooleanOption.ENABLE_JETIFIER)) {
    registerTransform(
        JetifyTransform::class.java,
        AndroidArtifacts.ArtifactType.AAR,
        jetifiedAarOutputType
    ) { params ->
        params.ignoreListOption.setDisallowChanges(jetifierIgnoreList)
    }
    registerTransform(
        JetifyTransform::class.java,
        AndroidArtifacts.ArtifactType.JAR,
        AndroidArtifacts.ArtifactType.PROCESSED_JAR
    ) { params ->
        params.ignoreListOption.setDisallowChanges(jetifierIgnoreList)
    }
}

// com.android.build.gradle.internal.dependency.JetifyTransform
override fun transform(transformOutputs: TransformOutputs) {
    val inputFile = inputArtifact.get().asFile

    val outputFile = transformOutputs.file("jetified-${inputFile.name}")
    jetifierProcessor.transform2(
        input = setOf(FileMapping(inputFile, outputFile)),
        copyUnmodifiedLibsAlso = true,
        skipLibsWithAndroidXReferences = true
    )
}
复制代码
  1. 读取并判断 ENABLE_JETIFIER 属性,这就是我们在 gradle.properties 中配置的 jetifier开关
  2. aarjar 类型的依赖都注册 JetifyTransform 转换
  3. transform 中对 support 包的依赖进行替换,完成后会将处理过的资源重新压缩,并且会带上 jetified 的前缀

总结

本文主要讲解了 TransformAction 是什么, TransformAction 自定义,以及 TransformActionAGP 中的应用,可以看出,目前 AGP 中的产物转换已经基本上都用 TransformAction 来实现了

事实上, AGPTransformAction 进行了一定的封装,如果你只是想利用 ASM 实现字节码插桩,那么直接使用 AsmClassVisitorFactory 就好了。但如果想要阅读 AGP 的源码,了解 AGP 构建的过程,还是需要了解一下 TransformAction 的基本使用与原理的

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表