继续创作,加速成长!这是我参与「日新计划 · 10 月更文挑战」的第5天,点击查看活动详情
前言
很高兴遇见你~
关于 Gradle 学习,我所了解的流程如下图:
在本系列的前 4 篇文章中,咱们了解了:
1、Groovy 语法
2、Gradle 常用 api,生命周期及 hook 点,Task 界说,依靠与挂接到构建流程的基本操作
3、自界说 Gradle 插件及实战演练
还不清楚上面这些知识点的朋友,建议先去阅览我创立的Gradle 学习专栏
下面我抛出一些问题,咱们能够考虑下🤔:
1、为了对 app 功能做一个全面的评价,咱们需求做 UI,内存,网络等方面的功能监控,怎么做?
2、发现某个第三方库有 bug ,用起来不爽,但又不想拿它的源码修正在重新编译,有什么好的办法?
3、我想在不修正源码的情况下,统计某个办法的耗时,对某个办法做埋点,怎么做?
为了完结上面的想法,或许咱们最开端的榜首反响:是能否经过 APT,反射,动态署理来完结?可是想来想去,这些计划都不能很好的满足上面的需求,而且,有些问题不能从 Java 源文件入手,咱们应该从 Class 字节码文件寻觅突破口
JVM 渠道上,修正、生成字节码无处不在,从 ORM 结构(如 Hibernate, MyBatis)到 Mock 结构(如 Mockito),再到 Java Web 中的常⻘树 Spring 家族,再到新式的 JVM 言语 Kotlin 编译器,还有大名鼎鼎的 cglib,都有字节码的身影
字节码相关技能的强大之处自然不必多说,而在 Android 开发中,无论是运用 Java 开发还是 Kotlin 开发,都是 JVM 渠道的言语,所以假如咱们在 Android 开发中,运用字节码技能做一下 hack,还能够天然地兼容 Java 和 Kotlin 言语
现在意图很明确,咱们便是要经过修正字节码的技能去处理上面的问题,那这和咱们今天要讲的 Gradle Transform 有什么关系呢?
接下来咱们就进入 Gradle Transform 的学习
一、Gradle Transform 介绍
Gradle Transform 是 AGP(Android Gradle Plugin )1.5 引进的特性,首要用于在 Android 构建进程中,在 Class→Dex 这个节点修正 Class 字节码。利用 Transform API,咱们能够拿到一切参与构建的 Class 文件,借助 Javassist 或 ASM 等字节码修正工具进行修正,插入自界说逻辑
一图胜千言:
虽然在 AGP 7.0 中 Transform 被标记为废弃了,但还能够运用,并不阻碍咱们的学习,可是会在 AGP 8.0 中移除。
后续文章我也会讲怎么适配运用新的 Api 去进行 Transform 的替换,因而咱们不必担心🍺
二、自界说 Gradle Transform
先不论细节,咱们直接完结一个自界说 Gradle Transform 在说,依照下面的进程,保姆式教程
完结一个 Transform 需求先创立 Gradle 插件,大致流程:自界说 Gradle 插件 -> 自界说 Transform -> 注册 Transform
假如你了解自界说 Gradle 插件,那么自界说 Gradle Transform 将会变得非常简略,不了解的去看我的这篇文章Gradle 系列 (三)、Gradle 插件开发
首先给咱们看一眼我项目初始化的一个装备:
能够看到:
1、AGP 版别:7.2.0
2、Gradle 版别:7.4
我的 AndroidStudio 版别:Dolphin | 2021.3.1
咱们需求对应好 AndroidStudio 版别所需的 AGP 版别,AGP 版别所需的 Gradle 版别,否则会呈现兼容性和各种未知的问题,对应关系能够去官网查询
别的咱们会发现,AGP 7.x 中 settings.gradle 和根 build.gradle 文件运用了一种新的装备办法,建议改回本来的装备办法,坑少😄:
//1、修正 settings.gradle
rootProject.name = "GradleTransformDemo"
include ':app'
//2、修正根 build.gradle
buildscript {
ext.kotlin_version = "1.7.20"
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.2.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
2.1、自界说 Gradle 插件
创立 Gradle 插件 Module:customtransformplugin,初始代码如下图:
留意:此插件我是运用 Kotlin 编写的,和之前 Groovy 编写插件的差异:
1、Kotlin 编写的插件能够直接写在 src/main/java
目录下,别的 AndroidStudio 对 Kotlin 多了很多扩展支撑,编写效率高
2、 Groovy 编写插件需求写在src/main/groovy
目录下
Transform 相关 Api 需求如下依靠:
implementation "com.android.tools.build:gradle-api:7.2.0"
可是上述并没有引进,是因为 AGP 相关 Api 依靠了它,根据依靠传递的特性,因而咱们能够引证到 Transform 相关 Api
2.2、自界说 Transform
初始代码如下图:
接着对其进行简略的修正:
package com.dream.customtransformplugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
/**
* function: 自界说 Transform
*/
class MyCustomTransform: Transform() {
/**
* 设置咱们自界说 Transform 对应的 Task 称号,Gradle 在编译的时分,会将这个称号经过一些拼接显示在操控台上
*/
override fun getName(): String {
return "ErdaiTransform"
}
/**
* 项目中会有各式各样格式的文件,该办法能够设置 Transform 接纳的文件类型
* 详细取值规模:
* CONTENT_CLASS:Java 字节码文件,
* CONTENT_JARS:jar 包
* CONTENT_RESOURCES:资源,包括 java 文件
* CONTENT_DEX:dex 文件
* CONTENT_DEX_WITH_RESOURCES:包括资源的 dex 文件
*
* 咱们能用的就两种:CONTENT_CLASS 和 CONTENT_JARS
* 其余几种仅 AGP 可用
*/
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
return TransformManager.CONTENT_CLASS
}
/**
* 界说 Transform 检索的规模:
* PROJECT:只检索项目内容
* SUB_PROJECTS:只检索子项目内容
* EXTERNAL_LIBRARIES:只检索外部库,包括当时模块和子模块本地依靠和长途依靠的 JAR/AAR
* TESTED_CODE:由当时变体所测验的代码(包括依靠项)
* PROVIDED_ONLY:本地依靠和长途依靠的 JAR/AAR(provided-only)
*/
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
return TransformManager.SCOPE_FULL_PROJECT
}
/**
* 表明当时 Transform 是否支撑增量编译 true:支撑 false:不支撑
*/
override fun isIncremental(): Boolean {
return false
}
/**
* 进行详细的检索操作
*/
override fun transform(transformInvocation: TransformInvocation?) {
printLog()
transformInvocation?.inputs?.forEach {
// 一、输入源为文件夹类型
it.directoryInputs.forEach {directoryInput->
//1、TODO 针对文件夹进行字节码操作,这个当地咱们就能够做一些狸猫换太子,偷天换日的事情了
//先对字节码进行修正,在仿制给 dest
//2、构建输出途径 dest
val dest = transformInvocation.outputProvider.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY
)
//3、将文件夹仿制给 dest ,dest 将会传递给下一个 Transform
FileUtils.copyDirectory(directoryInput.file,dest)
}
// 二、输入源为 jar 包类型
it.jarInputs.forEach { jarInput->
//1、TODO 针对 jar 包进行相关处理
//2、构建输出途径 dest
val dest = transformInvocation.outputProvider.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR
)
//3、将 jar 包仿制给 dest,dest 将会传递给下一个 Transform
FileUtils.copyFile(jarInput.file,dest)
}
}
}
/**
* 打印一段 log 日志
*/
fun printLog() {
println()
println("******************************************************************************")
println("****** ******")
println("****** 欢迎运用 ErdaiTransform 编译插件 ******")
println("****** ******")
println("******************************************************************************")
println()
}
}
2.3、注册 Transform
在 CustomTransformPlugin 中对 TransForm 进行注册,如下:
/**
* 自界说:CustomTransformPlugin
*/
class CustomTransformPlugin: Plugin<Project> {
override fun apply(project: Project) {
println("Hello CustomTransformPlugin")
//新增的代码
// 1、获取 Android 扩展
val androidExtension = project.extensions.getByType(AppExtension::class.java)
// 2、注册 Transform
androidExtension.registerTransform(MyCustomTransform())
}
}
ok,经过上面三步,一个最简略的自界说 Gradle Transform 插件现已完结了
2.4、上传插件到本地仓库
点击 publish
进行发布
假如你的项目多了如下内容,则证明发布成功了
2.5、作用验证
在根 build.gradle 进行插件依靠:
buildscript {
//...
repositories {
//...
//添加本地 maven 仓库
maven {
url('repo')
}
}
dependencies {
//...
//引进插件依靠
classpath "com.dream:customtransformplugin:1.0.0"
}
}
在 app 的 build.gradle 运用插件:
plugins {
//...
//运用插件
id 'CustomTransformPlugin'
}
同步一下项目,运行 app
装备阶段打印如下图:
履行阶段打印如下图:
这样咱们一个最简略的自界说 Gradle Transform 就完结了
别的需求留意:当你对自界说 Gradle Transform 做修正后想看作用,务必晋级插件的版别,重新发布,然后在根 build.gradle 中修正为新的版别,同步后在重新运行,否则 Gradle Transform 会不生效
消化一下,接下来咱们讲点 Transform 的细节
三、Transform 细节和相关 Api 介绍
3.1、Transform 数据活动
Transform 数据活动首要分为两种:
1、消费型 Transform :数据会输出给下一个 Transform
2、引证型 Transform :数据不会输出给下一个 Transform
3.1.1、消费型 Transform
如下图:
1、每个 Transform 其实都是一个 Gradle Task,AGP 中的 TaskManager 会将每个 Transform 串连起来
2、榜首个 Transform 会接纳:
1、来自 Javac 编译的结果
2、拉取到在本地的第三方依靠(jar,aar)
3、resource 资源(这儿的 resource 并非 Android 项目中的 res 资源,而是 assets 目录下的资源)
3、这些编译的中心产品,会在 Transform 组成的链条上活动,每个 Transform 节点能够对 Class 进行处理再传递给下一个Transform
4、咱们常⻅的混杂,Desugar 等逻辑,它们的完结都是封装在一个个 Transform 中,而咱们自界说的 Transform,会插入到这个Transform 链条的最前面
3.1.2、引证型 Transform
引证型 Transform 会读取上一个 Transform 输入的数据,而不需求输出给下一个Transform,例如 Instant Run 便是经过这种办法,查看两次编译之间的 diff 进行快速运行
ok,了解了 Transform 的数据活动,咱们回到自界说 Transform 的初始状态,如下:
class MyCustomTransform: Transform() {
override fun getName(): String {
return "ErdaiTransform"
}
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
return TransformManager.CONTENT_CLASS
}
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
return TransformManager.SCOPE_FULL_PROJECT
}
override fun isIncremental(): Boolean {
return false
}
override fun transform(transformInvocation: TransformInvocation?) {
super.transform(transformInvocation)
}
}
咱们重写了 Transform 的 5 个办法,接下来详细介绍下
3.2、getName
override fun getName(): String {
return "ErdaiTransform"
}
getName 办法首要是获取自界说 Transform 的称号,能够看到它接纳的是一个 String 字符串的类型,它的作用:
1、进行 Transform 唯一标识,一个运用内能够有多个 Transform,因而需求一个称号,方便后面调用
2、创立 Transform Task 命名时会用到它
经过源码验证一下,如下代码:
//TransformManager#addTransform
@NonNull
public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(
@NonNull TaskFactory taskFactory,
@NonNull VariantCreationConfig creationConfig,
@NonNull T transform,
@Nullable PreConfigAction preConfigAction,
@Nullable TaskConfigAction<TransformTask> configAction,
@Nullable TaskProviderCallback<TransformTask> providerCallback) {
//...
List<TransformStream> inputStreams = Lists.newArrayList();
String taskName = creationConfig.computeTaskName(getTaskNamePrefix(transform), "");
//...
}
//TransformManager#getTaskNamePrefix
@VisibleForTesting
@NonNull
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 Task 的命名规则能够了解为:
transform${inputType1.name}And${inputType2.name}With${transform.name}For${variantName}
经过咱们上面生成的 Transform Task 也能够验证这一点:
> Task :app:transformClassesWithErdaiTransformForDebug
3.3、getInputTypes
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
return TransformManager.CONTENT_CLASS
}
getInputTypes 办法首要用于获取输入类型,能够看到它接纳一个 ContentType 的 Set 调集,表明它答应输入多种类型。上述返回值咱们运用了 TransformManager 内置的输入类型,咱们也能够自界说,如下:
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
//实践 TransformManager.CONTENT_CLASS 内部便是对它的封装
return ImmutableSet.of(QualifiedContent.DefaultContentType.CLASSES)
}
ContentType 是一个接口,表明输入或输出内容的类型,它有两个完结枚举类 DefaultContentType
和 ExtendedContentType
。可是,咱们在自界说 Transform 时只能运用 DefaultContentType 中界说的枚举,即 CLASSES
和 RESOURCES
两种类型,其它类型仅供 AGP 内置的 Transform 运用
enum DefaultContentType implements ContentType {
// Java 字节码,包括 Jar 文件和由源码编译发生的
CLASSES(0x01),
// Java 资源
RESOURCES(0x02);
//...
}
// 加强类型,自界说 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),
;
//...
}
自界说 Transform 咱们能够在两个方位界说 ContentType:
1、Set getInputTypes(): 指定输入内容类型,答应经过 Set 调集设置输入多种类型
2、Set getOutputTypes(): 指定输出内容类型,默许取 getInputTypes() 的值,答应经过 Set 调集设置输出多种类型
看一眼 TransformManager 给咱们内置的 ContentType 调集,常用的是 CONTENT_CLASS :
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_DEX = ImmutableSet.of(ExtendedContentType.DEX);
public static final Set<ContentType> CONTENT_DEX_WITH_RESOURCES =
ImmutableSet.of(ExtendedContentType.DEX, RESOURCES);
3.4、getScopes
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
return TransformManager.SCOPE_FULL_PROJECT
}
getScopes 办法首要用来界说检索的规模,告知 Transform 需求处理哪些输入文件,能够看到它接纳的是一个 Scope 的 Set 调集。上述返回值咱们运用了 TransformManager 内置的 Scope 调集,假如不满足你的需求,你能够自界说,如下:
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
//实践 TransformManager.SCOPE_FULL_PROJECT 便是对它的封装
return ImmutableSet.of(QualifiedContent.Scope.PROJECT,
QualifiedContent.Scope.SUB_PROJECTS,
QualifiedContent.Scope.EXTERNAL_LIBRARIES)
}
Scope 是一个枚举类:
enum Scope implements ScopeType {
//只检索项目内容
PROJECT(0x01),
//只检索子项目内容
SUB_PROJECTS(0x04),
//只检索外部库,包括当时模块和子模块本地依靠和长途依靠的 JAR/AAR
EXTERNAL_LIBRARIES(0x10),
//由当时变体所测验的代码(包括依靠项)
TESTED_CODE(0x20),
//本地依靠和长途依靠的 JAR/AAR(provided-only)
PROVIDED_ONLY(0x40),
}
自界说 Transform 能够在两个方位界说 Scope:
1、Set getScopes() 消费型输入内容领域: 此规模的内容会被消费,因而当时 Transform 必须将修正后的内容仿制到 Transform 的中心目录中,否则无法将内容传递到下一个 Transform 处理
2、Set getReferencedScopes() 指定引证型输入内容领域: 默许是空调集,此规模的内容不会被消费,因而不需求仿制传递到下一个 Transform,也不答应修正。
看一眼 TransformManager 给咱们内置的 Scope 调集,常用的是 SCOPE_FULL_PROJECT 。需求留意,Library 模块注册的 Transform 只能运用 Scope.PROJECT
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);
3.5、isIncremental
override fun isIncremental(): Boolean {
return false
}
isIncremental 办法首要用于获取是否是增量编译,true:是, false:否。一个自界说 Transform 应该尽或许支撑增量编译,这样能够节省一些编译的时刻和资源,这个咱们一会单独讲
3.6、transform
override fun transform(transformInvocation: TransformInvocation?) {
super.transform(transformInvocation)
}
transform 办法首要用于对输入的数据做检索操作,它是 Transform 的核心办法,办法的参数是 TransformInvocation,它是一个接口,供给了一切与输入输出相关的信息:
public interface TransformInvocation {
//...
// 消费型输入内容
Collection<TransformInput> getInputs();
// 引证型输入内容
Collection<TransformInput> getReferencedInputs();
//...
// 输出信息
TransformOutputProvider getOutputProvider();
// 是否增量构建
boolean isIncremental();
}
1、isIncremental(): 当时 Transform 使命是否增量构建;
2、getInputs(): 获取 TransformInput 方针,它是消费型输入内容,对应于 Transform#getScopes() 界说的规模;
3、getReferencedInputs(): 获取 TransformInput 方针,它是引证型输入内容,对应于 Transform#getReferenceScope() 界说的内容规模;
4、getOutPutProvider(): TransformOutputProvider 是对输出文件的抽象。
输入内容 TransformInput 由两部分组成:
1、DirectoryInput 调集: 以源码办法参与构建的输入文件,包括完好的源码目录结构及其间的源码文件;
2、JarInput 调集: 以 jar 和 aar 依靠办法参与构建的输入文件,包括本地依靠和长途依靠。
输出内容 TransformOutputProvider 有两个首要功能:
1、deleteAll(): 当 Transform 运行在非增量构建模式时,需求删去上一次构建发生的一切中心文件,能够直接调用 deleteAll() 完结;
2、getContentLocation(): 获得指定规模+类型的输出方针途径。
四、Transform 的增量与并发
到此为止,看起来 Transform 用起来也不难,可是,假如直接这样运用,会大大拖慢编译时刻,为了处理这个问题,摸索了一段时刻,也借鉴了Android 编译器中 Desugar 等几个 Transform 的完结,发现咱们能够运用增量编译,并且上面 transform 办法遍历处理每个jar/class 的流程,其实能够并发处理,加上一般编译流程都是在 PC 上,所以咱们能够尽量敲诈机器的资源。
上面也讲了,想要敞开增量编译,只需求重写 Transform 的这个办法,返回 true 即可:
override fun isIncremental(): Boolean {
//敞开增量编译
return true
}
嗯,没了,现已敞开了😄。有这么简略就好了,言归正传:
1、假如不是增量编译,则会清空 output 目录,然后依照前面的办法,逐一处理 class/jar 。
2、假如是增量编译,则会查看每个文件的 Status,Status 分四种:
public enum Status {
// 未修正,不需求处理,也不需求仿制操作
NOTCHANGED,
// 新增,正常处理并仿制给下一个使命
ADDED,
// 已修正,正常处理并仿制给下一个使命
CHANGED,
// 已删去,需同步移除 OutputProvider 指定的方针文件
REMOVED;
}
根据不同的 Status 处理逻辑即可
3、完结增量编译后,咱们最好也支撑并发编译,并发编译的完结并不杂乱,原理:对上面处理单个 class/jar 的逻辑进行并发处理,最后阻塞等待一切使命完毕即可
4.1、自界说 Tranform 模版
整个 Transform 的核心进程是有固定套路的,模板流程引进诗与远方的一张图:
接下来,咱们就依照上面这张图,来处理 Transform 的增量和并发,并封装一套通用的模版代码,下面模版写了详细的注释:
留意:WaitableExecutor 在 AGP 7.0 中现已引证不到了,因而咱们需求手动添加WaitableExecutor源码
abstract class BaseCustomTransform(private val enableLog: Boolean) : Transform() {
//线程池,可提升 80% 的履行速度
private var waitableExecutor: WaitableExecutor = WaitableExecutor.useGlobalSharedThreadPool()
/**
* 此办法供给给上层进行字节码插桩
*/
abstract fun provideFunction(): ((InputStream, OutputStream) -> Unit)?
/**
* 上层可重写该办法进行文件过滤
*/
open fun classFilter(className: String) = className.endsWith(SdkConstants.DOT_CLASS)
/**
* 默许:获取输入的字节码文件
*/
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
return TransformManager.CONTENT_CLASS
}
/**
* 默许:检索整个项意图内容
*/
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
return TransformManager.SCOPE_FULL_PROJECT
}
/**
* 默许敞开增量编译
*/
override fun isIncremental(): Boolean {
return true
}
/**
* 对输入的数据做检索操作:
* 1、处理增量编译
* 2、处理并发逻辑
*/
override fun transform(transformInvocation: TransformInvocation) {
super.transform(transformInvocation)
log("Transform start...")
//输入内容
val inputProvider = transformInvocation.inputs
//输出内容
val outputProvider = transformInvocation.outputProvider
// 1. 子类完结字节码插桩操作
val function = provideFunction()
// 2. 不是增量编译,删去一切旧的输出内容
if (!transformInvocation.isIncremental) {
outputProvider.deleteAll()
}
for (input in inputProvider) {
// 3. Jar 包处理
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. 增量编译中处理 Jar 包逻辑
when (jarInput.status ?: Status.NOTCHANGED) {
Status.NOTCHANGED -> {
// Do nothing.
}
Status.ADDED, Status.CHANGED -> {
// Do transform.
waitableExecutor.execute {
doTransformJar(inputJar, outputJar, function)
}
}
Status.REMOVED -> {
// Delete.
FileUtils.delete(outputJar)
}
}
} else {
// 3.2 非增量编译中处理 Jar 包逻辑
waitableExecutor.execute {
doTransformJar(inputJar, outputJar, function)
}
}
}
// 4. 文件夹处理
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. 增量编译中处理文件夹逻辑
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.
waitableExecutor.execute {
doTransformFile(inputFile, outputFile, function)
}
}
Status.REMOVED -> {
// Delete
FileUtils.delete(outputFile)
}
}
}
} else {
// 4.2. 非增量编译中处理文件夹逻辑
// Traversal fileTree (depthFirstPreOrder).
for (inputFile in FileUtils.getAllFiles(inputDir)) {
waitableExecutor.execute {
val outputFile = concatOutputFilePath(outputDir, inputFile)
if (classFilter(inputFile.name)) {
doTransformFile(inputFile, outputFile, function)
} else {
// Copy.
Files.createParentDirs(outputFile)
FileUtils.copyFile(inputFile, outputFile)
}
}
}
}
}
}
waitableExecutor.waitForTasksWithQuickFail<Any>(true)
log("Transform end...")
}
/**
* Do transform Jar.
*/
private fun doTransformJar(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 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 concatOutputFilePath(outputDir: File, inputFile: File) = File(outputDir, inputFile.name)
/**
* log 打印
*/
private fun log(logStr: String) {
if (enableLog) {
println("$name - $logStr")
}
}
}
上述模版给咱们做了很多工作: Trasform 的输入文件遍历、加解压、增量,并发等,咱们只需求专注字节码文件的修正即可
五、自界说模版运用
ok,接下来修正自界说 Gradle Transform 的代码:
package com.dream.customtransformplugin
import java.io.InputStream
import java.io.OutputStream
/**
* function: 自界说 Transform
*/
class MyCustomTransform: BaseCustomTransform(true) {
override fun getName(): String {
return "ErdaiTransform"
}
/**
* 此办法能够运用 ASM 或 Javassist 进行字节码插桩
* 现在只是一个默许完结
*/
override fun provideFunction() = { ios: InputStream, zos: OutputStream ->
zos.write(ios.readAllBytes())
}
}
是不是瞬间清新了很多,发布一个新的插件版别,修正根 build.gradle 插件的版别,同步后重新运行 app,作用如下:
六、总结
本篇文章咱们首要介绍了:
1、Gradle Transform 是什么?
简略的了解:咱们能够自界说 Gradle Transform 修正字节码文件完结编译插桩
2、运用 Kotlin 编写自界说 Gradle Transform 的流程,留意和 Groovy 编写插件的差异
1、Kotlin 编写插件可直接写在 src/main/java 目录下
2、Groovy 编写插件需写在 src/main/groovy 目录下
3、介绍了 Transform 的数据活动和自界说 Gradle Transform 完结的相关 Api
4、介绍了 Transform 的增量与并发,并封装了一个模版,简化咱们自界说 Gradle Transform 的运用
别的,本篇文章,咱们只是讲了 Gradle Transform 简略运用,还没有做详细的插桩逻辑,因而前言中的问题暂时还处理不了
预知后事怎么,请听下回分解
好了,本篇文章到这儿就完毕了,希望能给你带来协助 🤝
Github Demo 地址 , 咱们能够结合 demo 一同看,作用杠杠滴🍺
感谢你阅览这篇文章
参阅和推荐
Gradle 系列(8)其实 Gradle Transform 便是个纸老虎
Gradle Transform + ASM 探索
Android Gradle 插件版别说明
你的点赞,评论,是对我巨大的鼓励!
欢迎重视我的大众号: sweetying ,文章更新可榜首时刻收到
假如有问题,大众号内有加我微信的进口,在技能学习、个人成长的道路上,咱们一同前进!