Gradle+Transform+Asm自动化注入代码

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会在ProguardTransformDexTransform之前执行,实际上Transform是Android一种特殊的Task,自定义的Transform是会在自带的Transform之前执行,所以自动注册的Transform不需要考虑混淆的情况。

APK 打包流程

我们平时在开发的过程中,每天在Android Studio Run项目,Android Studio就会将apk自动安装到手机上了,那么这中间都经历过哪些流程呢,来看看官方的项目构建流程图

build-process_2x.png

如图所示,典型 Android 应用模块的构建流程通常按照以下步骤执行:
*1、 编译器将源代码转换成 DEX 文件(Dalvik 可执行文件),并将其他所有内容转换成编译后的资源。

2、 APK 打包器将 DEX 文件和编译后的资源合并到一个 APK 中。不过,在将应用安装并部署到 Android 设备之前,必须先为 APK 签名。

3、APK 打包器使用调试或发布密钥库为 APK 签名:

  • 如果您构建的是调试版应用(即专用于测试和分析的应用),则打包器会使用调试密钥库为应用签名。Android Studio 会自动使用调试密钥库配置新项目。
  • 如果您构建的是打算对外发布的发布版应用,则打包器会使用发布密钥库为应用签名。要创建发布密钥库,请参阅在 Android Studio 中为应用签名

4、 在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,以减少其在设备上运行时所占用的内存。

可能从图中并不会看出什么来,实际上对于Java编程语言来说,这个过程要从Java源代码到apk,那么我们来看一张图:


gradle打包.png

这张图就非常的清晰了,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目录,如:

plugin.png

并在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接口 直接增加一个优先级方法即可完成,而不需要在去修改我们织入字节码的操作,那太复杂了容易出错。

AOP 的利器:ASM 3.0 介绍

参考借鉴文章:

我看很多人留言说需要代码,我最近给大家写了个模板,希望对大家有用:

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

最后就是你只需要继承该类,然后实现doJarActiondoDirectoryAction方法实现相应功能即可,当然这是Groovy版本的。该模本实现了 增量更新并发处理 打打提升编译速度。

升级 AGP 7.0

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容