文章

02.AGP之Transform

02.AGP之Transform

Android Gradle Plugin 之 Transform

什么是 Transform?

Android Gradle 工具从 1.5.0-beta1 版本开始提供了 Transform API 工具,它可以在将 .class 文件转换为 .dex 文件之前对其进行操作。可以通过自定义 Gradle 插件来注册自定义的 Transform,注册后 Transform 会包装成一个 Gradle Task 任务,这个 Task 在 compile task 执行完毕后运行。

要实现 transform 需要继承 com.android.build.api.transform.Transform 并实现其方法,实现了 Transform 以后,要想应用,就调用 project.android.registerTransform()

每个 Transform 其实都是一个 Gradle Task,Android 编译器中的 TaskManager 将每个 Transform 串连起来,第一个 Transform 接收来自 Javac 编译的结果,以及已经拉取到在本地的第三方依赖(jaraar),还有 resource 资源,注意,这里的 resource 并非 android 项目中的 res 资源,而是 asset 目录下的资源。
这些编译的中间产物,在 Transform 组成的链条上流动,每个 Transform 节点可以对 class 进行处理再传递给下一个 Transform。我们常见的混淆,Desugar 等逻辑,它们的实现如今都是封装在一个个 Transform 中,而我们自定义的 Transform,会插入到这个 Transform 链条的最前面。
我们定义的 Transform 会被转化成一个个 TransformTask,在 Gradle 编译时调用。

Transform 基础

Transform 执行流程

transform 工作在图中红色箭头处拦截(生成 class 文件之后,dex 文件之前):
t0ggz
transform 工作流程:
wue1j

Transform API

TransformInput

TransformInput 可认为是所有输入文件的一个抽象,它主要包括两个部分:

  • DirectoryInput 集合
    是指以源码的方式参与项目编译的所有目录结构及其目录下的源码文件
  • JarInput 集合
    表示以 jar 包方式参与项目编译的所有本地 jar 包和远程 jar 包。需要注意的是,这个 jar 所指也包括 aar。
1
2
3
4
5
6
7
8
9
public interface TransformInput {
    // 表示 Jar 包
    @NonNull
    Collection<JarInput> getJarInputs();

    // 表示目录,包含 class 文件(如果一个Transform不想处理任何输入,只是想查看输入的内容,调用这个)
    @NonNull
    Collection<DirectoryInput> getDirectoryInputs();
}

TransformOutputProvider

Transform 的输出,通过它可以获取到输出路径等信息

1
2
3
4
5
6
7
8
9
10
11
12
public interface TransformOutputProvider {

    void deleteAll() throws IOException;

    // 根据 name、ContentType、QualifiedContent.Scope 返回对应的文件(jar / directory)
    @NonNull
    File getContentLocation(
            @NonNull String name,
            @NonNull Set<QualifiedContent.ContentType> types,
            @NonNull Set<? super QualifiedContent.Scope> scopes,
            @NonNull Format format);
}

Transform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public abstract class Transform {
    public Transform() {
    }

    // Transform名称
    public abstract String getName();

    public abstract Set<ContentType> getInputTypes();

    public Set<ContentType> getOutputTypes() {
        return this.getInputTypes();
    }

    public abstract Set<? super Scope> getScopes();

    public abstract boolean isIncremental();

    @Deprecated
    public void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
    }

    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        this.transform(transformInvocation.getContext(), transformInvocation.getInputs(), transformInvocation.getReferencedInputs(), transformInvocation.getOutputProvider(), transformInvocation.isIncremental());
    }

    public boolean isCacheable() {
        return false;
    }
    // ...
}
getName() transform 名称

transform 名称的构成, transformClasses[AndResources]With[getName()]For[BuildType],它会出现在 app/build/intermediates/transforms 目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();
}

如:

1
:app:transformClassesAndResourcesWithMyTransformNameForDebug
getInputTypes() 处理的数据类型

getInputTypes 方法用于确定我们需要对哪些类型的结果进行转换:如字节码、资源⽂件等等
DefaultContentType 有 2 种类型:CLASSES 和 RESOURCES

1
2
3
4
5
6
7
8
interface ContentType {
    String name();
    int getValue();
}
enum DefaultContentType implements ContentType {
    CLASSES(0x01), // Javac编译后的class
    RESOURCES(0x02); // Java的标准resource
}

还有其他的 ContentType 类型:

1
2
3
4
5
6
7
8
9
10
11
public enum ExtendedContentType implements ContentType {
    /**
     * The content is dex files.
     */
    DEX(0x1000), // dex文件

    /**
     * Content is a native library.
     */
    NATIVE_LIBS(0x2000), // native库
}

TransformManager 定义了有 6 种枚举类型:

1
2
3
4
5
6
7
// TransformManager
1CONTENT_CLASS表示需要处理 java  class 文件
2CONTENT_JARS表示需要处理 java  class  资源文件
3CONTENT_RESOURCES表示需要处理 java 的资源文件
4CONTENT_NATIVE_LIBS表示需要处理 native 库的代码
5CONTENT_DEX表示需要处理 DEX 文件
6CONTENT_DEX_WITH_RESOURCES表示需要处理 DEX  java 的资源文件

其中,很多类型是不允许自定义 Transform 来处理的,我们常使用 CONTENT_CLASS 来操作 Class 文件。

getScopes() 指定插件的适用范围

getScopes 方法则是用于确定插件的适用范围:目前 Scope 有 五种基本类型,如下所示:

  1. PROJECT:只有项目内容。
  2. SUB_PROJECTS:只有子项目。
  3. EXTERNAL_LIBRARIES:只有外部库,
  4. TESTED_CODE:由当前变体(包括依赖项)所测试的代码。
  5. PROVIDED_ONLY:只提供本地或远程依赖项。

还有一些复合类型,它们是都是由这五种基本类型组成,以实现灵活确定自定义插件的范围,这里通常是指定整个 project,也可以指定其它范围:

在 TransformManager 类中定义了几种范围:

1
2
3
4
5
6
1 PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT);
2 SCOPE_FULL_PROJECT = ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES); // 代表所有Project(当前项目、子项目以及外部的依赖库)
3 SCOPE_FULL_WITH_FEATURES = new ImmutableSet.Builder<ScopeType>().addAll(SCOPE_FULL_PROJECT).add(InternalScope.FEATURES).build();
4SCOPE_FEATURES = ImmutableSet.of(InternalScope.FEATURES);
5SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS = ImmutableSet.of(Scope.PROJECT, InternalScope.LOCAL_DEPS);
6SCOPE_FULL_PROJECT_WITH_LOCAL_JARS = new ImmutableSet.Builder<ScopeType>().addAll(SCOPE_FULL_PROJECT).add(InternalScope.LOCAL_DEPS).build();

要处理所有的 class 字节码,返回 TransformManager.SCOPE_FULL_PROJECT

isIncremental() 表示是否支持增量更新

isIncremental 方法用于确定是否支持增量更新,如果返回 true,TransformInput 会包含一份修改的文件列表,如果返回 false,则会进行全量编译,并且会删除上一次的输出内容。

当我们开启增量编译的时候,相当 input 包含了 changed/removed/added 三种状态,实际上还有 notchanged。需要做的操作如下:

  1. NOTCHANGED: 当前文件不需处理,甚至复制操作都不用;
  2. ADDED、CHANGED: 正常处理,输出给下一个任务;
  3. REMOVED: 移除 outputProvider 获取路径对应的文件。

有时即使返回 true, 在某些情况下它还是会当作 false 返回。

transform() 进行具体的转换过程

在 transform 方法中,就是用来给我们进行具体的转换过程的。input 的内容将会打包成一个 TransformInvocation 对象

  1. 如果拿取了 getInputs() 的输入进行消费,则 transform 后必须再输出给下一级
  2. 如果拿取了 getReferencedInputs() 的输入,则不应该被 transform
  3. 是否增量编译要以 transformInvocation.isIncremental() 为准
isCacheable()

如果我们的 transform 需要被缓存,则为 true,它被 TransformTask 所用到

TransformInvocation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface TransformInvocation {

    // 上下文
    @NonNull
    Context getContext();

    // 输入作为 TransformInput 返回
    @NonNull
    Collection<TransformInput> getInputs();

     // 返回不被这个 transformation 消费的 input
    @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.
     */
    @NonNull Collection<SecondaryInput> getSecondaryInputs();

    // 返回允许创建内容的 output provider(可以用来创建输出内容)
    @Nullable
    TransformOutputProvider getOutputProvider();

    boolean isIncremental();
}

Transform 的增量编译与并发

增量编译定义

编译过程中会去遍历所有的 jar .class 文件,然后对文件进行 io 操作,以及 asm 插入代码,这个过程耗时一般都会很长。
需要注意一点:不是每次的编译都是可以怎量编译的,毕竟一次 clean build 完全没有增量的基础,所以,我们需要检查当前的编译是否增量编译。
需要做区分:

  • 不是增量编译,则清空 output 目录,然后按照前面的方式,逐个 class/jar 处理
  • 增量编译,则要检查每个文件的 Status,Status 分为四种,并且对四种文件的操作不尽相同

NOTCHANGED 当前文件不需要处理,甚至复制操作都不用
ADDED、CHANGED 正常处理,输出给下一个任务
REMOVED 移除 outputProvider 获取路径对应的文件
上述是对增量的一些定义,可以看出来在 transform 过程中,应该是对文件打了一些 tag 标签。
那么我们在开发阶段首先要先区分当前这次是不是增量编译,然后再编译当前变更的文件,对变更的文件进行处理。

增量编译代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
 public void startTransform() {
    try {
        if (!isIncremental) {
            outputProvider.deleteAll();
        }
        for (TransformInput input : inputs) {
            for (JarInput jarInput : input.getJarInputs()) {
                Status status = jarInput.getStatus();
                String destName = jarInput.getFile().getName();
                /* 重名名输出文件,因为可能同名,会覆盖*/
                String hexName = DigestUtils.md5Hex(jarInput.getFile().getAbsolutePath()).substring(0, 8);
                if (destName.endsWith(".jar")) {
                    destName = destName.substring(0, destName.length() - 4);
                }
                /*获得输出文件*/
                File dest = outputProvider.getContentLocation(destName + "_" + hexName,
                        jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
                if (isIncremental) {
                    switch (status) {
                        case NOTCHANGED:
                            break;
                        case ADDED:
                            foreachJar(dest, jarInput);
                            break;
                        case CHANGED:
                            diffJar(dest, jarInput);
                            break;
                        case REMOVED:
                            try {
                                deleteScan(dest);
                                if (dest.exists()) {
                                    FileUtils.forceDelete(dest);
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                    }
                } else {
                    foreachJar(dest, jarInput);
                }
            }
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                foreachClass(directoryInput);
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

遍历循环 jar,开始的时候我们先判断当前这次是不是增量编译,如果不是增量则开始遍历所有 jar,如果是增量编译,会去获取当前 jar 的状态,如果状态是删除则先扫描 jar 之后把 output 中的文件删除。如果状态是 ADD 的情况下,则扫描修改这个 jar 文件。最后如果是 CHANGE 状态,则先扫描新久两个 jar,比较获取删除的文件,然后重复 ADD 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void foreachClass(DirectoryInput directoryInput) throws IOException {
    File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(),
            directoryInput.getScopes(), Format.DIRECTORY);
    Map<File, Status> map = directoryInput.getChangedFiles();
    File dir = directoryInput.getFile();
    if (isIncremental) {
        for (Map.Entry<File, Status> entry : map.entrySet()) {
            Status status = entry.getValue();
            File file = entry.getKey();
            String destFilePath = file.getAbsolutePath().replace(dir.getAbsolutePath(), dest.getAbsolutePath());
            File destFile = new File(destFilePath);
            switch (status) {
                case NOTCHANGED:
                    break;
                case ADDED:
                case CHANGED:
                    try {
                        FileUtils.touch(destFile);
                    } catch (Exception ignored) {
                        Files.createParentDirs(destFile);
                    }
                    modifySingleFile(dir, file, dest);
                    break;
                case REMOVED:
                    Log.info(entry);
                    deleteDirectory(destFile, dest);
                    break;
            }
        }
    } else {
        changeFile(dir, dest);
    }
}

修改.class 文件的操作 , 和修改 jar 包的逻辑基本一样,但是又一个区别,如果是增量编译的情况下,我们获取的对象是一个 Map,而非增量编译的情况下,我们使用的是整个文件夹路径。

实现 Transform 步骤

1、首先,配置 Android DSL 相关的依赖

1
2
3
4
5
6
7
8
9
// 由于 buildSrc 的执行时机要早于任何一个 project,因此需要⾃⼰添加仓库 
repositories {
    google()
    jcenter() 
}
dependencies {
    // Android DSL
    implementation 'com.android.tools.build:gradle:3.6.2'
}

2、然后,继承 com.android.build.api.transform.Transform ,创建⼀个 Transform 的子类

其创建步骤可以细分为五步,如下所示:

  1. 重写 getName 方法:返回对应的 Task 名称。
  2. 重写 getInputTypes 方法:确定对那些类型的结果进行转换。
  3. 重写 getScopes 方法:指定插件的适用范围。
  4. 重写 isIncremental 方法:表示是否支持增量更新。
  5. 重写 transform 方法:进行具体的转换过程。

3、注册 Transform

在自定义插件中注册它,然后在 build.gradle 中 apply 就可以了。

1
2
3
4
5
6
7
8
// MyCustomPlgin.groovy
public class MyCustomPlgin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        project.getExtensions().findByType(BaseExtension.class)
                .registerTransform(new MyCustomTransform());
    }
}

如果你包含了你编写的 transform 库,我们也可以直接在 build.gradle 中注册:

1
2
// 在build.gradle中也是可以直接编写 groovy代码的。
project.extensions.findByType(BaseExtension.class).registerTransform(new MyCustomTransform());

Transform 模板代码

简易 Transform 模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class AspectJTransform extends Transform {

    final String NAME =  "JokerwanTransform"

    @Override
    String getName() {
        return NAME
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @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 {
        super.transform(transformInvocation)

        // OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        transformInvocation.inputs.each { TransformInput input ->
            input.jarInputs.each { JarInput jarInput ->
                // 处理Jar
                processJarInput(jarInput, outputProvider)
            }

            input.directoryInputs.each { DirectoryInput directoryInput ->
                // 处理源码文件
                processDirectoryInputs(directoryInput, outputProvider)
            }
        }
    }

    void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
        File dest = outputProvider.getContentLocation(
                jarInput.getFile().getAbsolutePath(),
                jarInput.getContentTypes(),
                jarInput.getScopes(),
                Format.JAR)
                
        // to do some transform
        
        // 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了        
        FileUtils.copyFiley(jarInput.getFile(), dest)
    }

    void processDirectoryInputs(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        File dest = outputProvider.getContentLocation(directoryInput.getName(),
                directoryInput.getContentTypes(), directoryInput.getScopes(),
                Format.DIRECTORY)
        // 建立文件夹        
        FileUtils.forceMkdir(dest)
        
        // to do some transform
        
        // 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了        
        FileUtils.copyDirectory(directoryInput.getFile(), dest)
    }
}

Transform + ASM 模板代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
package me.hacket.transform

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import groovy.io.FileType
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.IOUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes

import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry

class MyTransform extends Transform {

    /**
     * 每一个Transform都有一个与之对应的Transform task,
     * 这里便是返回的task name。它会出现在app/build/intermediates/transforms目录下
     *
     * @return TransformName
     */
    @Override
    String getName() {
        return "MyCustomTransform"
    }

    /**
     * 需要处理的数据类型,目前ContentType有六种枚举类型,通常我们使用比较频繁的有前两种:
     * 1、CONTENT_CLASS:表示需要处理java的class文件。
     * 2、CONTENT_JARS:表示需要处理java的class与资源文件。
     * 3、CONTENT_RESOURCES:表示需要处理java的资源文件。
     * 4、CONTENT_NATIVE_LIBS:表示需要处理native库的代码。
     * 5、CONTENT_DEX:表示需要处理DEX文件。
     * 6、CONTENT_DEX_WITH_RESOURCES:表示需要处理DEX与java的资源文件。
     *
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        // 用于确定我们需要对哪些类型的结果进行转换:如字节码、资源⽂件等等。
        // return TransformManager.RESOURCES
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 表示Transform要操作的内容范围,目前Scope有五种基本类型:
     * 1、PROJECT 只有项目内容
     * 2、SUB_PROJECTS 只有子项目
     * 3、EXTERNAL_LIBRARIES 只有外部库
     * 4、TESTED_CODE 由当前变体(包括依赖项)所测试的代码
     * 5、PROVIDED_ONLY 只提供本地或远程依赖项
     * SCOPE_FULL_PROJECT是一个Scope集合,包含Scope.PROJECT,Scope.SUB_PROJECTS,Scope.EXTERNAL_LIBRARIES这三项,即当前Transform的作用域包括当前项目、子项目以及外部的依赖库
     *
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        // 适用范围:通常是指定整个project,也可以指定其它范围
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        // 是否支持增量更新
        // 如果返回true,TransformInput会包含一份修改的文件列表
        // 如果返回false,会进行全量编译,删除上一次的输出内容
        return false
    }

    /**
     * 进行具体的转换过程
     *
     * @param ti
     */
    @Override
    void transform(TransformInvocation ti) throws TransformException, InterruptedException, IOException {
        super.transform(ti)
        println '[MyTransform]---------------MyTransform visit start---------------'
        def startTime = System.currentTimeMillis()
        def inputs = ti.inputs
        def outputProvider = ti.outputProvider
        // 删除之前的输出
        if (!ti.isIncremental() && outputProvider != null) {
            outputProvider.deleteAll()
        }

        // Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历
        inputs.each { TransformInput input ->
            // 遍历directoryInputs(本地project编译成的多个class⽂件存放的目录)
            input.directoryInputs.each { DirectoryInput directoryInput ->
                handleDirectory(directoryInput, outputProvider)
            }

            // 遍历jarInputs(各个依赖所编译成的jar文件)
            input.jarInputs.each { JarInput jarInput ->
                handleJar(jarInput, outputProvider)
            }
        }

        def cost = (System.currentTimeMillis() - startTime) / 1000
        println '[MyTransform]---------------MyTransform visit end!---------------'
        println "[MyTransform] cost:$cost s"
    }

    private static void handleJar(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
            // 截取文件路径的md5值重命名输出文件,避免同名导致覆盖的情况出现
            def jarName = jarInput.name
            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }
            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
            //避免上次的缓存被重复插入
            if (tmpFile.exists()) {
                tmpFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(jarEntry)
                println "[MyTransform]handleJar, entryName=$entryName, checkClassFile=${checkClassFile(entryName)}"
                if (checkClassFile(entryName)) {
                    // 使用ASM对class文件进行操控
                    println '-----------deal with "jar" classfile<' + entryName + '>-----------'
                    jarOutputStream.putNextEntry(zipEntry)

                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))

                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)

                    ClassVisitor cv = new MyCustomClassVisitor(Opcodes.ASM5, classWriter)
                    classReader.accept(cv, ClassReader.EXPAND_FRAMES)

                    byte[] code = classWriter.toByteArray()
                    jarOutputStream.write(code)
                } else {
                    jarOutputStream.putNextEntry(zipEntry)
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            jarOutputStream.close()
            jarFile.close()

            // 生成输出路径dest:./app/build/intermediates/transforms/xxxTransform/...
            def dest = outputProvider.getContentLocation(jarName + md5Name,
                    jarInput.contentTypes, jarInput.scopes, Format.JAR)
            // println "[MyTransform] handleJar=${jarInput.file.getAbsolutePath()}, jarName=$jarName, tmpFile=$tmpFile,dest=$dest"
            // 将input的目录复制到output指定目录
            FileUtils.copyFile(tmpFile, dest)
            tmpFile.delete()
        }
    }

    private static void handleDirectory(DirectoryInput input, TransformOutputProvider to) {
        // 在增量模式下可以通过directoryInput.changedFiles方法获取修改的文件  directoryInput.changedFiles
        if (input.file.size() == 0) {
            return
        }
        if (input.file.isDirectory()) {
            /*遍历以某一扩展名结尾的文件*/
            input.file.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File classFile ->
                def name = classFile.name
                println "[MyTransform]handleDirectory, classFile=$classFile, checkClassFile=${checkClassFile(name)}, ${name.startsWith("R\$")},${"R.class" != name}, ${"BuildConfig.class" != name}"
                if (checkClassFile(name)) {
                    println '[MyTransform]-----------deal with "class" file<' + name + '>-----------'

                    def classReader = new ClassReader(classFile.bytes)

                    def classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)

                    def classVisitor = new MyCustomClassVisitor(Opcodes.ASM5, classWriter)

                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

                    byte[] codeBytes = classWriter.toByteArray()

                    FileOutputStream fileOutputStream = new FileOutputStream(classFile.parentFile.absolutePath + File.separator + name)
                    fileOutputStream.write(codeBytes)
                    fileOutputStream.close()
                }
            }
        }
        // 获取output目录dest:./app/build/intermediates/transforms/hencoderTransform/
        def destFile = to.getContentLocation(
                input.name,
                input.contentTypes,
                input.scopes,
                Format.DIRECTORY
        )
        // 将input的目录复制到output指定目录
        FileUtils.copyDirectory(input.file, destFile)
    }

    /**
     * 检查class文件是否需要处理
     *
     * @param fileName
     * @return class文件是否需要处理
     */
    static boolean checkClassFile(String name) {
        // 只处理需要的class文件
        return (name.endsWith(".class") && !name.startsWith("R\$")
                && "R.class" != name && "BuildConfig.class" != name)
    }
}

class MyCustomClassVisitor extends ClassVisitor {
    MyCustomClassVisitor(int api) {
        this(api, null)
    }

    MyCustomClassVisitor(int api, ClassVisitor cv) {
        super(api, cv)
    }
}

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class MyTransform extends Transform {
    @Override
    public String getName() {
        // 返回 transform 的名称,最终的名称会是 transformClassesWithMyTransformForDebug 这种形式   
        return "MyTransform";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        /**
        返回需要处理的数据类型 有 下面几种类型可选
        public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
        public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
        public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
        public static final Set<ContentType> CONTENT_NATIVE_LIBS = ImmutableSet.of(NATIVE_LIBS);
        public static final Set<ContentType> CONTENT_DEX = ImmutableSet.of(ExtendedContentType.DEX);
        public static final Set<ContentType> DATA_BINDING_ARTIFACT = ImmutableSet.of(ExtendedContentType.DATA_BINDING);
        */
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        /**
        返回需要处理内容的范围,有下面几种类型
        PROJECT(1), 只处理项目的内容
        SUB_PROJECTS(4), 只处理子项目
        EXTERNAL_LIBRARIES(16), 只处理外部库
        TESTED_CODE(32), 只处理当前 variant 对应的测试代码
        PROVIDED_ONLY(64), 处理依赖
        @Deprecated
        PROJECT_LOCAL_DEPS(2),
        @Deprecated
        SUB_PROJECTS_LOCAL_DEPS(8);
        */
        return Sets.immutableEnumSet(QualifiedContent.Scope.PROJECT);
    }

    @Override
    public boolean isIncremental() {
        // 是否增量,如果返回 true,TransformInput 会包括一份修改的文件列表,返回 false,会进行全量编译,删除上一次的输出内容
        return false;
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        // 在这里处理 class
        super.transform(transformInvocation)
        // 在 transform 里,如果没有任何修改,也要把 input 的内容输出到 output,否则会报错
        for (TransformInput input : transformInvocation.inputs) {
            input.directoryInputs.each { dir ->
                // 获取对应的输出目录
                File output = transformInvocation.outputProvider.getContentLocation(dir.name, dir.contentTypes, dir.scopes, Format.DIRECTORY)
                dir.changedFiles // 增量模式下修改的文件
                dir.file // 获取输入的目录
                FileUtils.copyDirectory(dir.file, output) // input 内容输出到 output
            }
            input.jarInputs.each { jar ->
                // 获取对应的输出 jar
                File output = transformInvocation.outputProvider.getContentLocation(jar.name, jar.contentTypes, jar.scopes, Format.JAR)
                jar.file // 获取输入的 jar 文件
                FileUtils.copyFile(jar.file, output) // input 内容输出到 output
            }
        }
    }
}

// 注册 transform
android.registerTransform(new MyTransform())

7.2 GAP Transform 适配

Transform 在 AGP7.2 被废弃掉了,AGP8.0 会被移除掉。
可用 AsmClassVisitorFactory 替代,据说有个 18% 性能提升,减少 5 倍的代码量

本文由作者按照 CC BY 4.0 进行授权