为什么说TransformAction不是Transform的替代品

1,627 阅读10分钟

背景

现在已经要步入AGP8.0时代了,Transform过时并被删除已经是板上钉钉的事儿了,网上传言TransformAction是Transform的替代品。事实究竟如何呢,我们要了解TransformAction不是什么,首先我们要了解它是什么。

让我想起了刚开始学Service的时候,老外写文档会说Service是一个专门在后台处理长时间任务的Android组件。

  1. Service不是一个单独的进程;
  2. Service也不是一个单独的线程;
    并且也会说Service不是什么,这种写注释的思路还是值得我们借鉴的。

好了回到正题,我们先了解一下TransformAction是什么

ps:

  • 本文主要讲了TransformAction中产物转换部分,至于配置依赖部分有兴趣的可以点击官方链接Transforming artifacts

什么是TransformAction

最直接的方式,看源码和官方文档TransformAction

//Interface for artifact transform actions.
public interface TransformAction<T extends TransformParameters> {  
  
    @Inject  
    T getParameters();  

    void transform(TransformOutputs outputs);  
}

API非常简洁,只有一个transform方法,getParameters方法还不需要实现,官方对它的介绍也比较简单依赖产物转换的接口

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

实现一个TransformAction

UnzipTransform的作用呢就是解压缩,很容易理解

abstract class UnzipTransform : TransformAction<TransformParameters.None> {          
    @get:InputArtifact                                                      
    abstract val inputArtifact: Provider<FileSystemLocation>

    override
    fun transform(outputs: TransformOutputs) {
        val input = inputArtifact.get().asFile
        val unzipDir = outputs.dir(input.name)                              
        unzipTo(input, unzipDir)                                            
    }

    private fun unzipTo(zipFile: File, unzipDir: File) {
        // implementation...
    }
}

注册TransformAction

注册是在dependencies闭包下进行注册

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

dependencies {
    registerTransform(UnzipTransform::class) {
        from.attribute(artifactType, "jar")
        to.attribute(artifactType, "java-classes-directory")
    }
}

注册它的作用是将jar类型的产物转换为java-classes-directory类型的产物 光看这个还是有点懵,到底怎么用呢,从我们熟悉的场景入手,最容易理解

AGP中TransformAction的应用

Gradle官方也举了一些例子,但是不太好理解,理解之后想不到实际使用场景,可能我比较菜

Jetifier依赖转换

[TransformAction] to convert a third-party library that uses old support libraries into an equivalent library that uses AndroidX.

将Support依赖替换为AndroidX依赖,无论项目是否包含support依赖,只要我们在gradle.properties中开启了android.enableJetifier=true都会进行转换操作

@CacheableTransform
abstract class JetifyTransform : TransformAction<JetifyTransform.Parameters> {

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

    override fun transform(transformOutputs: TransformOutputs) {
        val inputFile = inputArtifact.get().asFile

        // Case 1: If this is an AndroidX library, no need to jetify it
        if (jetifierProcessor.isNewDependencyFile(inputFile)) {
            transformOutputs.file(inputFile)
            return
        }

        // Case 2: If this is an old support library, it means that it was not replaced during
        // dependency substitution earlier, either because it does not yet have an AndroidX version,
        // or because its AndroidX version is not yet available on remote repositories. Again, no
        // need to jetify it.
        if (jetifierProcessor.isOldDependencyFile(inputFile)) {
            transformOutputs.file(inputFile)
            return
        }

        val jetifierIgnoreList: List<Regex> = getJetifierIgnoreList(parameters.ignoreListOption.get())

        // Case 3: If the library is ignored, do not jetify it
        if (jetifierIgnoreList.any { it.containsMatchIn(inputFile.absolutePath) }) {
            transformOutputs.file(inputFile)
            return
        }

        // Case 4: For the remaining libraries, let's jetify them
        val outputFile = transformOutputs.file("jetified-${inputFile.name}")
        val result = try {
            jetifierProcessor.transform2(
                input = setOf(FileMapping(inputFile, outputFile)),
                copyUnmodifiedLibsAlso = true,
                skipLibsWithAndroidXReferences = true
            )
        } catch (exception: Exception) {
            ....
            throw RuntimeException(message, exception)
        }
    }
}


实现原理

jetifier将要处理的依赖分为4类

1.androidx的依赖库
2.废弃的support依赖库
3.被配置为忽略的依赖库
4.不符合上述条件的其他依赖库

transform结束后会将处理过的资源重新压缩,比如.class文件、.java文件、xml文件、proguard.txt等等,并且会带上jetified的前缀, 从而供下游的TransformAction消费。

注册时机

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)  
    }  
}

是在DependencyConfigurator中进行注册,这里我们只要记住一个点,是在依赖配置中进行注册

Android工程aar转换成jar

Transform that returns the content of an extracted AAR folder

我们知道Java工程是无法使用AAR的,但是Android工程可以,AGP是如何实现的呢

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

    @PathSensitive(PathSensitivity.ABSOLUTE)
    @InputArtifact
    public abstract Provider<FileSystemLocation> getInputArtifact();

    @NonNull
    public static ArtifactType[] getTransformTargets() {
        return new ArtifactType[] {
            // For CLASSES, this transform is ues for runtime, and AarCompileClassesTransform is
            // used for compile
            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,
            ArtifactType.AIDL,
            ArtifactType.RENDERSCRIPT,
            ArtifactType.UNFILTERED_PROGUARD_RULES,
            ArtifactType.LINT,
            ArtifactType.ANNOTATIONS,
            ArtifactType.PUBLIC_RES,
            ArtifactType.COMPILE_SYMBOL_LIST,
            ArtifactType.DATA_BINDING_ARTIFACT,
            ArtifactType.DATA_BINDING_BASE_CLASS_LOG_ARTIFACT,
            ArtifactType.RES_STATIC_LIBRARY,
            ArtifactType.RES_SHARED_STATIC_LIBRARY,
            ArtifactType.PREFAB_PACKAGE,
            ArtifactType.AAR_METADATA,
            ArtifactType.ART_PROFILE,
            ArtifactType.NAVIGATION_JSON,
        };
    }

    @Override
    public void transform(@NonNull TransformOutputs transformOutputs) {
        File input = getInputArtifact().get().getAsFile();
        ArtifactType targetType = getParameters().getTargetType().get();
        switch (targetType) {
            case CLASSES_JAR:
            case JAVA_RES:
            case PROCESSED_JAR:
                // even though resources are supposed to only be in the main jar of the AAR, this
                // is not necessarily enforced by all build systems generating AAR so it's safer to
                // read all jars from the manifest.
                // For shared libraries, these are provided via SHARED_CLASSES and SHARED_JAVA_RES.
                if (!isShared(input)) {
                    AarTransformUtil.getJars(input).forEach(transformOutputs::file);
                }
                break;
            case SHARED_CLASSES:
            case SHARED_JAVA_RES:
                if (isShared(input)) {
                    AarTransformUtil.getJars(input).forEach(transformOutputs::file);
                }
                break;
            case LINT:
                outputIfExists(FileUtils.join(input, FD_JARS, FN_LINT_JAR), transformOutputs);
                break;
            case MANIFEST:
                // Return both the manifest and the extra snippet for the shared library.
                outputIfExists(new File(input, FN_ANDROID_MANIFEST_XML), transformOutputs);
                if (isShared(input)) {
                    outputIfExists(
                            new File(input, FN_SHARED_LIBRARY_ANDROID_MANIFEST_XML),
                            transformOutputs);
                }
                break;
                
            //代码太长省略多个case
            
            case PREFAB_PACKAGE:
                outputIfExists(new File(input, FD_PREFAB_PACKAGE), transformOutputs);
                break;
            case AAR_METADATA:
                outputIfExists(
                        FileUtils.join(input, AarMetadataTask.AAR_METADATA_ENTRY_PATH.split("/")),
                        transformOutputs);
                break;
            case ART_PROFILE:
                outputIfExists(
                        FileUtils.join(
                                input,
                                SdkConstants.FN_ART_PROFILE),
                        transformOutputs);
                break;
            case NAVIGATION_JSON:
                outputIfExists(new File(input, FN_NAVIGATION_JSON), transformOutputs);
                break;
            default:
                throw new RuntimeException("Unsupported type in AarTransform: " + targetType);
        }
    }
}

getTransformTargets()声明了所有AAR包中可能出现的资源类型,在transform()中根据类型将aar包中的文件解压到输出目录,这就是AarTransform的作用

注册时机

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

AarTransform同样是在DependencyConfigurator中进行注册,注意它是用一个for循环进行注册,也就是为getTransformTargets()这个数组中每一种产物类型都注册一个AarTransform

当某个Task的输入配置依赖中的artifactType包含这个数组中其中一项时,都有可能执行到AarTransform

Gradle中的Configuration是什么

我们在build.gradle中写的compile,implementation,provided,compileOnly等等,都是Configuration。

承担项目或者aar依赖只是configuration表面的任务,对于项目依赖来说,它真正的作用,其实是当project A依赖project B时,它就拥有了对于project B中产物的所有权,即它能够获取到project B中的产物

什么是artifacts

artifacts的意思就是产物,有的地方也翻译成构件或工件,说的是同一个东西,这里我就称呼为产物,对于大部分task来说,执行完之后都会有产物输出,输出的就叫artifacts,同样artifacts也可以作为Task的输入。

与artifacts密切关联的有2个类:

  • ArtifactCollection

  • FileCollection

其实基本上所有的AndroidTask的输出都是文件。

/**
 * 配置的一组已解析的产物集合。当查询集合时,将按需解析配置
 */
public interface ArtifactCollection extends Iterable<ResolvedArtifactResult> {
    /**
     * 用于返回包含有所有产物的文件集合,这个方法的返回值可作为一个task的input,
     利用这个input构建起task之间的依赖关系
     */
    FileCollection getArtifactFiles();

    /**
     * 用于返回解析好的artifacts,如果没有解析,则会进行解析。在这个过程中,
     如果需要的话,可能会下载artifact的metadata和artifact文件
     */
    Set<ResolvedArtifactResult> getArtifacts();

    /**
     * 以ResolvedArtifactResult实例的Provider形式返回已解析的产物。
      返回的Provider是动态的,会跟踪此产物集合的生产者Tasks。
      这个Provider将根据需要解析产物的metadata并下载产物文件。
     */
    @Incubating
    Provider<Set<ResolvedArtifactResult>> getResolvedArtifacts();

    Collection<Throwable> getFailures();
}

可以看到几个方法都是跟获取artifacts相关的,我们先不要陷入这些api如何使用,了解它的具体功能即可

FileCollection的定义如下

public interface FileCollection extends Iterable<File>, AntBuilderAware, Buildable

这里只要关注一点,一个Buildable对象代表一个或多个artifact,这些artifacts也是由一个或多个task产生的,而FileCollection继承了Buildable接口

Task和artifacts、ArtifactCollection是密切相关的,那么讲这些有什么作用呢,它对于理解TransformAction的产物转换有很重要的作用

Task中的ArtifactView

A view over the artifacts resolved for this set of dependencies. By default, the view returns all files and artifacts, but this can be restricted by component identifier or by attributes.

这是一组解析出的依赖项的产物视图。默认情况下,该视图返回所有文件和产物,但可以通过组件标识符或属性来进行限制
之所以又介绍ArtifactView这个类,是因为Task中artifactType是由ArtifactView来控制的

public interface ArtifactView extends HasAttributes {

    /**
     * Returns the collection of artifacts matching the requested attributes that are sourced from Components matching the specified filter.
     */
    ArtifactCollection getArtifacts();

    /**
     * Returns the collection of artifact files matching the requested attributes that are sourced from Components matching the specified filter.
     */
    FileCollection getFiles();
 }

注意ArtifactView接口中2个方法的返回值,就是我们前面说过的ArtifactCollection和FileCollection

Task如何添加输入artifactType

从而调用我们的TransformAction呢,借鉴了网上大佬的代码

tasks.register("consumerTask", ConsumerTask::class.java) {
            it.artifactCollection = myConfiguration.incoming
            .artifactView {viewConfiguration ->
            //将我们自定义的或者需要依赖的artifactType加入到Task的输入产物集合
                viewConfiguration.attributes.attribute(artifactType, "java-classes-directory")
            }.artifacts
            it.outputFile.set(File("build/consumerTask/output/output.txt"))
            }

我们把artifactType="java-classes-directory"加入到ConsumerTask配置的输入依赖中,那么当我们执行ConsumerTask的时候

请求java-classes-directory类型的产物时,如果找不到,就会查找一系列能够将其他类型的产物转为java-classes-directory类型的TransformAction(例如文章开头的UnzipTransform),并按照一定的规则进行产物转换(为了偷懒和便于理解,我对官方的demo做了些修改,勿喷)

AGP中的ArtifactView使用

这是VariantDependencies.kt中的一段代码,可以看到使用方式基本相同,只不过AGP中包装了一个attributesAction

configuration
            .incoming
            .artifactView { config: ArtifactView.ViewConfiguration ->
                config.attributes(attributesAction)
                filter?.let { config.componentFilter(it) }
                // TODO somehow read the unresolved dependencies?
                config.lenient(lenientMode)
            }
            .artifacts

TransformAction调用时机

当Gradle解析配置并且配置中的依赖关系不具有带有所请求属性的变体时,Gradle会尝试查找一系列TransformAction进行转换,下面是它的转换规则

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

同时还有两个特殊情况:

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

上面是Gradle官方文档翻译过来的,不太好消化。

说一下我的理解 它的执行时机是在Gradle执行依赖项解析和转换的过程中,分为2类

  1. 配置依赖
  2. 产物转换

如果有配置缓存,则不会进行转换;如果有多个TransformAction,会按照最优的方式选择TransformAction进行转换。

TransformAction在使用依赖前才会执行,并且第一次执行后会在.gradle/caches下生成缓存,实际上是在Task执行之前执行

上面举得2个例子,JetifyTransform和AarTransform都属于产物转换,而且AGP中使用最多的也是产物转换,也就是上面特殊情况的第2条。

至于配置依赖,虽然Gradle官方举了一个例子,但是我想不到这个例子到底有什么用处,理解这个例子也费了一段时间,而且也没有碰到使用的场景,这里就不介绍了。

有兴趣的可以点击官方链接Transforming artifacts

对比

回到主题,AGP(Android Gradle Plugin)中的Transform是一个用于在Android构建过程中进行字节码操作的API

  1. 提供者是AGP
  2. 作用是操作字节码
  3. 以task的形式执行

TransformAction是Gradle提供的依赖产物转换的API

  1. 提供者是Gradle
  2. 作用是依赖产物转换
  3. 在Task执行之前执行

Artifact transform 做的主要是依赖产物的转换,不同于AGP对中间产物局部的Transform, Gradle全局的产物转换可以保证同步后可以使用到转换过的产物, 可以在一次 transform过程中同时处理class, layout, manifest中的support依赖,例如上面的JetifyTransform,这在AGP的Transform中是无法实现的

TransformAction的注册API

是在project的dependencies模块进行注册,这也从侧面反应了TransformAction是为工程的依赖而服务的

project.dependencies.registerTransform(
            transformClass
        ) { spec: TransformSpec<T> ->
            spec.from.attribute(ArtifactAttributes.ARTIFACT_FORMAT, fromArtifactType)
            spec.to.attribute(ArtifactAttributes.ARTIFACT_FORMAT, toArtifactType)
            spec.parameters.projectName.setDisallowChanges(project.name)
            parametersSetter?.let { it(spec.parameters) }
        }

Google官方说明

Google已经明确说明Transform API 没有单一的替代 API,每个case都会有新的针对性 API。所有替代 API 都位于 androidComponents {} 代码块中

image.png

总结

TransformAction和Transform两者的执行时机以及作用,是大不相同的,而且官方也明确表明了Transform替代api的代码块,所以transformAction并不是Transform的替代品。

当然TransformAction还有一些细节内容没有介绍,比如支持增量编译、输出写入规则等等

TransformAction目前主要应用在AGP中,实际开发中我还没有找到适合使用的场景

以上观点如有错漏,欢迎批评指正,另外对于TransformAction的实际开发中的使用场景也欢迎留言交流

参考文章

本文借鉴了以下大佬的文章,非常感谢

哔哩哔哩 Android 同步优化•Jetifier
傻傻分不清楚:Gradle TransformAction和AGP Transform Transform 被废弃,TransformAction 了解一下~
连载 | 深入理解gradle框架之三:artifacts的发布