Gradle 相关总结
APT 和 AGPTransform 区别
Gradle+Transform+Asm自动化注入代码
Android 360加固+Walle多渠道自动化打包上传蒲公英
最近将公司的项目进行重构,将原本的模块化进行了组件化,在这个过程中遇到了很多,最典型的就是如何去初始化其他组件,比如:消息组件,而组件化最主要的是其他组件能够单独运行和集成到壳工程,也就是说业务组件又多种形态,那么有个问题就是怎么去初始化业务组件,业务组件在单独运行时能将初始化可以自己的在Application中去做初始化,而当成是Library时就需要将宿主工程的Application下发给组件,怎么下发宿主工程的上下文到组件中?下面有几种:
- 在BaseApp中直接初始化,但是这样耦合度就非常高了,已经背离了组件化的初衷;
- 在公共组件中定义IComponent接口,并通过配置(注解\文件\SPI)实现类的全类名,然后通过反射实现业务组件的初始化,实际上SPI底层是通过通过解析文件得到类名通过反射实现的;
- 在Manifest中配置,然后解析Manifest文件,读取到类的全路径,然后通过反射实现组件初始化;
上面是我目前所知道组件初始化的方式虽然能够解决初始化问题,但是都存在缺点,那么除了上面所说的方式,我们是否还有更好的方式去做到解耦合并且不会造成性能损耗呢?
在编译时,扫描即将打包到apk中的所有类的字节码,将所有组件类收集起来,通过修改字节码的方式生成注册代码到组件管理类中,从而实现编译时自动注册的功能,不用再关心项目中有哪些组件类了。
特点:不需要注解,不会增加新的类;性能高,不需要反射,运行时直接调用组件的构造方法;能扫描到所有类,不会出现遗漏,而且还可以添加组件优先排序
。怎么实现呢?
AGP Transform
如果大家了解过apk打包的过程那么一定会知道Android 提供的Transform API
,在Android apk打包过程中会利用 Transform
去完成每一部分的操作,并且会有输入和输出,比如DexTransform,是将class字节码转换成dex文件,那么输入就是class字节码,而输出就是.dex文件,ProguardTransform则是完成混淆的,实际上Transform 是Android 提供的一种特殊Task,Task也是有输入和输出。
AGP Transform API
是从Gradle 1.5.0版本之后提供的,它允许第三方在打包Dex文件之前的编译过程中修改字节码,在自定义插件中注册的Transform会在ProguardTransform
和DexTransform
之前执行,实际上Transform
是Android一种特殊的Task
,自定义的Transform
是会在自带的Transform
之前执行,所以自动注册的Transform
不需要考虑混淆的情况。
APK 打包流程
我们平时在开发的过程中,每天在Android Studio Run项目,Android Studio就会将apk自动安装到手机上了,那么这中间都经历过哪些流程呢,来看看官方的项目构建流程图
如图所示,典型 Android 应用模块的构建流程通常按照以下步骤执行:
*1、 编译器将源代码转换成 DEX 文件(Dalvik 可执行文件),并将其他所有内容转换成编译后的资源。
2、 APK 打包器将 DEX 文件和编译后的资源合并到一个 APK 中。不过,在将应用安装并部署到 Android 设备之前,必须先为 APK 签名。
3、APK 打包器使用调试或发布密钥库为 APK 签名:
- 如果您构建的是调试版应用(即专用于测试和分析的应用),则打包器会使用调试密钥库为应用签名。Android Studio 会自动使用调试密钥库配置新项目。
- 如果您构建的是打算对外发布的发布版应用,则打包器会使用发布密钥库为应用签名。要创建发布密钥库,请参阅在 Android Studio 中为应用签名。
4、 在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,以减少其在设备上运行时所占用的内存。
可能从图中并不会看出什么来,实际上对于Java编程语言来说,这个过程要从Java源代码到apk,那么我们来看一张图:
这张图就非常的清晰了,gradle打包过程中基本上是通过官方提供的Transform完成的,文章开始我就说了自动注入就是通过自定义Transform并将自定义Transform注册到自定义gradle插件中,而却我们自定义的Transform是优先于ProguardTransform执行的,所以不会造成因为混淆而无法扫描到类信息。
自定义Gradle 插件
Gradle官方文档目前定义插件只有3中方式:
Build script
,即:脚本插件
,直接在构建脚本(build.gradle)中直接写插件的代码,编译器会自动将插件编译并添加到构建脚本的classpath中。但是该插件在构建脚本之外是不可见的,所以不能在定义它的构建脚本之外重用该插件。buildSrc project
,执行Gradle时会将根目录下的buildSrc目录作为插件源码目录进行编译,并将编译结果加入到构建脚本的classpath中,所以对整个项目是可用的,方便调试插件。Standalone project
,在独立项目中开发插件,然后将项目打成jar包,发布到本地或者maven服务器上,可在多个项目间复用,不好调试插件。最后,除了第一种方式,后面两种方式都是可以发布到本地或者maven服务器上,提供给其他项目使用的。所以我推荐
buildSrc project
这种方式开发插件。
我最后选择的是 buildSrc project
开发插件。
配置自定义gradle 插件的环境
1、首先在工程下新建一个java Libray项目,把其他无用的资源文件和目录删掉就保留src目录和build.gradle文件;
2、在main目录下新建resources/MATE-INF/gradle-plugins目录,如:
并在gradle-plugins目下新建xxx.properties
文件,而xxx
是可以随意定义名称,而这个名称(xxx)就是你的插件的名称,以后要引用该插件你可以通过 apply plugin: 'xxx' 方式引用插件。
当然除了这种配置还有另一中比较简单的配置方式:
apply plugin: 'java-gradle-plugin'
gradlePlugin {
plugins {
create('compoentPlugin') {
id = 'xxx'
implementationClass = 'com.github.plugin.ModuleComponentPluginKt'
}
}
}
gradle 中可以这样定义Plugin ,并不一定在resources/META-INF/gradle-plugins/xxx.properties中定义插件。
3、xxx.properties文件中的内容就是
implementation-class=com.github.plugin.ModuleComponentPluginKt
implementation-class是固定写法,而com.github.plugin.ModuleComponentPluginKt就是你的自定义插件类的全类名,同一个项目中还可以定义多个插件,你可以按照功能分插件引入项目。
4、build.gradle配置
apply plugin: 'groovy'
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'kotlin-android-extensions'
buildscript {
ext.kotlin_version = '1.3.50'
repositories {
mavenCentral()
jcenter()
google()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
sourceSets {
main {
groovy {
srcDir '../buildSrc/src/main/groovy'
}
java {
srcDir "../buildSrc/src/main/java"
}
kotlin {
srcDir "../buildSrc/src/main/kotlin"
}
resources {
srcDir '../buildSrc/src/main/resources'
}
}
}
dependencies {
repositories {
mavenCentral()
jcenter()
google()
}
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation gradleApi()
implementation localGroovy()
implementation group: 'org.ow2.asm', name: 'asm', version: '7.1'
implementation group: 'org.ow2.asm', name: 'asm-commons', version: '7.1'
implementation 'com.android.tools.build:gradle:3.4.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}
sourceCompatibility = "8"
targetCompatibility = "8"
因为我的插件是使用的Kotlin编写,所以这build.gradle的配置会有kotlin的配置,以及asm的依赖等等相关。
Transform Api Android 提供的,所以你必须引入这个依赖:implementation 'com.android.tools.build:gradle:3.4.2'
开发gradle 插件
正如xxx.properties文件中定义的全类名,所以在com.github.plugin包下
ModuleComponentPluginKt类,让ModuleComponentPluginKt实现至org.gradle.api.Plugin接口,代码如下:
class ModuleComponentPluginKt : Plugin<Project> {
private lateinit var mProject: Project
override fun apply(project: Project) {
this.mProject = project
KLogger.inject(project.logger)
KLogger.e("自定义插件ModuleComponentPluginKt")
PluginInitializer.initial(project)
if (project.plugins.hasPlugin(AppPlugin::class.java)) {
// 监听每个任务的执行时间
project.gradle.addListener(BuildTimeListener())
val android = project.extensions.getByType(AppExtension::class.java)
//主要操作就是收集满足条件的类
android.registerTransform(ScannerComponentTransformKt())
//收集完毕,在这里完成代码的织入
android.registerTransform(ScannerAfterTransformKt())
}
}
}
在ModuleComponentPluginKt 类中,通过project获取到AppExtension并调用registerTransform方法将我们自定义的Transform注册到AppExtension,而AppExtension就是application plugins。也就是App module的apply plugin: 'com.android.application'插件为com.android.application。
在ModuleComponentPluginKt 中还有 PluginInitializer.initial(project)是什么意思呢?这个也比较重要,代码如下:
object PluginInitializer {
fun initial(project: Project) {
val hasAppPlugin = project.plugins.hasPlugin(AppPlugin::class.java)
val hasLibPlugin = project.plugins.hasPlugin(LibraryPlugin::class.java)
if (!hasAppPlugin && !hasLibPlugin) {
throw GradleException("Component: The 'com.android.application' or 'com.android.library' plugin is required.")
}
this.project = project
// 创建extensions ,可以通过extensions.getByType拿到这个拓展对象
project.extensions.create(COMPONENT_CONFIG_NAME, ComponentExtension::class.java)
}
lateinit var project: Project
}
在 PluginInitializer 类中比较重要的这行代码
project.extensions.create("componentExt", ComponentExtension::class.java)
拓展类:
open class ComponentExtension {
var matcherInterfaceType: String = "" //组件实现接口 如:com/github/plugin/common/IComponent
var matcherManagerTypeMethod: String = "" //管理类初始化方法 如: initComponent
var matcherManagerType: String = "" //管理类的全类名 如:com/github/plugin/common/InjectManager
}
这行代码就是给插件创建拓展(extensions)名字是componentExt,为什么会创建extensions,先看看使用就明白:
componentExt {
matcherInterfaceType "com.github.plugin.common.IComponent"
matcherManagerType "com.github.plugin.common.InjectManager"
matcherManagerTypeMethod "initComponent"
}
是不是明白了extensions的作用了,其实就是我们需要提供开发者动态的配置一些信息,这样会更灵活。
Android Transform 结合Asm字节码插桩完成代码自动注入(重点)
自定义插件的代码比较简单,基本上都是套路,通过拿到AppExtension并将我们自定义的Transform注册进去,看看那Transform的代码,感兴趣可以去Transform API
class ScannerComponentTransformKt : Transform() {
override fun getName(): String {
return "scanner_component_result"
}
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
return TransformManager.CONTENT_CLASS
}
override fun isIncremental(): Boolean {
return false
}
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
return TransformManager.SCOPE_FULL_PROJECT
}
override fun transform(transformInvocation: TransformInvocation) {
if (!transformInvocation.isIncremental) {
transformInvocation.outputProvider.deleteAll()
}
transformInvocation.inputs.forEach { input ->
input.directoryInputs.forEach { dirInput ->
//处理完输入文件之后,要把输出给下一个任务,就是在:transforms\ScannerComponentTransformKt\debug\0目录中
// name就是会在__content__.json文件中的name,唯一的,随便取,但是一定要保证唯一
val dest = transformInvocation.outputProvider.getContentLocation(DigestUtils.md5Hex(dirInput.name),
dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY).also(FileUtils::forceMkdir)
//1、遍历目录中的文件;
//2、修改这些文件;
//3、然后将这些修改过的文件,复制到transforms的输出目录,那么为什么将这些修改过的文件放到transforms,
// 就会被打包到apk中呢?因为我们自定义的transforms会优先于其他transform执行并且是优先于其他的执行,详细的
//可以去看看BaseExtension的构造方法
dirInput.file.eachFileRecurse { file ->
// dest===> transforms\ScannerComponentTransformKt\debug\0 D8编译成dex文件
// file===> build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\com\github\plugin\examlple\MainActivity.class javac 编译生成的字节码
//现在来认证一下,通过asm修改的字节码,是否在javac 或 transforms中?
//确实会存在于transforms目录中,但是javac中不存在
if (TypeUtil.isMatchCondition(file.name)) {
val outputFile = File(file.absolutePath.replace(dirInput.file.absolutePath, dest.absolutePath))
FileUtils.touch(outputFile)
//Dest目录: build\intermediates\transforms\ScannerComponentTransformKt\debug\0
//输入文件: build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\com\github\plugin\examlple\Inject.class
//输出文件: build\intermediates\transforms\ScannerComponentTransformKt\debug\0\com\github\plugin\exalple\Inject.class
KLogger.e("inputFile: ${file.absolutePath} outputFile: ${outputFile.absolutePath} destFile: ${dest.absolutePath}")
val inputStream = FileInputStream(file)
// 开始织入代码,修改这些文件,即:对输入的文件进行修改
val bytes = WeaveSingleClass.weaveSingleClassToByteArray(inputStream)//需要织入代码
//修改输入文件完毕复制输出文件中
val fos = FileOutputStream(outputFile)
fos.write(bytes)
fos.close()
inputStream.close()
}
}
//这里和上面的处理是一样的,将目录中的文件复制到dest目录中
// FileUtils.copyDirectory(dirInput.file, dest)
}
//首先jar是需要解压因为jar是通过zip进行压缩的
// TODO 多模块需要处理Jar,因为lib最后打包是已jar形式引入
//common\build\intermediates\runtime_library_classes\debug\classes.jar
//usercenter\build\intermediates\runtime_library_classes\debug\classes.jar
input.jarInputs.forEach { jarInput ->
if (jarInput.file.absolutePath.endsWith(".jar")) {
//用于存放临时操作的class文件,当操作完毕,便将临时文件拷贝到dest文件即可
val tmpFile = File(jarInput.file.parent + File.separator + "classes_temp.jar")
if (tmpFile.exists()) tmpFile.delete() //避免上次的缓存被重复插入
val tmpJarOutputStream = JarOutputStream(FileOutputStream(tmpFile))
//jar文件
val jarFile = JarFile(jarInput.file)
//拿到所有的jar中的文件
val enumeration = jarFile.entries()
//用于保存JAR文件,修改JAR中的class
while (enumeration.hasMoreElements()) {
val jarEntry = enumeration.nextElement()
val entryName = jarEntry.name
val zipEntry = ZipEntry(entryName)
if (zipEntry.isDirectory) continue
//读取jar中的文件输入流
val inputStream = jarFile.getInputStream(jarEntry)
//插桩class
if (TypeUtil.isMatchCondition(entryName)) {
KLogger.e("ASM 开始处理Jar文件中${entryName}文件")
tmpJarOutputStream.putNextEntry(zipEntry)
val updateCodeBytes = WeaveSingleClass.weaveSingleClassToByteArray(inputStream)
tmpJarOutputStream.write(updateCodeBytes)
KLogger.e("ASM 结束处理Jar文件中${entryName}文件")
} else {
KLogger.e("不满足条件Jar文件中${entryName}文件")
tmpJarOutputStream.putNextEntry(zipEntry)
tmpJarOutputStream.write(IOUtils.toByteArray(inputStream))
}
tmpJarOutputStream.closeEntry()
}
//结束
tmpJarOutputStream.close()
jarFile.close()
// 将临时class文件拷贝到目标dest文件
var jarName = jarInput.name//重名名输出文件,因为可能同名,会覆盖
val md5Name = DigestUtils.md5Hex(jarInput.file.absolutePath)
//截取.jar,即 去掉.jar name就是会在__content__.json文件中的name,唯一的
// name就是会在__content__.json文件中的name,唯一的,随便取,但是一定要保证唯一
if (jarName.endsWith(".jar")) jarName = jarName.substring(0, jarName.length - 4)
val dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
//input: build\intermediates\runtime_library_classes\debug\classes.jar
// //output: build\intermediates\transforms\ScannerComponentTransformKt\debug\0.jar
// //KLogger.e("input: ${jarInput.file.absolutePath} output: ${dest.absolutePath}")
// //KLogger.e("${jarInput.name} $jarName ${jarName + md5Name}")
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}
}
KLogger.e("transform..................end")
}
}
可以看到在Transform的transform方法中通过directoryInputs和jarInputs就可以拿到目录下的.class文件和Jar中的.class文件,也叫输入数据,这里我叫上游,而TransformOutputProvider的getContentLocation方法就是输出,也叫下游。
注意:在Transform中,无论是否更新或修改某个输入文件,你都必须将这些输入文件复制到指定Transform的目录中,不然打包的APK是找不到类的。即: ATransform(上游) 的输出则作为 BTransform(下游)的输入,而这个过程就是中最后的产物就是APK。
其实整个构建流程可以比作是以工厂流水线,而Transform则是流水线上专门负责特定某个任务节点。即:上一个节点的输出则作为下一个节点的输入,所以字节码插装
就是得益于Android 给我们提供的这个机制。
asm操作的是class字节码,在整个构建过程中所有的字节码都是在Transform中作为输入,所以我们只需要遍历Transform的输入数据,对于我们的Transform而言就是收集满足条件的字节码文件,然后通过asm织入一个我们的指定管理类即可,对于Transform Api我不过多的介绍,网上很多博客写得非常好,大家可以去看看。
还是那句话Transform 不管你是否修改class或不修改class这个class输入文件,都必须复制到指定transform的目录中,不然打包的apk是找不到类的,比如:你的MainActivity 继承Androidx 的AppCompatActivity,那么如果你不处理AppCompatActivity的Jar,就会奔溃抛出ClassFileNotFoundException异常。但是你将MainActivity 的父类继承为Activity,那么就不会奔溃,因为Activity属于 Android.jar,而 Android.jar 则属于系统类,Transform不会对android.jar中class做任何搜集和处理,即Transform你必须处理文件并将其写入输出文件夹。即使不处理类文件,你仍然必须将它们复制到输出文件夹。如果你不这样做,所有的类文件都会被删除。
asm 代码如下:
object WeaveSingleClass {
fun weaveSingleClassToByteArray(inputStream: InputStream): ByteArray {
//1、解析字节码
val classReader = ClassReader(inputStream)
//2、修改字节码
val classWriter = ExtendClassWriter(ClassWriter.COMPUTE_MAXS)
val customClassVisitor = CustomInjectClassVisitor(classWriter)
//3、开始解析字节码
classReader.accept(customClassVisitor, ClassReader.EXPAND_FRAMES)
return classWriter.toByteArray()
}
fun weaveSingleClassToByteArrayAutoInject(inputStream: InputStream): ByteArray {
//1、解析字节码
val classReader = ClassReader(inputStream)
//2、修改字节码
val classWriter = ExtendClassWriter(ClassWriter.COMPUTE_MAXS)
val customClassVisitor = AutoInjectComponentClassVisitor(classWriter)
//3、开始解析字节码
classReader.accept(customClassVisitor, ClassReader.EXPAND_FRAMES)
return classWriter.toByteArray()
}
}
// 访问class信息
class AutoInjectComponentClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, classVisitor) {
//如果是实现了IComponent接口的话,将所有组件类收集起来,通过修改字节码的方式生成注册代码到组件管理类中
override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array<out String>?) {
KLogger.e("${interfaces?.joinToString { it }}")
KLogger.e("name>>>----$name")
if (interfaces?.contains(PluginInitializer.getComponentInterfaceName()) == true && name != "") {
ComponentNameCollection.add("$name")
}
super.visit(version, access, name, signature, superName, interfaces)
}
override fun visitMethod(access: Int, name: String, descriptor: String, signature: String?, exceptions: Array<out String>?): MethodVisitor {
KLogger.e("name:$name descriptor:$descriptor")
val visitMethod = super.visitMethod(access, name, descriptor, signature, exceptions)
if (PluginInitializer.getComponentManagerTypeInitMethodName() != name) {
return visitMethod
}
return AutoInjectComponentMethodVisitor(visitMethod, access, name, descriptor)
}
}
// 访问method信息
class AutoInjectComponentMethodVisitor(methodVisitor: MethodVisitor?, access: Int, name: String?, descriptor: String?)
: AdviceAdapter(Opcodes.ASM7, methodVisitor, access, name, descriptor) {
override fun onMethodExit(opcode: Int) {
KLogger.e("${ComponentNameCollection.size} $opcode")
mv.visitVarInsn(ALOAD, 0)
mv.visitFieldInsn(GETFIELD, PluginInitializer.getComponentManagerTypeName(), "components", "Ljava/util/List;")
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "clear", "()V", true)
ComponentNameCollection.forEach { name ->
KLogger.e(">><<<>>>>>>${name}")
// 加载this
mv.visitVarInsn(ALOAD, 0)
//拿到类的成员变量 坑,你需要注意的类名不要写错了
mv.visitFieldInsn(GETFIELD, PluginInitializer.getComponentManagerTypeName().replace(".", "/"), "components", "Ljava/util/List;")
//用无参构造方法创建一个组件实例
mv.visitTypeInsn(Opcodes.NEW, name)
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, name, "<init>", "()V", false)
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true)
mv.visitInsn(POP)
}
}
}
最后产生的字节码之前和之后对比如下:
public class InjectManager {
public synchronized void initComponent() { }
}
..........之后..........
public class InjectManager {
private List<IComponent> components = new ArrayList();
public synchronized void initComponent() {
this.components.clear();
this.components.add(new MainComponent());
this.components.add(new UserComponent());
this.components.add(new OrderComponent());
}
}
这样就完成了组件化在编译期自动注入其他组件初始化,当你要使用的就直接调用InjectManager .initComponent()就可以了。其实还有更好的方式就是像 android hilt 那样通过修改类的继承方式,把所有的逻辑放在了父类中,让我们Application 去继承该Application即可。
为什么我会定义IComponent接口并让所有初始化组件实现,这是因为后期可能增加一些其他功能或操作,比如:增加组件初始化的优先级,那么 IComponent接口 直接增加一个优先级方法即可完成,而不需要在去修改我们织入字节码的操作,那太复杂了容易出错。
参考借鉴文章:
我看很多人留言说需要代码,我最近给大家写了个模板,希望对大家有用:
abstract class IncrementalTransform extends Transform {
// 共享线程池
//protected final WaitableExecutor globalSharedThreadPool = WaitableExecutor.useGlobalSharedThreadPool()
protected final ThreadPool threadPool = new ThreadPool()
private Project project
IncrementalTransform(Project project) {
this.project = project
}
@Override
void transform(TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
doTransform(transformInvocation)
}
private void doTransform(TransformInvocation invocation) {
TransformOutputProvider outputProvider = invocation.outputProvider
if (!invocation.isIncremental()) {
outputProvider.deleteAll()
}
invocation.inputs.each { TransformInput transformInput ->
// JAR
transformInput.jarInputs.each { JarInput jarInput ->
threadPool.addTask(new ITask() {
@Override
Void call() throws Exception {
return handleJar(jarInput, outputProvider, invocation)
}
})
}
// DIR
transformInput.directoryInputs.each { DirectoryInput directoryInput ->
threadPool.addTask(new ITask() {
@Override
Void call() throws Exception {
return handleDirectory(directoryInput, outputProvider, invocation)
}
})
}
}
//等待所有任务结束
//globalSharedThreadPool.waitForTasksWithQuickFail(true)
threadPool.startWork()
}
private void handleJar(
JarInput jarInput,
TransformOutputProvider outputProvider,
TransformInvocation invocation) {
//得到上一个Transform输入文件
File inputJar = jarInput.file
// 得到当前Transform输出Jar文件
File outputJar =
outputProvider.getContentLocation(
jarInput.name, jarInput.contentTypes,
jarInput.scopes, Format.JAR)
if (invocation.isIncremental()) {// 增量处理
if (jarInput.status == Status.NOTCHANGED) {//文件没有改变
println("IncrementalTransform >>> File NOTCHANGED")
} else if (jarInput.status == Status.ADDED) {//有新增文件
dispatchAction(inputJar, outputJar, true)
} else if (jarInput.status == Status.CHANGED) {//有修改文件
FileUtils.deleteIfExists(outputJar)// 先把上次生成的文件删除
dispatchAction(inputJar, outputJar, true)
} else if (jarInput.status == Status.REMOVED) {//文件被移除
//把上次当前Transform输出文件删除
FileUtils.delete(outputJar)
}
} else {// 全量处理
dispatchAction(inputJar, outputJar, true)
}
}
private void handleDirectory(
DirectoryInput directoryInput, TransformOutputProvider outputProvider,
TransformInvocation invocation) {
//得到上一个Transform输入文件目录
File inputDir = directoryInput.file
// 得到当前Transform输出文件目录
File outputDir =
outputProvider.getContentLocation(
directoryInput.name, directoryInput.contentTypes,
directoryInput.scopes, Format.DIRECTORY)
if (invocation.isIncremental()) {
directoryInput.changedFiles.entrySet().each { Map.Entry<File, Status> entry ->
File inputFile = entry.key
if (entry.value == Status.NOTCHANGED) {//文件没有改变
println("IncrementalTransform >>> File NOTCHANGED")
} else if (entry.value == Status.ADDED) {//有增加文件
File outputFile = FileUtil.toOutputFile(outputDir, inputDir, inputFile)
dispatchAction(inputFile, outputFile, false)
} else if (entry.value == Status.CHANGED) {//文件有修改
File outputFile = FileUtil.toOutputFile(outputDir, inputDir, inputFile)
FileUtils.deleteIfExists(outputFile)// 先把上次生成的文件删除
dispatchAction(inputFile, outputFile, false)
} else if (entry.value == Status.REMOVED) {//文件被移除
//把上次输出的目录删除
File outputFile = FileUtil.toOutputFile(outputDir, inputDir, inputFile)
FileUtils.deleteIfExists(outputFile)
}
}
} else {
// 上一个Transform的输出目录下的所有文件
FluentIterable<File> dirChildFiles = FileUtils.getAllFiles(inputDir)
dirChildFiles.each { File inputFile ->
// 当前Transform输出文件
File outputFile = FileUtil.toOutputFile(outputDir, inputDir, inputFile)
dispatchAction(inputFile, outputFile, false)
}
}
}
protected void dispatchAction(
File inputFile, File outputFile, boolean handleJar) {
if (handleJar) {//JAR
// 输出目标jar文件
FileOutputStream fos = new FileOutputStream(outputFile)
JarOutputStream outputJarOs = new JarOutputStream(fos)
//处理输入Jar文件
JarFile inputJarFile = new JarFile(inputFile)
Enumeration<JarEntry> entries = inputJarFile.entries()
while (entries.hasMoreElements()) {
JarEntry inputJarEntry = entries.nextElement()
String inputJarEntryName = inputJarEntry.getName()
// 拿到jar包里面的输入流
InputStream inputJarEntryInputStream =
inputJarFile.getInputStream(inputJarEntry)
// 构造目标jar文件里的文件实体 即保持和上一个JarEntry名称一致
outputJarOs.putNextEntry(new ZipEntry(inputJarEntryName))
//如果不做是否处理,那么仅仅只是将该文件复制到当前Transform的目标输出文件即可
boolean isHandle =
doJarAction(inputJarEntryInputStream, outputJarOs)
if (!isHandle) {
//将修改过的字节码copy到dest
outputJarOs.write(
IOUtils.toByteArray(inputJarEntryInputStream)
)
}
inputJarEntryInputStream.close()
}
outputJarOs.closeEntry()
outputJarOs.close()
inputJarFile.close()
return
}
//DIR
//如果不做是否处理,那么仅仅只是将该文件复制到当前Transform的目标输出文件即可
boolean isHandle = doDirectoryAction(inputFile, outputFile)
if (!isHandle) {
//将修改过的字节码copy到dest
FileUtil.copyFileAndMkdirsAsNeed(inputFile, outputFile)
}
}
/**
* 处理Jar文件的资源
*
* 这些都是在工作线程中执行的
*
* @param inputStream 上一个Transform的输入流
* @param outputStream 当前Transform的输出流
* @return isHandle 是否已经处理了该文件,如果已经处理了文件返回 true
*
*/
protected abstract boolean doJarAction(InputStream inputStream, OutputStream outputStream)
/**
* 处理目录的资源文件
*
*
* 这些都是在 工作线程中执行的
*
* @param inputJar 上一个Transform的输入文件
* @param outputJar 当前Transform的输出文件
* @return 是否已经处理了该文件,如果已经处理了文件返回 true
*/
protected abstract boolean doDirectoryAction(File inputJar, File outputJar)
}
最后就是你只需要继承该类,然后实现doJarAction
和doDirectoryAction
方法实现相应功能即可,当然这是Groovy版本的。该模本实现了 增量更新
和 并发处理
打打提升编译速度。