如果你想在编译期间搞事情,如常用的有无痕埋点,方法耗时统计和组件通信中自动注入等等,就要来学习字节码插桩的技术。而所谓字节码插桩技术其实就是修改已经编译好的class文件,在里面添加自己的字节码,然后打出的包就是修改后的class文件。在动手开发之前,还需要了解如何自定义gradle插件 ,以及如何自定义Transform ,下面我们来看看具体做法。
一、 Gradle插件
在Gradle官方文档里目前定义插件的方式有 三种:
脚本插件 :直接在构建脚本中直接写插件的代码,编译器会自动将插件编译并添加到构建脚本的classpath中。buildSrc project :执行Gradle时 会把根目录下的buildSrc目录作为插件源码目录进行编译,编译后会把结果加入到构建脚本的classpath中,对于整个项目是可用的。Standalone project :可以在独立项目中开发插件,然后将项目达成jar包,发布到本地或者mave服务器上。
实例代码可以参考 GradleTestDemo
1.1 直接在build.gradle文件中实现
apply plugin: CustomPluginA
class CustomPluginA implements Plugin<Project> {
@Override
void apply(Project target) {
println 'Hello gradle!'
}
}
这种方式在构建脚本之外是不可以见的,所以只有在定义该插件的gradle脚本里才可以引用改插件。
1.2 在默认目录buildSrc中实现
buildSrc目录是gradle默认的目录之一,该目录会在构建时自动的进行编译打包,所以在这里面不需要任何额外的配置,就可以直接被其他模块中的gradle脚本所引用。
- 创建的目录结构
2. 将项目中的build.gradle中的所有配置去掉,并配置groovy、resources为源码目录以及相关依赖:
buildscript {
ext {
kotlin_version = '1.5.31'
apg_Version = '3.4.0'
booster_version = '4.0.0'
}
repositories {
mavenCentral()
google()
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.android.tools.build:gradle:$apg_Version"
}
}
apply plugin: 'maven'
apply plugin: 'maven-publish'
apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
repositories {
mavenCentral()
google()
jcenter()
}
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation "commons-io:commons-io:2.6"
implementation 'com.android.tools.build:gradle:3.4.0'
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-util:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
}
gradle插件是可以使用java,groovy,kotlin编写 ,所以你可以根据自己的需要引入相关的依赖。
-
在main目录下新建resources/MATE-INF/gradle-plugins目录: 在里面新建“HelloPlugin.properties”文件,其中“HelloPlugin”是可以随意定义的名称,而这个名称也就是你的插件名称。然后在引用该插件时你可以通过apply plugin: 'HelloPlugin' 的方式来引用。 HelloPlugin.properties 文件中的内容就是: implementation-class=transform.hello.HelloTransformPlugin
另外定义在buildSrc 下面的插件也可以直接用apply plugin: HelloPlugin 来引入,而HelloPlugin就是你定义的plugin的类名了。
注意:格式一定要写成resources/MATE-INF/gradle-plugins这样的三级目录,有些同学操作的时候碰到自定义的plugin找不到就是因为在直接复制目录地址的时候,由于编译器缩写的关系,目录地址变成了resources/MATE-INF.gradle-plugins。
-
自定义gradle插件: 在transform 目录下创建HelloTransformPlugin 类,并实现Plugin接口。 class HelloTransformPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
println "Hello TransformPlugin"
def extension = project.extensions.create("custom", CustomExtension)
AppExtension appExtension = project.extensions.getByType(AppExtension)
appExtension.registerTransform(new HelloTransform())
}
}
你会看到这里多了个Transform类 也是接下来我们要说的。另外其中的 CustomExtension 是自定义属性类,可以通过主项目的 build.gradle 文件传值,这样就可以在脚本中去扩展属性: class CustomExtension {
String extensionArgs = ""
}
然后在主项目的build.gradle 命名要与注册时保持一致: custom{
extensionArgs = "我是参数"
}
在 project.extensions.create 方法的内部其实质是 通过 project.extensions.create() 方法来获取在 custom 闭包中定义的内容并通过反射将闭包的内容转换成一个 CustomExtension 对象。
1.3 在独立项目开发中实现
这种方式基本跟第二种相似,不过要引入这个插件的话要先把它发布到本地或者mave服务器上。
-
修改 build.gradle 的内容,增加上传到本地的代码,可以如下这样修改: apply plugin: 'groovy'
apply plugin: 'java'
apply plugin: 'maven'
repositories {
jcenter()
}
uploadArchives {
repositories.mavenDeployer {
repository(url: uri('../repo'))
pom.groupId = "com.xiam.plugin"
pom.artifactId = "startplugin"
pom.version = "1.0.0"
}
}
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.4.0'
}
-
修改相关的 build.gradle 文件,添加依赖,在根项目的 build.gradle 中添加: -
最后是构建在 gradle task 里面,运行 uploadArchives 任务即可,或者通过./gradlew uploadArchivers 来执行这个 task:
二、玩转Transform
Google官方在Android GradleV1.5.0 版本以后提供了Transform API,它允许第三方插件在编译后的类文件转换为dex文件之前做处理操作,我们需要做的就是实现Transform来对.class文件便遍历来拿到所有方法,修改完成后再对源文件进行替换就可以了。感兴趣可以去看看Transform版本历史。
2.1 Transform的使用
在前面我们已经看到了怎样对一个transform进行注册,也就是我们在我们自定义的plugin中,通过如下进行注册,这里我选择使用kotlin来实现:
class HelloPlugin: Plugin<Project> {
override fun apply(target: Project) {
target.extensions.findByType(AppExtension::class.java)?.run {
registerTransform(HelloTransform(target))
}
}
}
自定义的Transform是要继承于com.android.build.api.transform.Transform ,可以看下Transform文档介绍,现在我们先来定义一个自定义的Transform(不支持增量 ):
class HelloTransform: Transform() {
override fun getName(): String = "HelloTransform"
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_CLASS
override fun isIncremental(): Boolean = false
override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.SCOPE_FULL_PROJECT
override fun transform(transformInvocation: TransformInvocation?) {
transformInvocation?.inputs?.forEach {
it.directoryInputs.forEach {directoryInput->
with(directoryInput){
val dest = transformInvocation.outputProvider.getContentLocation(
name,
contentTypes,
scopes,
Format.DIRECTORY
)
file.copyTo(dest)
}
}
it.jarInputs.forEach { jarInput->
with(jarInput){
val dest = transformInvocation.outputProvider.getContentLocation(
name,
contentTypes,
scopes,
Format.JAR
)
file.copyTo(dest)
}
}
}
}
}
2.1.1 getName()
这个方法返回的就是我们的Transform名称,也就是会在Build的流程里会出现的:
这名字最终是如何构成的? 可以在gradle源码里有个TransformManager 的类,这个类负责管理所有的Transform的子类,可以找到一个getTaskNamePrefix 方法。会以tansform开头,之后拼接contentType,这个也就是Transform的输入类型,有Classes 和Resources 两种类型,最后就是会跟上我们这个Transform 的Name 了。
#TransformManager
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()
.collect(Collectors.joining("And")));
sb.append("With");
StringHelper.appendCapitalized(sb, transform.getName());
sb.append("For");
return sb.toString();
}
2.1.2 getInputTypes()
这个则是用来限定transform处理文件的类型,在对class文件进行处理时,返回的是TransformManager.CONTENT_CLASS ,而在对资源文件处理时,返回的是TransformManager.CONTENT_RESOURCES 。
除了CLASSES和RESOURCES,还有一些我们开发过程无法使用的类型,比如DEX文件,这些隐藏类型在一个独立的枚举类ExtendedContentType中,这些类型只能给Android编译器使用。
2.1.3 getScopes()
这个是指定需要处理哪种文件,也就是用来表明作用域。可以看下有哪些选项:
enum Scope implements ScopeType {
PROJECT(0x01),
SUB_PROJECTS(0x04),
EXTERNAL_LIBRARIES(0x10),
TESTED_CODE(0x20),
PROVIDED_ONLY(0x40),
@Deprecated
PROJECT_LOCAL_DEPS(0x02),
@Deprecated
SUB_PROJECTS_LOCAL_DEPS(0x08);
......
}
一般来说如果要处理所有class字节码的话,一般使用TransformManager.SCOPE_FULL_PROJECT ,也就是如下:
public static final Set<Scope> SCOPE_FULL_PROJECT =
Sets.immutableEnumSet(
Scope.PROJECT,
Scope.SUB_PROJECTS,
Scope.EXTERNAL_LIBRARIES);
2.1.4 inIncremental()
表示是否支持增量编译,关闭时就会进行全量编译,并且会删除上一次的输出内容。当我们开启增量编译的时候,input就包含了changed/removed/added/notchanged四种状态:
NOTCHANGED : 当前文件没有改变,不需处理,甚至复制操作都不用ADDED、CHANGED : 有修改文件,并输出给下一个任务REMOVED : outputProvider获取路径对应的文件被移除
2.1.5 transform()
在这个方法中里我们将每个jar包和class文件赋值到dest路径下,这个dest路径也就是下一个Transform的输入数据,在复制的过程中,我们就可以对jar包和class文件的字节码进行修改(ASM在这里飘过~) 。
处理后的class/jar包可以到/build/intermediates/transforms/HelloTransform/ 下查看,你会看到所有jar包都是123456递增着来的。可以看下获取输出路径的方法:
# IntermediateFolderUtils
public synchronized File getContentLocation(String name, Set<ContentType> types, Set<? super Scope> scopes, Format format) {
Preconditions.checkNotNull(name);
Preconditions.checkNotNull(types);
Preconditions.checkNotNull(scopes);
Preconditions.checkNotNull(format);
Preconditions.checkState(!name.isEmpty());
Preconditions.checkState(!types.isEmpty());
Preconditions.checkState(!scopes.isEmpty());
Iterator var5 = this.subStreams.iterator();
SubStream subStream;
do {
if (!var5.hasNext()) {
SubStream newSubStream = new SubStream(name, this.nextIndex++, scopes, types, format, true);
this.subStreams.add(newSubStream);
return new File(this.rootFolder, newSubStream.getFilename());
}
subStream = (SubStream)var5.next();
} while(!name.equals(subStream.getName()) || !types.equals(subStream.getTypes()) || !scopes.equals(subStream.getScopes()) || format != subStream.getFormat());
return new File(this.rootFolder, subStream.getFilename());
}
2.2 Transform的原理
介绍了如何使用Transfoem之后,我们再来看下它的原理(gradle插件7.0.2版本)。
首先我们来看下从Java源代码到apk的过程,如下图:
从这里我们可以清楚看到gradle的打包过程基本上是通过官方的Transform 来完成。而每个Transform 其实都是一个gradle task ,Android编译器中的TaskManager 将每个Transform 串连起来,第一个Transform 接收来自javac编译的结果,以及本地的第三方依赖还有asset目录下的resource 资源。然后这些编译的中间产物会在Transform 组成的链条上流动,每一个Tansform 会对class进行处理之后再传给下一个Transform 。
而我们自定的Transform 会插入到这个Tansform 链条的最前面,要优先于ProguardTransform 执行的,所以不会造成因为混淆而无法扫描到类信息。
2.2.1 TransformManager
在前面自定义plugin中调用registerTransform 对transform 进行注册时,实际上是放入了BaseExtension 类中的list数组里,然后是由TaskManager 调用了TransformManager 的addTransform 方法。这里TransformManager 管理了项目对应变体的所有Transform 对象。
我们来看下addTransform方法具体实现:
# TransformManager
public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(
@NonNull TaskFactory taskFactory,
@NonNull TransformVariantScope scope,
@NonNull T transform,
@Nullable PreConfigAction preConfigAction,
@Nullable TaskConfigAction<TransformTask> configAction,
@Nullable TaskProviderCallback<TransformTask> providerCallback) {
......
List<TransformStream> inputStreams = Lists.newArrayList();
String taskName = scope.getTaskName(getTaskNamePrefix(transform));
List<TransformStream> referencedStreams = grabReferencedStreams(transform);
IntermediateStream outputStream = findTransformStreams(
transform,
scope,
inputStreams,
taskName,
scope.getGlobalScope().getBuildDir());
......
transforms.add(transform);
return Optional.of(
taskFactory.register(
new TransformTask.CreationAction<>(
scope.getFullVariantName(),
taskName,
transform,
inputStreams,
referencedStreams,
outputStream,
recorder),
preConfigAction,
configAction,
providerCallback));
}
-
在getTaskNamePrefix 方法中会定义task的名字,前面也已经分析过了。 -
然后在grabReferencedStreams 方法中,对transform的数据输入,通过内部定义的引用型输入的Scope和ContentType两个维度进行过滤,可以看到grabReferencedStreams 方法里求取与streams 作用域和作用类型的交集来获取对应的流, 将其定义为我们需要的引用型流。 private List<TransformStream> grabReferencedStreams(@NonNull Transform transform) {
Set<? super Scope> requestedScopes = transform.getReferencedScopes();
......
List<TransformStream> streamMatches = Lists.newArrayListWithExpectedSize(streams.size());
Set<ContentType> requestedTypes = transform.getInputTypes();
for (TransformStream stream : streams) {
Set<ContentType> availableTypes = stream.getContentTypes();
Set<? super Scope> availableScopes = stream.getScopes();
Set<ContentType> commonTypes = Sets.intersection(requestedTypes,
availableTypes);
Set<? super Scope> commonScopes = Sets.intersection(requestedScopes, availableScopes);
if (!commonTypes.isEmpty() && !commonScopes.isEmpty()) {
streamMatches.add(stream);
}
}
return streamMatches;
}
-
之后在findTransformStreams 方法中,会根据定义的SCOPE和INPUT_TYPE,获取对应的消费型输入流,移除这一部分的消费性的输入流。为所有类型和范围创建单个组合输出流,并将其添加到下一次转换的可用流列表中。 private IntermediateStream findTransformStreams(
@NonNull Transform transform,
@NonNull TransformVariantScope scope,
@NonNull List<TransformStream> inputStreams,
@NonNull String taskName,
@NonNull File buildDir) {
......
Set<ContentType> requestedTypes = transform.getInputTypes();
consumeStreams(requestedScopes, requestedTypes, inputStreams);
Set<ContentType> outputTypes = transform.getOutputTypes();
File outRootFolder =
FileUtils.join(
buildDir,
StringHelper.toStrings(
AndroidProject.FD_INTERMEDIATES,
FD_TRANSFORMS,
transform.getName(),
scope.getDirectorySegments()));
IntermediateStream outputStream =
IntermediateStream.builder(
project,
transform.getName() + "-" + scope.getFullVariantName(),
taskName)
.addContentTypes(outputTypes)
.addScopes(requestedScopes)
.setRootLocation(outRootFolder)
.build();
streams.add(outputStream);
return outputStream;
}
-
最后将新创建的TransformTask 注册到TaskManager 中。
2.2.2 TransformTask
在这个类里我们看到最终Transform 的tansform方法 被调用,也就是在其对应的TaskAction方法中执行:
# TransformTask
@TaskAction
void transform(final IncrementalTaskInputs incrementalTaskInputs)
throws IOException, TransformException, InterruptedException {
final ReferenceHolder<List<TransformInput>> consumedInputs = ReferenceHolder.empty();
final ReferenceHolder<List<TransformInput>> referencedInputs = ReferenceHolder.empty();
final ReferenceHolder<Boolean> isIncremental = ReferenceHolder.empty();
final ReferenceHolder<Collection<SecondaryInput>> changedSecondaryInputs =
ReferenceHolder.empty();
isIncremental.setValue(transform.isIncremental() && incrementalTaskInputs.isIncremental());
......
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(
isIncremental.getValue())
: null)
.setIncrementalMode(isIncremental.getValue())
.build());
if (outputStream != null) {
outputStream.save();
}
return null;
}
});
}
到这里我们已经知道了Transform的数据流动原理、输入的类型和过滤机制。
2.3 Transform的增量与并发
学习了上面之后我们可以轻松的定义出一个Transform。可是每次编译transform 方法都会执行,就会遍历所有的class文件,会解压所有jar文件,然后重新压缩成所有的jar文件,这样就会拖慢编译的时间。如何解决,这里我们就用到了增量编译。
但不是每次的编译都可以增量编译,毕竟第一次编译或clean后重新编译directory.changedFiles 为空,需要做好区分经测试。如果不是增量编译,则清空output目录,然后按照前面的方式,逐个class/jar处理。如果是增量编译,则要检查每个文件的Status,Status分为四种NOTCHANGED /ADDED /CHANGED /REMOVED ,并且对四种文件的操作不尽相同。
可以来看下增量编译的代码实现,详细代码可以看下–>BaseTransform
override fun transform(transformInvocation: TransformInvocation) {
Log.log("transform start--------------->")
onTransformStart()
val outputProvider = transformInvocation.outputProvider
val context = transformInvocation.context
val isIncremental = transformInvocation.isIncremental
val startTime = System.currentTimeMillis()
if (!isIncremental){
outputProvider.deleteAll()
}
transformInvocation?.inputs?.forEach{input ->
input.directoryInputs.forEach{directoryInput ->
submitTask {
handleDirectory(directoryInput, outputProvider, context, isIncremental)
}
}
input.jarInputs.forEach{jarInput ->
submitTask {
handleJar(jarInput, outputProvider, context, isIncremental)
}
}
}
val taskListFeature = executorService.invokeAll(taskList)
taskListFeature.forEach{
it.get()
}
onTransformEnd()
Log.log("transform end--------------->" + "duration : " + (System.currentTimeMillis() - startTime) + " ms")
}
对输入为jar包类型处理:
private fun handleJar(jarInput: JarInput, outputProvider: TransformOutputProvider, context: Context, isIncremental: Boolean) {
val inputJar = jarInput.file
val outputJar = outputProvider.getContentLocation(
jarInput.name, jarInput.contentTypes,
jarInput.scopes, Format.JAR
)
if (isIncremental){
when(jarInput.status){
Status.NOTCHANGED -> {
}
Status.ADDED,Status.CHANGED -> {
}
Status.REMOVED -> {
if (outputJar.exists()){
FileUtils.forceDelete(outputJar)
}
return
}
else -> {
return
}
}
}
if (outputJar.exists()){
FileUtils.forceDelete(outputJar)
}
val modifiedJar = if (ClassUtils.isLegalJar(jarInput.file)) {
modifyJar(jarInput.file, context.temporaryDir)
} else {
Log.log("不处理: " + jarInput.file.absoluteFile)
jarInput.file
}
FileUtils.copyFile(modifiedJar, outputJar)
}
对输入为文件夹类型处理:
private fun handleDirectory(directoryInput: DirectoryInput, outputProvider: TransformOutputProvider, context: Context, isIncremental: Boolean) {
val inputDir = directoryInput.file
val outputDir = outputProvider.getContentLocation(
directoryInput.name, directoryInput.contentTypes,
directoryInput.scopes, Format.DIRECTORY
)
val srcDirPath = inputDir.absolutePath
val destDirPath = outputDir.absolutePath
val temporaryDir = context.temporaryDir
FileUtils.forceMkdir(outputDir)
if (isIncremental){
directoryInput.changedFiles.entries.forEach { entry ->
val inputFile = entry.key
when(entry.value){
Status.ADDED, Status.CHANGED ->{
modifyClassFile(inputFile, srcDirPath, destDirPath, temporaryDir)
}
Status.REMOVED -> {
val destFilePath = inputFile.absolutePath.replace(srcDirPath, destDirPath)
val destFile = File(destFilePath)
if (destFile.exists()){
destFile.delete()
}
}
Status.NOTCHANGED -> {
}
}
}
} else {
directoryInput.file.walkTopDown().filter { it.isFile }
.forEach { classFile ->
modifyClassFile(classFile, srcDirPath, destDirPath, temporaryDir)
}
}
}
根据这个就能为我们的编译插件提供增量的特性。
三、结语
通过本文,我们学习了如何自定义一个Gradle插件,如何定义一个Transform 以及Transform 的内部原理。学完这些还是不够的,跟之后要讲到的ASM结合起来,你就能利用字节码插桩技术为所欲为了。
参考:
深度探索 Gradle 自动化构建技术(四、自定义 Gradle 插件)
写个更牛逼的Transform | Plugin 进阶教程
Android Transform增量编译
Gradle+Transform+Asm自动化注入代码
Transform API
如何开发一款高性能的gradle transform
一起玩转Android项目中的字节码
深入理解Transform
Transform详解
|