概述

Google从 Android Gradle 1.5.0 开始,提供了Transform API。通过Transform API,允许第三方以插件的形式,在Android应用程序打包成dex文件之前的编译过程中操作.class文件。我们只要实现一套Transform,去遍历所有.class文件的所有方法,然后进行修改,再对源文件进行替换,即可以达到插入代码的目的。

Transform可以做什么

首先,我们可以先执行一次build操作,命令行会输出如下内容:

1
2
3
4
5
6
7
8
9
10
11
> Transform core-runtime.aar (androidx.arch.core:core-runtime:2.0.0) with AarTransform
> Transform lifecycle-livedata-core.aar (androidx.lifecycle:lifecycle-livedata-core:2.0.0) with AarTransform
> Transform lifecycle-livedata.aar (androidx.lifecycle:lifecycle-livedata:2.0.0) with AarTransform
> Transform interpolator.aar (androidx.interpolator:interpolator:1.0.0) with AarTransform
> Transform savedstate.aar (androidx.savedstate:savedstate:1.0.0) with AarTransform
> Transform lifecycle-viewmodel.aar (androidx.lifecycle:lifecycle-viewmodel:2.1.0) with AarTransform
> Transform lifecycle-runtime.aar (androidx.lifecycle:lifecycle-runtime:2.1.0) with AarTransform
> Transform versionedparcelable.aar (androidx.versionedparcelable:versionedparcelable:1.1.0) with AarTransform
> Transform cursoradapter.aar (androidx.cursoradapter:cursoradapter:1.0.0) with AarTransform
> Transform core.aar (androidx.core:core:1.3.2) with AarTransform
> Transform customview.aar (androidx.customview:customview:1.0.0) with AarTransform

也就是在构建过程中,会执行一个个的Transform。那么回到刚开始的问题,Transform可以做什么,我先列一些大家常听的,以及常见的:

  • 无痕埋点:不需要侵入代码即可以对页面进行埋点,不过一般这种都是针对比较简单的case,复杂的业务场景很难通过无痕埋点处理。

  • 性能监控:这个也很常见。

  • 事件防抖:避免短期内多次点击按钮。

  • 热修复:在方法前插入预留函数已做替换。

  • ……

那么Transform的操作到底是在什么时候将代码植入的呢?我们看一张google官方的打包图:
image

Transform阶段就是在图中红圈的位置,也就是.class文件变成.dex文件过程进行插入的。说白了Transform就是Android官方提供给开发者在项目构建阶段由class到dex转换期间修改class文件的一套api。比较经典的应用就是字节码插桩和代码注入技术。有了这个API,我们就可以根据自己的业务需求做一些定制。

Transform使用

前面说了那么多,主要是介绍了,Transform是什么,能做什么。那么该如何使用呢?

我们先在我们build.gradle中新增一个依赖:

1
2
3
4
5
6
7
8
9
10
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
//新增
implementation "com.android.tools.build:gradle:3.3.2"
}

然后新建一个MyTransform:

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
//注意Transform有很多路径
import com.android.build.api.transform.Transform

class MyTransform extends Transform {

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

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

getName

指定自定义 Transform 的名称,返回的是对应的Task名称

getInputTypes

可以看到这个方法是返回一个Set<QualifiedContent.ContentType>集合,其实就是返回Transform需要处理的文件类型。具体有哪些,TransformManager已经给我们提供了,我们来看一下:

1
2
3
4
5
6
7
8
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> CONTENT_DEX_WITH_RESOURCES =
ImmutableSet.of(ExtendedContentType.DEX, RESOURCES);
类型 描述
CONTENT_CLASS 表示需要处理 java 的 class 文件
CONTENT_JARS 表示需要处理 java 的 class 与 资源文件
CONTENT_RESOURCES 表示需要处理 java 的资源文件
CONTENT_NATIVE_LIBS 表示需要处理 native 库的代码
CONTENT_DEX 表示需要处理 DEX 文件
CONTENT_DEX_WITH_RESOURCES 表示需要处理 DEX 与 java 的资源文件

getScopes

可以看到这个方法是返回一个Set<QualifiedContent.Scope>集合,其实就是返回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
/** Only the project (module) content */
PROJECT(0x01),
/** Only the sub-projects (other modules) */
SUB_PROJECTS(0x04),
/** Only the external libraries */
EXTERNAL_LIBRARIES(0x10),
/** Code that is being tested by the current variant, including dependencies */
TESTED_CODE(0x20),
/** Local or remote dependencies that are provided-only */
PROVIDED_ONLY(0x40),
/**
* Only the project's local dependencies (local jars)
*
* @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
*/
@Deprecated
PROJECT_LOCAL_DEPS(0x02),
/**
* Only the sub-projects's local dependencies (local jars).
*
* @deprecated local dependencies are now processed as {@link#EXTERNAL_LIBRARIES}
*/
@Deprecated
SUB_PROJECTS_LOCAL_DEPS(0x08);

这里主要介绍下前面五个。

类型 描述
PROJECT 只处理当前的项目
SUB_PROJECTS 只处理子项目
EXTERNAL_LIBRARIES 只处理外部依赖库
TESTED_CODE 测试代码
PROVIDED_ONLY 只提供本地或者远程依赖项

同样,TransformManager为我们分装了Scope的返回集合,具体如下:

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
public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT);
public static final Set<Scope> SCOPE_FULL_PROJECT =
Sets.immutableEnumSet(
Scope.PROJECT,
Scope.SUB_PROJECTS,
Scope.EXTERNAL_LIBRARIES);
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_FOR_DEXING =
new ImmutableSet.Builder<ScopeType>()
.addAll(SCOPE_FULL_PROJECT)
.add(InternalScope.MAIN_SPLIT)
.build();
public static final Set<ScopeType> SCOPE_FULL_WITH_FEATURES =
new ImmutableSet.Builder<ScopeType>()
.addAll(SCOPE_FULL_PROJECT)
.add(InternalScope.FEATURES)
.build();
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_AND_FEATURES =
new ImmutableSet.Builder<ScopeType>()
.addAll(SCOPE_FULL_PROJECT)
.add(InternalScope.MAIN_SPLIT)
.add(InternalScope.FEATURES)
.build();
public static final Set<ScopeType> SCOPE_FEATURES = ImmutableSet.of(InternalScope.FEATURES);
public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS =
ImmutableSet.of(Scope.PROJECT, InternalScope.LOCAL_DEPS);
public static final Set<ScopeType> SCOPE_IR_FOR_SLICING =
ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS);

isIncremental

是否进行增量更新,如果返回true,TransformInput会包含一份修改的文件列表,如果返回 false,则会删除上次修改的记录并进行全量编译。

transform

这是最主要的方法,对文件和jar对处理都是在这里进行的,代码植入也是通过此方法进行操作的。常用到的属性有以下几个:

  • TransformInput:对输入的class文件转变成目标字节码文件,TransformInput就是这些输入文件的抽象。目前它包含DirectoryInput集合与JarInput集合。

  • DirectoryInput:源码方式参与项目编译的所有目录结构及其目录下的源文件。

  • JarInput:Jar包方式参与项目编译的所有本地jar或远程jar包。

  • TransformOutProvider:通过这个类来获取输出路径。

使用

当你编写完成之后,我们只需要在我们的plugin中添加如下代码就可以使用你自己写的Transform了。

1
2
3
4
5
6
7
8
9
10
11
12
class MyGradlePlugin implements Plugin<Project> {

@Override
void apply(Project project) {
...
def android = project.extensions.getByType(AppExtension)
def classTransform = new MyTransform(project)
//注册你的Transform
android.registerTransform(classTransform)
...
}
}

总结

回到标题,Transform是什么?Transform其实就是在编译过程中可以动态织入代码。最主要的目的就是解耦。让开发更注重于业务开发。一些数据监控、无痕埋点等逻辑交给Transfrom处理。

参考

Gradle-初探代码注入Transform