Android Gradle Plugin打包Apk过程中的Transform API

本文以com.android.tools.build:gradle:3.1.2 源码来进行分析,文章对应的项目地址:aop-tech

Transform API 是在1.5.0-beta1版开始使用,利用Transform API,第三方的插件可以在.class文件转为dex文件之前,对一些.class 文件进行处理。Transform API 简化了这个处理过程,而且使用起来很灵活。

使用Transform API

使用Transform API主要是写一个类继承Transform,并把该Transform注入到打包过程中。
注入Transform很简单,先获取com.android.build.gradle.AppExtension对象,然后调用它的registerTransform()方法。
这个方法实际上是属于BaseExtension的,AppExtension继承自BaseExtension。

#com.android.build.gradle.BaseExtension
public void registerTransform(@NonNull Transform transform, Object... dependencies) {
    transforms.add(transform);
    transformDependencies.add(Arrays.asList(dependencies));
}

注入Transform对象:

AppExtension android = project.extensions.getByType(AppExtension)
android.registerTransform(new AJXTransform(project))

AJXTransform是自定义的Transform类:

lass AJXTransform extends Transform {
    Project project
    AJXTransform(Project project) {
        this.project = project
    }

    @Override
    String getName() {
        return "ajx"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        // 输入类型,可以使class文件,也可以是源码文件 ,这是表示输入的class文件
        return ImmutableSet.<QualifiedContent.ContentType> of(QualifiedContent.DefaultContentType.CLASSES)
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        // 作用范围
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        //是否支持增量编译
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        //在这里对输入输出的class进行处理
    }
}

ContentType是一个接口,默认有一个枚举类型DefaultContentType实现了ContentType,包含有CLASSES和RESOURCES类型。

  • CLASSES类型表示的是在jar包或者文件夹中的.class文件。
  • RESOURCES类型表示的是标准的Java源文件。

Scope 作用范围

Scope类型 说明
PROJECT 只处理当前的项目
SUB_PROJECTS 只处理子项目
EXTERNAL_LIBRARIES 只处理外部的依赖库
TESTED_CODE 只处理测试代码
PROVIDED_ONLY 只处理provided-only的依赖库
PROJECT_LOCAL_DEPS 只处理当前项目的本地依赖,例如jar, aar(过期,被EXTERNAL_LIBRARIES替代)
SUB_PROJECTS_LOCAL_DEPS 只处理子项目的本地依赖,例如jar, aar(过期,被EXTERNAL_LIBRARIES替代)

Transform中的getInputTypes()方法和getScopes() 方法返回的是Set集合,因此这些类型是可以进行组合的。在com.android.build.gradle.internal.pipeline.TransformManager中就包含了多种Set集合。

TransformManager.png

Transform 的 isIncremental() 方法表示是否支持增量编译,返回true的话表示支持,这个时候可以根据 com.android.build.api.transform.TransformInput 来获得更改、移除或者添加的文件目录或者jar包。

public interface TransformInput {

    /**
     * Returns a collection of {@link JarInput}.
     */
    @NonNull
    Collection<JarInput> getJarInputs();

    /**
     * Returns a collection of {@link DirectoryInput}.
     */
    @NonNull
    Collection<DirectoryInput> getDirectoryInputs();
}

JarInput有一个方法是getStatus()来获取 com.android.build.api.transform.Status。Status是一个枚举类,包含了NOTCHANGED、ADDED、CHANGED、REMOVED,所以可以根据JarInput的status来对它进行相应的处理,比如添加或者移除。

DirectoryInput有一个方法getChangedFiles()开获取一个Map<File, Status>集合,所以可以遍历这个Map集合,然后根据File对应的Status来对File进行处理。

如果不支持增量编译,就在处理.class之前把之前的输出目录中的文件删除。

获取TransformInput对象是根据 com.android.build.api.transform.TransformInvocation

public interface TransformInvocation {
// 返回transform运行的上下文,在android gradle plugin中有唯一的实现类TransformTask
    @NonNull
    Context getContext();

// 获取transform的输入
    @NonNull
    Collection<TransformInput> getInputs();

    /**
     * Returns the referenced-only inputs which are not consumed by this transformation.
     * @return the referenced-only inputs.
     */
    @NonNull Collection<TransformInput> getReferencedInputs();
    /**
     * Returns the list of secondary file changes since last. Only secondary files that this
     * transform can handle incrementally will be part of this change set.
     * @return the list of changes impacting a {@link SecondaryInput}
     */
    @NonNull Collection<SecondaryInput> getSecondaryInputs();

//TransformOutputProvider用于删除输出目录或者创建文件对应的生成目录
    @Nullable
    TransformOutputProvider getOutputProvider();

// transform过程是否支持增量编译
    boolean isIncremental();
}

TransformInvocation包含了输入、输出相关信息。其输出相关内容是由TransformOutputProvider来做处理。TransformOutputProvider的getContentLocation()方法可以获取文件的输出目录,如果目录存在的话直接返回,如果不存在就会重新创建一个。例如:

// getContentLocation方法相当于创建一个对应名称表示的目录
// 是从0 、1、2开始递增。如果是目录,名称就是对应的数字,如果是jar包就类似0.jar
File outputDir = transformInvocation.outputProvider.getContentLocation("include", 
         dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)

File outputJar = transformInvocation.outputProvider.getContentLocation(jarInput.name
        , jarInput.contentTypes
        , jarInput.scopes
        , Format.JAR)

在执行编译过程中会生成对应的目录,例如在/app/build/intermediates/transforms目录下生成了一个名为ajx的目录,这个名称就是根据自定义的Transform类getName()方法返回的字符串来的。

transforms下的ajx目录.png

transforms下的ajx中内容.png

ajx目录下还会有一个名为__content__的.json文件。该文件中展示了ajx中文件目录下的内容

image.png

其实当你注入一个自定义的Transform的时候还会生成对应的Task,即TransformTask,该Task还会有一个对应的名称,例如:

TransformTask名称.png

这个名称生成过程是在 TransformManager 的 addTransform() 中:

public <T extends Transform> Optional<TransformTask> addTransform(
            @NonNull TaskFactory taskFactory,
            @NonNull TransformVariantScope scope,
            @NonNull T transform,
            @Nullable TransformTask.ConfigActionCallback<T> callback) {
...
        String taskName = scope.getTaskName(getTaskNamePrefix(transform));
...
}

static String getTaskNamePrefix(@NonNull Transform transform) {
    StringBuilder sb = new StringBuilder(100);
    sb.append("transform");

    sb.append(
            transform
                    .getInputTypes()
                    .stream()
                    .map(
                            inputType ->
                                    CaseFormat.UPPER_UNDERSCORE.to(
                                            CaseFormat.UPPER_CAMEL, inputType.name()))
                    .sorted() // Keep the order stable.
                    .collect(Collectors.joining("And")));
    sb.append("With");
    StringHelper.appendCapitalized(sb, transform.getName());
    sb.append("For");

    return sb.toString();
}

在Transform中,其transform() 方法是重头戏,需要对输入的文件进行处理,然后放到输出目录中。

例如,我在aop-tech中就是自定义一个Transform子类,然后在transform过程把输入的目录中的.class文件或者jar包中的.class 文件使用aspectj进行处理。如果对aspectj不太了解的,可以查看我之前写的一篇文章:AOP开发——AspectJ的使用

@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    TransformTask transformTask = (TransformTask) transformInvocation.context
    //VariantCache 就是保存一些跟当前variant相关的一些缓存,以及在支持增量编译的情况下存储一些信息
    VariantCache variantCache = new VariantCache(ajxProcedure.project, ajxProcedure.ajxCache, transformTask.variantName)

    if (transformInvocation.isIncremental()) {
        //TODO 增量
        print("====================增量编译=================")
    }else {
        print("====================非增量编译=================")
        //非增量,需要删除输出目录
        transformInvocation.outputProvider.deleteAll()
        variantCache.reset()

        AJXFileProcess ajxFileProcess = new AJXFileProcess(project, variantCache, transformInvocation)
        ajxFileProcess.proceed()
        AJXTaskProcess ajxTaskProcess = new AJXTaskProcess(project, variantCache, transformInvocation)
        ajxTaskProcess.proceed()
    }
}

Transform中的transform()方法是如何被执行的

我们在前面讲到的调用android.registerTransform(transform)注册方法,实际上只是把Transform对象放到了一个List集合中。那么什么时候用到这个集合呢?也就是说Transform的transform()方法是什么时候被执行的呢?

在这里可以先告诉你答案,Transform的transform()方法是在TransformTask的transform()方法中执行的。

# com.android.build.gradle.internal.pipeline.TransformTask
@TaskAction
void transform(final IncrementalTaskInputs incrementalTaskInputs)
        throws IOException, TransformException, InterruptedException {
    ...
    recorder.record(
            ExecutionType.TASK_TRANSFORM,
            executionInfo,
            getProject().getPath(),
            getVariantName(),
            new Recorder.Block<Void>() {
                @Override
                public Void call() throws Exception {

                    transform.transform(
                            new TransformInvocationBuilder(TransformTask.this)
                                    .addInputs(consumedInputs.getValue())
                                    .addReferencedInputs(referencedInputs.getValue())
                                    .addSecondaryInputs(changedSecondaryInputs.getValue())
                                    .addOutputProvider(
                                            outputStream != null
                                                    ? outputStream.asOutput()
                                                    : null)
                                    .setIncrementalMode(isIncremental.getValue())
                                    .build());

                    if (outputStream != null) {
                        outputStream.save();
                    }
                    return null;
                }
            });
}

实际上每个Transform都会有一个对应的TransformTask,TransformTask本质上就是表示Gradle中的一个Task,那么一个Task在执行的时候其@TaskAction注解的方法会被执行,也就是com.android.build.gradle.internal.pipeline.TransformTask#transform()方法会被执行,在该方法中会调用该TransformTask对应的Transform对象的transform()方法。

写过Gradle插件的都知道,在build.gradle中apply 插件后其apply(project)方法就会调用。例如我们在一个app中应用的是apply plugin: 'com.andorid.application' ,这个实际上引入的就是AppPlugin,其apply()方法会被调用。关于这个源码过程我们就不多说了,我们只讲一个这个过程,感兴趣的可以查看文章末尾的相关文章。

->com.android.build.gradle.BasePlugin#apply()
->com.android.build.gradle.BasePlugin#createTasks()
->com.android.build.gradle.BasePlugin#createAndroidTasks()
->com.android.build.gradle.internal.VariantManager#createAndroidTasks()
->com.android.build.gradle.internal.VariantManager#createTasksForVariantData()
->com.android.build.gradle.internal.ApplicationTaskManager#createTasksForVariantScope() //该方法中干的事很多,可以重点关注
->com.android.build.gradle.internal.ApplicationTaskManager#addCompileTask()
->com.android.build.gradle.internal.TaskManager#createPostCompilationTasks() //在这个方法中会调用AppExtension的getTransforms()方法,也就是我们之前注册的transform
-> com.android.build.gradle.internal.pipeline.TransformManager#addTransform() //在这个方法中创建了TransformTask

在上面的这些流程中TaskManager的createPostCompilationTasks()需要重点关注,在该方法中对Apk打包过程中的各种Transform进行处理,创建对应了TransformTask并构建隐式的依赖关系。

到此TransformTask是创建完毕,TransformTask相当于是对我们自定义的Transform进行的包装。

那么这个TransformTask是什么时候执行呢?
我们可以显式地使用Gradle命令去执行一个Task,这是没问题的。但为什么我们执行gradle assemble命令的时候,TransformTask也会执行呢?这个就牵涉到Task的依赖关系了。假设TaskA依赖TaskB,那么如果我们要执行TaskA,那么在TaskA执行之前TaskB就会执行。但是要注意一点就是,假设TaskA依赖TaskB和TaskC,那么只能保证TaskA执行之前TaskB和TaskC都执行了,并不能保证TaskB和TaskC的执行顺序。

构建Task的依赖关系可以显式的调用其 dependsOn() 方法。 但是我在查看Transform API的过程中发现这个TransformTask之间并没有显式地调用dependsOn() 方法来保证依赖关系,难道这个TransformTask的执行顺序是任意的吗?如果是任意的,比如一个task是dexMerger是要把每个class编译成的dex合成为一个dex,而此时输入目录中还没有dex存在,该任务不就失败了吗?android gradle plugin 的开发者当然不会让这种情况存在了。

实际情况是Task除了显式地通过dependsOn来指定Task依赖,其实还可以使用Task依赖推断来判断依赖关系,Gradle通过使用一个Task的输出作为另一个Task的输入,就可以推断出依赖关系。
在Transform API中,使用的是 TransformStream 来连接TransformTask的依赖关系,进行可以控制Transform的执行顺序。
计算TransformTask的输入输出是在 TransformManager 的 addTransform() 方法中。
通过输入输出已经隐式地确定了TransformTask的依赖关系。

# com.android.build.gradle.internal.pipeline.TransformManager
public <T extends Transform> Optional<TransformTask> addTransform(
        @NonNull TaskFactory taskFactory,
        @NonNull TransformVariantScope scope,
        @NonNull T transform,
        @Nullable TransformTask.ConfigActionCallback<T> callback) {
...
    List<TransformStream> inputStreams = Lists.newArrayList();
    String taskName = scope.getTaskName(getTaskNamePrefix(transform));

    // get referenced-only streams
    List<TransformStream> referencedStreams = grabReferencedStreams(transform);

    // find input streams, and compute output streams for the transform.
    // 通过之前添加的Transform来计算输入,并计算输出
    IntermediateStream outputStream = findTransformStreams(
            transform,
            scope,
            inputStreams,
            taskName,
            scope.getGlobalScope().getBuildDir());
   ...
    transforms.add(transform);

    // create the task...
    // 在创建Task的过程中传入了输入和输出,上一个Task的输出是该Task的输入,这就保证了Task的一个隐式的依赖关系
    TransformTask task =
            taskFactory.create(
                    new TransformTask.ConfigAction<>(
                            scope.getFullVariantName(),
                            taskName,
                            transform,
                            inputStreams,
                            referencedStreams,
                            outputStream,
                            recorder,
                            callback));

    return Optional.ofNullable(task);
}

所以通过以上分析也可以当我们自定义Plugin要注入多个Transform的时候,按照添加顺序来保证依赖关系,先添加的Transform先执行。对于下面的例子,transformA的transform()方法会先于transformB执行。

android.registerTransform(transformA);
android.registerTransform(transformB);

Android Gradle Plugin中的Transform子类

Android Gradle Plugin中有多个Transform子类,在编写自己的Transform类的时候可以作为一个参考。


Transform子类.png

可以在图中看到,Google已经在使用kotlin写Gradle插件了。

总结

自定义Transform并注入:

class TransformPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.android.registerTransform(new ClassTransform(project))
    }
}

class ClassTransform extends Transform {

Transform API的执行有顺序,每一个Transform都对应一个TransformTask和TransformStream,通过输入输出隐式的构成Task依赖关系。开发者自定义的Transform按照注册顺序执行。

补充——源码调试

在之前的一篇文章启用Gradle远程调试中介绍了如何调试Gradle插件,实际上使用该方式也可以用来调试查看Android Gradle Plugin的执行流程。这个时候最好要找到有能下载到源码的版本,比如3.1.2。

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
 ...
        classpath 'com.android.tools.build:gradle:3.1.2'
...
    }
}

以下是App或者Library插件对应的类的继承关系图


如果你想要调试app打包过程,可以在 AppPlugin 的 apply() 方法中打断点。它直接调用了其父类 BasePlugin 的 apply() 方法。

#com.android.build.gradle.AppPlugin
@Override
public void apply(@NonNull Project project) {
    super.apply(project);
}

相关文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容