目前,使用 AGP Transform API 进行字节码插桩已经非常普遍了,例如 Booster、神策等框架中都有 Transform 的影子。Transform 听起来很高大上,其本质就是一个 Gradle Task。在这篇文章里,我将带你理解 Transform 的工作机制、使用方法和核心源码解析,并通过一个 Demo 帮助你融会贯通。
这篇文章是全面掌握 Gradle 构建系统系列的第 4 篇:
请点赞加关注,你的支持对我非常重要,满足下我的虚荣心。
🔥 Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,欢迎跟着我一起成长。(联系方式在 GitHub)
Transform API 是 Android Gradle Plugin 1.5 就引入的特性,主要用于在 Android 构建过程中,在 Class→Dex 这个节点修改 Class 字节码。利用 Transform API,我们可以拿到所有参与构建的 Class 文件,借助 Javassist 或 ASM 等字节码编辑工具进行修改,插入自定义逻辑。一般来说,这些自定义逻辑是与业务逻辑无关的。
使用 Transform 的常见的应用场景有:
先大概了解下 Transform 的工作机制:
app/build/intermediates/transform/[Transform Name]/[Variant]
文件夹中;了解 Transform 的基本工作机制后,我们先来看 Transform 的核心 API。这里仅列举出 Transform 抽象类中最核心的方法,有几个次要的方法后面再说。
com.android.build.api.transform.java
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; } }
ContentType 是一个枚举类接口,表示输入或输出内容的类型,在 AGP 中定义了 DefaultContentType
和 ExtendedContentType
两个枚举类。但是,我们在自定义 Transform 时只能使用 DefaultContentType 中定义的枚举,即 CLASSES
和 RESOURCES
两种类型,其它类型仅供 AGP 内置的 Transform 使用。
自定义 Transform 需要在两个位置定义内容类型:
ExtendedContentType.java
// 加强类型,自定义 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
enum DefaultContentType implements ContentType { // Java 字节码,包括 Jar 文件和由源码编译产生的 CLASSES(0x01), // Java 资源 RESOURCES(0x02); }
在 TransformManager 中,预定义了一部分内容类型集合,常用的是 CONTENT_CLASS 操作 Class。
TransformManager.java
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);
ScopeType 也是一个枚举类接口,表示输入内容的范畴。在 AGP 中定义了 InternalScope
和 Scope
两个枚举类。但是,我们在自定义 Transform 只能使用 Scope 中定义的枚举,其它类型仅供 AGP 内置的 Transform 使用。
Transform 需要在两个位置定义输入内容范围:
InternalScope.java
// 内部使用的作用域,自定义 Transform 无法使用 public enum InternalScope implements QualifiedContent.ScopeType { MAIN_SPLIT(0x10000), LOCAL_DEPS(0x20000), FEATURES(0x40000), ; }
QualifiedContent.java
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
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);
transform() 是实现 Transform 的核心方法,方法的参数是 TransformInvocation,它提供了所有与输入输出相关的信息:
public interface TransformInvocation { Context getContext(); // 消费型输入内容 Collection<TransformInput> getInputs(); // 引用型输入内容 Collection<TransformInput> getReferencedInputs(); // 额外输入内容 Collection<SecondaryInput> getSecondaryInputs(); // 输出信息 TransformOutputProvider getOutputProvider(); // 是否增量构建 boolean isIncremental(); }
输入内容 TransformInput 由两部分组成:
输入内容信息 TransformOutputProvider 有两个功能:
TransformOutputProvider.java
public interface TransformOutputProvider { // 删除所有中间文件 void deleteAll() // 获取指定范围+类型的目标路径 File getContentLocation(String name, Set<QualifiedContent.ContentType> types, Set<? super QualifiedContent.Scope> scopes, Format format); }
获取输入内容对应的输出路径:
for (input in transformInvocation.inputs) { for (jarInput in input.jarInputs) { // 输出路径 val outputJar = outputProvider.getContentLocation( jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR ) } }
任何构建系统都会尽量避免重复执行相同工作,Transform 也不例外。虽然增量构建并不是必须的,但作为一个合格的 Transform 实现应该具备增量能力。
1、增量模式标记位: Transform API 有两个增量标志位,不要混淆:
2、Task 增量模式与 Transform 增量模式的区别: Task 增量模式与 Transform 增量模式的区别在于,Task 增量执行时会跳过整个 Task 的动作列表,而 Transform 增量执行依然会执行 TransformTask,但输入内容会增加变更内容信息。
3、增量模式的输入: 增量模式下的所有输入都是带状态的,需要根据这些状态来做不同的处理,不需要每次所有流程都重新来一遍。比如新增的输入就需要处理,而未修改的输入就不需要处理。Transform 定义了四个输入文件状态:
com.android.build.api.transform.Status.java
public enum Status { // 未修改,不需要处理,也不需要复制操作 NOTCHANGED, // 新增,正常处理并复制给下一个任务 ADDED, // 已修改,正常处理并复制给下一个任务 CHANGED, // 已删除,需同步移除 OutputProvider 指定的目标文件 REMOVED; }
在 BaseExtension 中维护了一个 Transform 列表,自定义 Transform 需要注册才能生效,而且还支持额外设置 TransformTask 的依赖。
BaseExtension.kt
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:
// 获取 Android 扩展 val androidExtension = project.extensions.getByType(BaseExtension::class.java) // 注册 Transform,支持额外增加依赖 androidExtension.registerTransform(ToastTransform(project)/* 支持增加依赖*/)
提示: 为了提高编译效率,可以判断 Variant 为 release 类型才注册 Transform,也可以通过重写 Transform#applyToVariant() 来决定是否执行 Transform。
这一节我们来分析 Transform 相关核心源码,这里我们引用的是 Android Gradle Plugin 7.1.0 版本的源码。
Project 的构建逻辑由一系列 Task 的组成,每个 Task 负责完成一个基本的工作,例如 Javac 编译 Task。Transform 也是依靠 Task 执行的,在配置阶段,Gradle 会为注册的 Transform 创建对应的 Task。
提示: 说 “创建” 可能不太严谨,TransformManager 使用 register 懒创建的方式注册 Task,其实还没有创建 Task 实例。我们不要复杂化了,就说创建吧。
而 Task 的依赖关系是通过 TransformTask 的输入输出关系隐式确定的,TransformManager 通过 TransformStream 链接各个 TransformTask 的输入输出,进而控制 Transform 的依赖关系顺序。
LibraryTaskManager.java
@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 标记为过时了。
BaseExtension.kt
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)) } }
调用链概要:
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) } }
Transform#getName() 会用于构造 Task Name,命名格式为 transform[InputTypes]With[name]For[Configuration]
,例如 transformClassed。这块源码体现在 TransformManager 中创建 Task 的位置:
TransformManager.java
// 创建 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(); }
TransformTask 通过 @Input 和 @OutputDirectory 等注解,将 Transform API 关联到 Task 的输入输出上:
TransformTask.java
public abstract class TransformTask extends StreamBasedTask { ... @Input public Set<QualifiedContent.ContentType> getInputTypes() { return transform.getInputTypes(); } @OutputDirectory @Optional public abstract DirectoryProperty getOutputDirectory(); }
每个 Task 内部都保持了一个 Action 列表 actions
,执行 Task 就是按顺序执行这个列表,对于自定义 Task,可以通过 @TaskAction
注解添加默认 Action。
TransformTask.java
@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()); ... }
Library 模块仅只支持使用 Scope.PROJECT 作用域:
LibraryTaskManager.java
// 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)); }
上一节我们探讨了 Transform 的基本工作机制,第 3 节和第 4 节我们来实现一个 Transform Demo。Transform 的核心代码在 transform() 方法中,我们要做的就是遍历输入文件,再把修改后的文件复制到目标路径中,对于 JarInputs 还有一次解压和压缩。更进一步,再考虑增量编译的情况。
因此,整个 Transform 的核心过程是有固定套路,模板流程图如下:
—— 图片引用自 https://rebooters.github.io/2020/01/04/Gradle-Transform-ASM-探索/
我们把整个流程图做成一个抽象模板类,子类需要重写 provideFunction()
方法,从输入流读取 Class 文件,修改完字节码后再写入到输出流。甚至不需要考虑 Trasform 的输入文件遍历、加解压、增量等,舒服!
BaseCustomTransform.kt
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. for (inputFile in FileUtils.getAllFiles(inputDir)) { // Traversal fileTree (depthFirstPreOrder). if (classFilter(inputFile.name)) { val outputFile = concatOutputFilePath(outputDir, inputFile) doTransformFile(inputFile, outputFile, function) } } } } } 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 && classFilter(entry.name)) { zos.putNextEntry(ZipEntry(entry.name)) // Apply transform function. applyFunction(zis, zos, function) } 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") } } }
现在,我手把手带你基于 BaseCustomTransform 实现一个 Transform Demo。示例代码我已经上传到 Github · DemoHall · HelloTransform。有用请给个免费的 Star 支持下。
Demo 效果很简单:
首先,我们先搭建工程的整体框架,再来编写核心的 Transform 逻辑。我们选择自定义 Gradle 插件来承载 Transform 的逻辑,可维护性更好。关于自定义 Gradle 插件的步骤具体见上一篇文章《手把手带你自定义 Gradle 插件》,此处不展开。
提示: 提醒一下,并不是说一定要由 Gradle 插件来承载,你直接在 .gradle 文件中实现也是 OK 的。
插件实现类如下:
ToastPlugin.kt
class ToastPlugin : Plugin<Project> { override fun apply(project: Project) { // 获取 Android 扩展 val androidExtension = project.extensions.getByType(BaseExtension::class.java) // 注册 Transform,支持额外增加依赖 androidExtension.registerTransform(ToastTransform(project)/* 支持增加依赖*/) } }
将我们实现的 BaseCustomTransform 模板类复制到工程下,再实现一个子类:
ToastTransform.kt
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 信息,修改后写入到输出流。
使用 Javassist API 从输入流加载数据,在匹配到 onCreate() 方法后检查是否声明 @Hello 注解。是则在该方法末尾织入一句 Toast:Hello Transform。本文重点不是 Javassist,此处就不展开了。
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() }
sample 模块 build.gradle
apply plugin: 'com.pengxr.toastplugin'
HelloActivity.kt
class HelloActivity : AppCompatActivity() { @Hello override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_hello) } }
完成以上步骤后,编译运行程序。可以在 Build Output 看到以下输出,HelloActivity 启动时会弹出 Toast HelloTransform,说明织入成功。
... 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
从 AGP 7.0 开始,Transform API 已经被废弃了。是的,就是卷,而且这次直接是降维打击。以前 Transform 是 AGP 的特性,现在 Gradle 也来整 Transform,不过换了个名字,叫 —— TransformAction。
那么,我们还有必要学 AGP Transform API 吗?如果你现在涉足字节码插桩这块,你建议你还是学以下:
例如,以下是 Gradle 官方文档的演示代码,是不是套路差不多?
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() } } } }
本文的示例代码已上传到 https://github.com/pengxurui/DemoHall,请 Star 支持。关注我,带你了解更多,我们下次见。
你的点赞对我意义重大!微信搜索公众号 [彭旭锐],希望大家可以一起讨论技术,找到志同道合的朋友,我们下次见!