Gradle 系列(8)其实 Gradle Transfor

请点赞关注,你的支持对我意义重大。

🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。

前言

目前,使用 AGP Transform API 进行字节码插桩已经非常普遍了,例如 Booster、神策等框架中都有 Transform 的影子。Transform 听起来很高大上,其本质就是一个 Gradle Task。在这篇文章里,我将带你理解 Transform 的工作机制、使用方法和核心源码解析,并通过一个 Github · DemoHall · HelloTransform Demo 帮助你融会贯通。


这篇文章是 Gradle 系列文章第 8 篇,相关 Android 工程化专栏完整文章列表:

一、Gradle 基础:

二、AGP 插件:

三、组件化开发:

四、AOP 面向切面编程:

五、相关计算机基础


  1. 认识 Transform

1.1 什么是 Transform?

Transform API 是 Android Gradle Plugin 1.5 就引入的特性,主要用于在 Android 构建过程中,在 Class→Dex 这个节点修改 Class 字节码。利用 Transform API,我们可以拿到所有参与构建的 Class 文件,借助 Javassist 或 ASM 等字节码编辑工具进行修改,插入自定义逻辑。一般来说,这些自定义逻辑是与业务逻辑无关的。

使用 Transform 的常见的应用场景有:

  • 埋点统计: 在页面展现和退出等生命周期中插入埋点统计代码,以统计页面展现数据;
  • 耗时监控: 在指定方法的前后插入耗时计算,以观察方法执行时间;
  • 方法替换: 将方法调用替换为调用另一个方法。

1.2 Transform 的基本原理

先大概了解下 Transform 的工作机制:

  • 1、工作时机: Transform 工作在 Android 构建中由 Class → Dex 的节点;
  • 2、处理对象: 处理对象包括 Javac 编译后的 Class 文件、Java 标准 resource 资源、本地依赖和远程依赖的 JAR/AAR。Android 资源文件不属于 Transform 的操作范围,因为它们不是字节码;
  • 3、Transform Task: 每个 Transform 都对应一个 Task,Transform 的输入和输出可以理解为对应 Transform Task 的输入输出。每个 TransformTask 的输出都分别存储在 app/build/intermediates/transform/[Transform Name]/[Variant] 文件夹中;
  • 4、Transform 链: TaskManager 会将每个 TransformTask 串联起来,前一个 Transform 的输出会作为下一个 Transform 的输入。

1.3 Transform API

了解 Transform 的基本工作机制后,我们先来看 Transform 的核心 API。这里仅列举出 Transform 抽象类中最核心的方法,有几个次要的方法后面再说。

com.android.build.api.transform.java

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
typescript复制代码public abstract class Transform {

// 指定 Transform 的名称,该名称还会用于组成 Task 的名称
// 格式为 transform[InputTypes]With[name]For[Configuration]
   public abstract String getName();

   // (孵化中)用于过滤 Variant,返回 false 表示该 Variant 不执行 Transform
   public boolean applyToVariant(VariantInfo variant) {
       return true;
  }

   // 指定输入内容类型
   public abstract Set<ContentType> getInputTypes();

   // 指定输出内容类型,默认取 getInputTypes() 的值
   public Set<ContentType> getOutputTypes() {
       return getInputTypes();
  }

   // 指定消费型输入内容范畴
   public abstract Set<? super Scope> getScopes();

   // 指定引用型输入内容范畴
   public Set<? super Scope> getReferencedScopes() {
       return ImmutableSet.of();
  }

   // 指定是否支持增量编译
   public abstract boolean isIncremental();

   // 核心 API
   public void transform(TransformInvocation transformInvocation)
           throws TransformException, InterruptedException, IOException {
       // 分发到过时 API,以兼容旧版本的 Transform
       //noinspection deprecation
       transform(transformInvocation.getContext(), transformInvocation.getInputs(),
               transformInvocation.getReferencedInputs(),
               transformInvocation.getOutputProvider(),
               transformInvocation.isIncremental());
  }

   // 指定是否支持缓存
   public boolean isCacheable() {
       return false;
  }
}

1.4 ContentType 内容类型

ContentType 是一个枚举类接口,表示输入或输出内容的类型,在 AGP 中定义了 DefaultContentTypeExtendedContentType 两个枚举类。但是,我们在自定义 Transform 时只能使用 DefaultContentType 中定义的枚举,即 CLASSESRESOURCES 两种类型,其它类型仅供 AGP 内置的 Transform 使用。

自定义 Transform 需要在两个位置定义内容类型:

  • 1、Set getInputTypes(): 指定输入内容类型,允许通过 Set 集合设置输入多种类型;
  • 2、Set getOutputTypes(): 指定输出内容类型,默认取 getInputTypes() 的值,允许通过 Set 集合设置输出多种类型。

ExtendedContentType.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码// 加强类型,自定义 Transform 无法使用
public enum ExtendedContentType implements ContentType {

   // DEX 文件
   DEX(0x1000),

   // Native 库
   NATIVE_LIBS(0x2000),

   // Instant Run 加强类
   CLASSES_ENHANCED(0x4000),

   // Data Binding 中间产物
   DATA_BINDING(0x10000),

   // Dex Archive
   DEX_ARCHIVE(0x40000),
;
}

QualifiedContent.java

1
2
3
4
5
6
7
8
scss复制代码enum DefaultContentType implements ContentType {

// Java 字节码,包括 Jar 文件和由源码编译产生的
   CLASSES(0x01),

   // Java 资源
   RESOURCES(0x02);
}

在 TransformManager 中,预定义了一部分内容类型集合,常用的是 CONTENT_CLASS 操作 Class。

TransformManager.java

1
2
3
swift复制代码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);

1.5 ScopeType 作用域

ScopeType 也是一个枚举类接口,表示输入内容的范畴。在 AGP 中定义了 InternalScopeScope 两个枚举类。但是,我们在自定义 Transform 只能使用 Scope 中定义的枚举,其它类型仅供 AGP 内置的 Transform 使用。

Transform 需要在两个位置定义输入内容范围:

  • 1、Set getScopes() 消费型输入内容范畴: 此范围的内容会被消费,因此当前 Transform 必须将修改后的内容复制到 Transform 的中间目录中,否则无法将内容传递到下一个 Transform 处理;
  • 2、Set getReferencedScopes() 指定引用型输入内容范畴: 默认是空集合,此范围的内容不会被消费,因此不需要复制传递到下一个 Transform,也不允许修改。

InternalScope.java

1
2
3
4
5
6
7
8
9
10
scss复制代码// 内部使用的作用域,自定义 Transform 无法使用
public enum InternalScope implements QualifiedContent.ScopeType {

   MAIN_SPLIT(0x10000),

   LOCAL_DEPS(0x20000),

   FEATURES(0x40000),
  ;
}

QualifiedContent.java

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码enum Scope implements ScopeType {

   // 当前模块
   PROJECT(0x01),
// 子模块
   SUB_PROJECTS(0x04),
// 外部依赖,包括当前模块和子模块本地依赖和远程依赖的 JAR/AAR
   EXTERNAL_LIBRARIES(0x10),
// 当前变体所测试的代码(包括依赖项)
   TESTED_CODE(0x20),
// 本地依赖和远程依赖的 JAR/AAR(provided-only)
   PROVIDED_ONLY(0x40),
}

在 TransformManager 中,预定义了一部分作用域集合,常用的是 SCOPE_FULL_PROJECT 所有模块。需要注意,Library 模块注册的 Transform 只能使用 Scope.PROJECT。

TransformManager.java

1
2
swift复制代码public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT);
public static final Set<ScopeType> SCOPE_FULL_PROJECT = ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES);

1.6 transform 方法

transform() 是实现 Transform 的核心方法,方法的参数是 TransformInvocation,它提供了所有与输入输出相关的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csharp复制代码public interface TransformInvocation {

   Context getContext();

   // 消费型输入内容
   Collection<TransformInput> getInputs();

   // 引用型输入内容
   Collection<TransformInput> getReferencedInputs();

// 额外输入内容
   Collection<SecondaryInput> getSecondaryInputs();

   // 输出信息
   TransformOutputProvider getOutputProvider();

   // 是否增量构建
   boolean isIncremental();
}
  • isIncremental(): 当前 Transform 任务是否增量构建;
  • getInputs(): 获取 TransformInput 对象,它是消费型输入内容,对应于 Transform#getScopes() 定义的范围;
  • getReferencedInputs(): 获取 TransformInput 对象,它是引用型输入内容,对应于 Transform#getReferenceScope() 定义的内容范围;
  • getOutPutProvider(): TransformOutputProvider 是对输出文件的抽象。

输入内容 TransformInput 由两部分组成:

  • DirectoryInput 集合: 以源码方式参与构建的输入文件,包括完整的源码目录结构及其中的源码文件;
  • JarInput 集合: 以 Jar 和 aar 依赖方式参与构建的输入文件,包含本地依赖和远程依赖。

输入内容信息 TransformOutputProvider 有两个功能:

  • deleteAll(): 当 Transform 运行在非增量构建模式时,需要删除上一次构建产生的所有中间文件,可以直接调用 deleteAll() 完成;
  • getContentLocation(): 获得指定范围+类型的输出目标路径。

TransformOutputProvider.java

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码public interface TransformOutputProvider {

// 删除所有中间文件
void deleteAll()

// 获取指定范围+类型的目标路径
File getContentLocation(String name,
Set<QualifiedContent.ContentType> types,
Set<? super QualifiedContent.Scope> scopes,
Format format);
}

获取输入内容对应的输出路径:

1
2
3
4
5
6
7
8
9
10
11
scss复制代码for (input in transformInvocation.inputs) {
for (jarInput in input.jarInputs) {
// 输出路径
val outputJar = outputProvider.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR
)
}
}

1.7 Transform 增量模式

任何构建系统都会尽量避免重复执行相同工作,Transform 也不例外。虽然增量构建并不是必须的,但作为一个合格的 Transform 实现应该具备增量能力。

1、增量模式标记位: Transform API 有两个增量标志位,不要混淆:

  • Transform#isIncremental(): Transform 增量构建的使能开关,返回 true 才有可能触发增量构建;
  • TransformInvocation#isIncremental(): 当次 TransformTask 是否增量执行,返回 true 表示正在增量模式。

2、Task 增量模式与 Transform 增量模式的区别: Task 增量模式与 Transform 增量模式的区别在于,Task 增量执行时会跳过整个 Task 的动作列表,而 Transform 增量执行依然会执行 TransformTask,但输入内容会增加变更内容信息。

3、增量模式的输入: 增量模式下的所有输入都是带状态的,需要根据这些状态来做不同的处理,不需要每次所有流程都重新来一遍。比如新增的输入就需要处理,而未修改的输入就不需要处理。Transform 定义了四个输入文件状态:

com.android.build.api.transform.Status.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arduino复制代码public enum Status {

// 未修改,不需要处理,也不需要复制操作
NOTCHANGED,

// 新增,正常处理并复制给下一个任务
ADDED,

// 已修改,正常处理并复制给下一个任务
CHANGED,

// 已删除,需同步移除 OutputProvider 指定的目标文件
REMOVED;
}

1.8 注册 Transform

在 BaseExtension 中维护了一个 Transform 列表,自定义 Transform 需要注册才能生效,而且还支持额外设置 TransformTask 的依赖。

BaseExtension.kt

1
2
3
4
5
6
7
8
9
10
kotlin复制代码abstract class BaseExtension {
private val _transforms: MutableList<Transform> = mutableListOf()
private val _transformDependencies: MutableList<List<Any>> = mutableListOf()
...

fun registerTransform(transform: Transform, vararg dependencies: Any) {
_transforms.add(transform)
_transformDependencies.add(listOf(dependencies))
}
}

注册 Transform:

1
2
3
4
kotlin复制代码// 获取 Android 扩展
val androidExtension = project.extensions.getByType(BaseExtension::class.java)
// 注册 Transform,支持额外增加依赖
androidExtension.registerTransform(ToastTransform(project)/* 支持增加依赖*/)

提示: 为了提高编译效率,可以判断 Variant 为 release 类型才注册 Transform,也可以通过重写 Transform#applyToVariant() 来决定是否执行 Transform。


  1. Transform 核心源码分析

这一节我们来分析 Transform 相关核心源码,这里我们引用的是 Android Gradle Plugin 7.1.0 版本的源码。

2.1 Transform 与 Task 的关系

Project 的构建逻辑由一系列 Task 的组成,每个 Task 负责完成一个基本的工作,例如 Javac 编译 Task。Transform 也是依靠 Task 执行的,在配置阶段,Gradle 会为注册的 Transform 创建对应的 Task。

提示: 说 “创建” 可能不太严谨,TransformManager 使用 register 懒创建的方式注册 Task,其实还没有创建 Task 实例。我们不要复杂化了,就说创建吧。

而 Task 的依赖关系是通过 TransformTask 的输入输出关系隐式确定的,TransformManager 通过 TransformStream 链接各个 TransformTask 的输入输出,进而控制 Transform 的依赖关系顺序。

LibraryTaskManager.java

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
scss复制代码@Override
protected void doCreateTasksForVariant(ComponentInfo<LibraryVariantBuilderImpl, LibraryVariantImpl> variantInfo) {
...
// ----- External Transforms -----
// apply all the external transforms.
List<Transform> customTransforms = extension.getTransforms();
List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies();

final IssueReporter issueReporter = libraryVariant.getServices().getIssueReporter();

for (int i = 0, count = customTransforms.size(); i < count; i++) {
Transform transform = customTransforms.get(i);

// Check the transform only applies to supported scopes for libraries:
// We cannot transform scopes that are not packaged in the library
// itself.
Sets.SetView<? super Scope> difference = Sets.difference(transform.getScopes(), TransformManager.PROJECT_ONLY);
if (!difference.isEmpty()) {
String scopes = difference.toString();
issueReporter.reportError(
Type.GENERIC,
String.format(
"Transforms with scopes '%s' cannot be applied to library projects.",
scopes));
}

List<Object> deps = customTransformsDependencies.get(i);
transformManager.addTransform(
taskFactory,
libraryVariant,
transform,
null,
task -> {
// (3.2节提到的额外依赖)
// 在注册 Transform 时,可以额外增加依赖
if (!deps.isEmpty()) {
task.dependsOn(deps);
}
},
taskProvider -> {
// if the task is a no-op then we make assemble task
// depend on it.
if (transform.getScopes().isEmpty()) {
TaskFactoryUtils.dependsOn(
libraryVariant.getTaskContainer().getAssembleTask(),
taskProvider);
}
});
}

// Create jar with library classes used for publishing to runtime elements.
taskFactory.register(new BundleLibraryClassesJar.CreationAction(
libraryVariant, AndroidArtifacts.PublishedConfigType.RUNTIME_ELEMENTS));
...
}

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

2.2 Transform 的创建过程

  • 1、注册 Transform: 注册 Transform 仅是将对象注册到 BaseExtension 中的列表中。TransformManager 会通过 Task 的输入输出隐式建立 Transform 的依赖顺序,另外还支持在注册时添加额外的依赖。

BaseExtension.kt

1
2
3
4
5
6
7
8
9
10
kotlin复制代码abstract class BaseExtension {
private val _transforms: MutableList<Transform> = mutableListOf()
private val _transformDependencies: MutableList<List<Any>> = mutableListOf()
...

fun registerTransform(transform: Transform, vararg dependencies: Any) {
_transforms.add(transform)
_transformDependencies.add(listOf(dependencies))
}
}
  • 2、创建 TransformTask 的执行链: TransformTask 属于 Android 构建构成的一部分,所有 Android Task 的创建入口都从 BasePlugin#createAndroidTasks() 开始。其中会为所有 Variant 变体创建相关的 Task,经过一系列调用后,会通过抽象方法 TaskManager#doCreateTaskForVariant() 分派到 ApplicationTaskManager 和 LibraryTaskManager 两个子类中,以区分 App 模块和 Library 模块。

调用链概要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码BasePlugin#createAndroidTasks()
-> TaskManager#createTasks()->遍历所有变体
-> for {
TaskManager#createTasksForVariant(variant)
-> abstract TaskManager#doCreateTasksForVariant(variant)
// App
-> ApplicationTaskManager#doCreateTasksForVariant(variant)
-> ApplicationTaskManager#createCommonTask(variant)
-> ApplicationTaskManager#createCompileTask(variant)
-> TaskManager#createPostCompilationTasks(config)
-> for { Transform#addTransform(transform) }
// Library
-> LibraryTaskManager#doCreateTasksForVariant(variant)
-> for { Transform#addTransform(transform) }
}

2.3 TransformTask 的命名格式

Transform#getName() 会用于构造 Task Name,命名格式为 transform[InputTypes]With[name]For[Configuration],例如 transformClassed。这块源码体现在 TransformManager 中创建 Task 的位置:

TransformManager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
scss复制代码// 创建 Transform Task
public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(...) {
...
// TaskName = 前缀 + Configuration
String taskName = creationConfig.computeTaskName(getTaskNamePrefix(transform), "");
...
}

// TaskName 前缀
static String getTaskNamePrefix(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();
}

2.4 TransformTask 的输入输出

TransformTask 通过 @Input 和 @OutputDirectory 等注解,将 Transform API 关联到 Task 的输入输出上:

TransformTask.java

1
2
3
4
5
6
7
8
9
10
11
12
13
scala复制代码public abstract class TransformTask extends StreamBasedTask {

...

@Input
public Set<QualifiedContent.ContentType> getInputTypes() {
return transform.getInputTypes();
}

@OutputDirectory
@Optional
public abstract DirectoryProperty getOutputDirectory();
}

2.5 执行 transform() 方法

每个 Task 内部都保持了一个 Action 列表 actions,执行 Task 就是按顺序执行这个列表,对于自定义 Task,可以通过 @TaskAction 注解添加默认 Action。

TransformTask.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@TaskAction
void transform(final IncrementalTaskInputs incrementalTaskInputs) {
...
transform.transform(new TransformInvocationBuilder(context)
.addInputs(consumedInputs.getValue())
.addReferencedInputs(referencedInputs.getValue())
.addSecondaryInputs(changedSecondaryInputs.getValue())
.addOutputProvider(outputStream != null
? outputStream.asOutput()
: null)
.setIncrementalMode(isIncremental.getValue())
.build());
...
}

2.6 Library 模块限制

Library 模块仅只支持使用 Scope.PROJECT 作用域:

LibraryTaskManager.java

1
2
3
4
5
6
7
8
scss复制代码// Check the transform only applies to supported scopes for libraries:
// We cannot transform scopes that are not packaged in the library
// itself.
Sets.SetView<? super Scope> difference = Sets.difference(transform.getScopes(), TransformManager.PROJECT_ONLY);
if (!difference.isEmpty()) {
String scopes = difference.toString();
issueReporter.reportError(Type.GENERIC, String.format("Transforms with scopes '%s' cannot be applied to library projects.",scopes));
}

  1. 自定义 Transform 模板

上一节我们探讨了 Transform 的基本工作机制,第 3 节和第 4 节我们来实现一个 Transform Demo。Transform 的核心代码在 transform() 方法中,我们要做的就是遍历输入文件,再把修改后的文件复制到目标路径中,对于 JarInputs 还有一次解压和压缩。更进一步,再考虑增量编译的情况。

因此,整个 Transform 的核心过程是有固定套路,模板流程图如下:

—— 图片引用自 rebooters.github.io/2020/01/04/…

我们把整个流程图做成一个抽象模板类,子类需要重写 provideFunction() 方法,从输入流读取 Class 文件,修改完字节码后再写入到输出流。甚至不需要考虑 Trasform 的输入文件遍历、加解压、增量等,舒服!

BaseCustomTransform.kt

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
kotlin复制代码abstract class BaseCustomTransform(private val debug: Boolean) : Transform() {

abstract fun provideFunction(): ((InputStream, OutputStream) -> Unit)?

open fun classFilter(className: String) = className.endsWith(SdkConstants.DOT_CLASS)

override fun isIncremental() = true

override fun transform(transformInvocation: TransformInvocation) {
super.transform(transformInvocation)

log("Transform start, isIncremental = ${transformInvocation.isIncremental}.")

val inputProvider = transformInvocation.inputs
val referenceProvider = transformInvocation.referencedInputs
val outputProvider = transformInvocation.outputProvider

// 1. Transform logic implemented by subclasses.
val function = provideFunction()

// 2. Delete all transform tmp files when not in incremental build.
if (!transformInvocation.isIncremental) {
log("All File deleted.")
outputProvider.deleteAll()
}

for (input in inputProvider) {
// 3. Transform jar input.
log("Transform jarInputs start.")
for (jarInput in input.jarInputs) {
val inputJar = jarInput.file
val outputJar = outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
if (transformInvocation.isIncremental) {
// 3.1 Transform jar input in incremental build.
when (jarInput.status ?: Status.NOTCHANGED) {
Status.NOTCHANGED -> {
// Do nothing.
}
Status.ADDED, Status.CHANGED -> {
// Do transform.
transformJar(inputJar, outputJar, function)
}
Status.REMOVED -> {
// Delete.
FileUtils.delete(outputJar)
}
}
} else {
// 3.2 Transform jar input in full build.
transformJar(inputJar, outputJar, function)
}
}
// 4. Transform dir input.
log("Transform dirInput start.")
for (dirInput in input.directoryInputs) {
val inputDir = dirInput.file
val outputDir = outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)
if (transformInvocation.isIncremental) {
// 4.1 Transform dir input in incremental build.
for ((inputFile, status) in dirInput.changedFiles) {
val outputFile = concatOutputFilePath(outputDir, inputFile)
when (status ?: Status.NOTCHANGED) {
Status.NOTCHANGED -> {
// Do nothing.
}
Status.ADDED, Status.CHANGED -> {
// Do transform.
doTransformFile(inputFile, outputFile, function)
}
Status.REMOVED -> {
// Delete
FileUtils.delete(outputFile)
}
}
}
} else {
// 4.2 Transform dir input in full build.
// Traversal fileTree (depthFirstPreOrder).
for (inputFile in FileUtils.getAllFiles(inputDir)) {
val outputFile = concatOutputFilePath(outputDir, inputFile)
if (classFilter(inputFile.name)) {
doTransformFile(inputFile, outputFile, function)
} else {
// Copy.
Files.createParentDirs(outputFile)
FileUtils.copyFile(inputFile, outputFile)
}
}
}
}
}
log("Transform end.")
}

/**
* Do transform Jar.
*/
private fun transformJar(inputJar: File, outputJar: File, function: ((InputStream, OutputStream) -> Unit)?) {
// Create parent directories to hold outputJar file.
Files.createParentDirs(outputJar)
// Unzip.
FileInputStream(inputJar).use { fis ->
ZipInputStream(fis).use { zis ->
// Zip.
FileOutputStream(outputJar).use { fos ->
ZipOutputStream(fos).use { zos ->
var entry = zis.nextEntry
while (entry != null && isValidZipEntryName(entry)) {
if (!entry.isDirectory) {
zos.putNextEntry(ZipEntry(entry.name))
if (classFilter(entry.name)) {
// Apply transform function.
applyFunction(zis, zos, function)
} else {
// Copy.
zis.copyTo(zos)
}
}
entry = zis.nextEntry
}
}
}
}
}
}

/**
* Do transform file.
*/
private fun doTransformFile(inputFile: File, outputFile: File, function: ((InputStream, OutputStream) -> Unit)?) {
// Create parent directories to hold outputFile file.
Files.createParentDirs(outputFile)
FileInputStream(inputFile).use { fis ->
FileOutputStream(outputFile).use { fos ->
// Apply transform function.
applyFunction(fis, fos, function)
}
}
}

private fun concatOutputFilePath(outputDir: File, inputFile: File) = File(outputDir, inputFile.name)

private fun applyFunction(input: InputStream, output: OutputStream, function: ((InputStream, OutputStream) -> Unit)?) {
try {
if (null != function) {
function.invoke(input, output)
} else {
// Copy
input.copyTo(output)
}
} catch (e: UncheckedIOException) {
throw e.cause!!
}
}

private fun log(logStr: String) {
if (debug) {
println("$name - $logStr")
}
}
}

  1. Hello Transform 示例

现在,我手把手带你基于 BaseCustomTransform 实现一个 Transform Demo。示例代码我已经上传到 Github · DemoHall · HelloTransform。有用请给个免费的 Star 支持下。

Demo 效果很简单:

  • 实现一个 Transform,在编译时在 Activity#onCreate() 方法末尾织入一个 Toast 语句;
  • 仅通过自定义注解 @Hello 修饰的 Activity#onCreate() 方法会生效。

4.1 步骤 1:初始化代码框架

首先,我们先搭建工程的整体框架,再来编写核心的 Transform 逻辑。我们选择自定义 Gradle 插件来承载 Transform 的逻辑,可维护性更好。关于自定义 Gradle 插件的步骤具体见上一篇文章《手把手带你自定义 Gradle 插件》,此处不展开。

提示: 提醒一下,并不是说一定要由 Gradle 插件来承载,你直接在 .gradle 文件中实现也是 OK 的。

插件实现类如下:

ToastPlugin.kt

1
2
3
4
5
6
7
8
kotlin复制代码class ToastPlugin : Plugin<Project> {
override fun apply(project: Project) {
// 获取 Android 扩展
val androidExtension = project.extensions.getByType(BaseExtension::class.java)
// 注册 Transform,支持额外增加依赖
androidExtension.registerTransform(ToastTransform(project)/* 支持增加依赖*/)
}
}

4.2 步骤 2:拷贝 Transform 模板类

将我们实现的 BaseCustomTransform 模板类复制到工程下,再实现一个子类:

ToastTransform.kt

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
kotlin复制代码internal class ToastTransform(val project: Project) : BaseCustomTransform(true) {

// Transform 名
override fun getName() = "ToastTransform"

// 是否支持增量构建
override fun isIncremental() = true

/**
* 用于过滤 Variant,返回 false 表示该 Variant 不执行 Transform
*/
@Incubating
override fun applyToVariant(variant: VariantInfo?): Boolean {
return "debug" == variant?.buildTypeName
}

// 指定输入内容类型
override fun getInputTypes() = TransformManager.CONTENT_CLASS

// 指定消费型输入内容范畴
override fun getScopes() = TransformManager.SCOPE_FULL_PROJECT

// 转换方法
override fun provideFunction() = { ios: InputStream, zos: OutputStream ->
input.copyTo(output)
}
}

其中,provideFunction() 是模板代码,参数分别表示源 Class 文件的输入流和目标 Class 文件输出流。子类要做的事,就是从输入流读取 Class 信息,修改后写入到输出流。

4.3 步骤 3:使用 Javassist 修改字节码

使用 Javassist API 从输入流加载数据,在匹配到 onCreate() 方法后检查是否声明 @Hello 注解。是则在该方法末尾织入一句 Toast:Hello Transform。本文重点不是 Javassist,此处就不展开了。

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
kotlin复制代码override fun provideFunction() = { ios: InputStream, zos: OutputStream ->
val classPool = ClassPool.getDefault()
// 加入android.jar
classPool.appendClassPath((project.extensions.getByName("android") as BaseExtension).bootClasspath[0].toString())
classPool.importPackage("android.os.Bundle")
// Input
val ctClass = classPool.makeClass(ios)
try {
ctClass.getDeclaredMethod("onCreate").also {
println("onCreate found in ${ctClass.simpleName}")
val attribute = it.methodInfo.getAttribute(AnnotationsAttribute.invisibleTag) as? AnnotationsAttribute
if (null != attribute?.getAnnotation("com.pengxr.hellotransform.Hello")) {
println("Insert toast in ${ctClass.simpleName}")
it.insertAfter(
"""android.widget.Toast.makeText(this,"Hello Transform!",android.widget.Toast.LENGTH_SHORT).show();
"""
)
}
}
} catch (e: NotFoundException) {
// ignore
}
// Output
zos.write(ctClass.toBytecode())
ctClass.detach()
}

4.4 步骤 4:应用插件

sample 模块 build.gradle

1
arduino复制代码apply plugin: 'com.pengxr.toastplugin'

4.5 步骤 5:声明 @Hello 注解

HelloActivity.kt

1
2
3
4
5
6
7
8
kotlin复制代码class HelloActivity : AppCompatActivity() {

@Hello
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_hello)
}
}

4.6 步骤 6:运行

完成以上步骤后,编译运行程序。可以在 Build Output 看到以下输出,HelloActivity 启动时会弹出 Toast HelloTransform,说明织入成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ruby复制代码...
Task :sample:mergeDebugJavaResource

> Task :sample:transformClassesWithToastTransformForDebug
...
onCreate found in HelloActivity
Insert toast in HelloActivity
ToastTransform - Transform end.

> Task :sample:dexBuilderDebug
> Task :sample:mergeExtDexDebug
> Task :sample:mergeDexDebug
> Task :sample:packageDebug
> Task :sample:createDebugApkListingFileRedirect
> Task :sample:assembleDebug

BUILD SUCCESSFUL in 3m 18s
33 actionable tasks: 33 executed

Build Analyzer results available

  1. Transform 的未来

从 AGP 7.0 开始,Transform API 已经被废弃了。是的,就是卷,而且这次直接是降维打击。以前 Transform 是 AGP 的特性,现在 Gradle 也来整 Transform,不过换了个名字,叫 —— TransformAction

那么,我们还有必要学 AGP Transform API 吗?如果你现在涉足字节码插桩这块,你建议你还是学以下:

  • 1、社区沉淀: AGP Transform API 发展多年,目前社区中已经沉淀下非常多优秀的开源组件和博客,这些资源对你非常有帮助。而 TransformAction 的社区沉淀还非常单薄;
  • 2、技术思维: 虽然换了一套 API,但背后的思路 / 套路是相似的。理解 AGP Transform 的工作机制,对你理解 Gradle TransformAction 有事半功倍的效果。

例如,以下是 Gradle 官方文档的演示代码,是不是套路差不多?

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
less复制代码abstract class CountLoc implements TransformAction<TransformParameters.None> {

@Inject
abstract InputChanges getInputChanges()

@PathSensitive(PathSensitivity.RELATIVE)
@InputArtifact
abstract Provider<FileSystemLocation> getInput()

@Override
void transform(TransformOutputs outputs) {
def outputDir = outputs.dir("${input.get().asFile.name}.loc")
println("Running transform on ${input.get().asFile.name}, incremental: ${inputChanges.incremental}")
inputChanges.getFileChanges(input).forEach { change ->
def changedFile = change.file
if (change.fileType != FileType.FILE) {
return
}
def outputLocation = new File(outputDir, "${change.normalizedPath}.loc")
switch (change.changeType) {
case ADDED:
case MODIFIED:
println("Processing file ${changedFile.name}")
outputLocation.parentFile.mkdirs()

outputLocation.text = changedFile.readLines().size()

case REMOVED:
println("Removing leftover output file ${outputLocation.name}")
outputLocation.delete()

}
}
}
}
  1. 总结

本文的示例代码已上传到 github.com/pengxurui/D…,请 Star 支持。关注我,带你了解更多,我们下次见。


2022 年 8 月 15 号更新

经评论区 @奋斗的bigHead 反馈,BaseCustomTransform 存在缺陷:在 classFilter() 设置过滤部分 class 文件(例如 className.endsWith("Activity.class") )时,编译时会出现找不到类。这是因为 BaseCustomTransform 未透明复制被过滤掉的文件。该缺陷现已修复,模板代码和 GitHub Demo 已同步修改。

参考资料

我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐] 私信我提问。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%