上篇介绍了自定义 plugin,这一篇我们学习下 Transform。这个在平时应用中会有更多的应用情景。

和上一篇不同的是这回,我们直接在 AndroidStudio 中新建一个 Android module 删除我们不需要的 res 和其他和android 相关的目录。具体如图所示。一定要新建 groovy 目录下放 groovy文件。还有gradle-plugins 目录下的文件,具体参照上一篇。

image.png

一、创建

1、创建自定义 Plugin

  1. import com.android.build.gradle.AppExtension
  2. import org.gradle.api.Plugin
  3. import org.gradle.api.Project
  4. class TestPlugin implements Plugin<Project> {
  5. @Override
  6. void apply(Project project) {
  7. project.extensions.findByType(AppExtension).registerTransform(new MyTransform(project))
  8. }
  9. }

2、创建自定义 Transform

import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.gradle.api.Project

class MyTransform extends Transform {

    Project project

    MyTransform(Project project) {
        this.project = project
    }

    @Override
    String getName() {
        return "ssy transform"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {

        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }
//当前是否是增量编译
    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        // super.transform(transformInvocation)
        println "=============hello, body!=============="
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        transformInvocation.inputs.each {
            TransformInput input ->
                println "===========================--------------------"

                input.directoryInputs.each {
                    DirectoryInput directoryInput ->
                        MyInject.injectDir(directoryInput.file.absolutePath, "com.ssy.demo0628", project)
                        def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                        println "===========================--------------------${dest.absolutePath}"
                        FileUtils.copyDirectory(directoryInput.file, dest)

                }
                input.jarInputs.each {
                    JarInput jarInput ->
                        String jarName = jarInput.name
                        def md5Name = org.apache.commons.codec.digest.DigestUtils.md5Hex(jarInput.file.getAbsoluteFile().toString())
                        if (jarName.endsWith(".jar")) {
                            jarName = jarName.substring(0, jarName.length() - 4)
                        }
                        def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                        FileUtils.copyFile(jarInput.file, dest)
                }


        }

    }
}

3、处理代码

import javassist.ClassPool
import javassist.CtClass
import javassist.CtConstructor
import javassist.CtField
import javassist.CtMethod
import org.gradle.api.Project

public class MyInject {
    private static ClassPool pool = ClassPool.getDefault()
    private static String injectStr = "System.out.println(\"I im Mr.S\" ); "

    static void injectDir(String path, String packageName, Project project) {
        pool.appendClassPath(project.android.bootClasspath[0].toString())

        pool.appendClassPath(path)
        File dir = new File(path)
        if (dir.directory) {
            dir.eachFileRecurse {
                File file ->
                    String filePath = file.absolutePath
                    //确保当前文件是class 文件,并且不是系统自动生成的 class 文件
                    if (filePath.endsWith(".class")
                            && !filePath.contains('R$')
                            && !filePath.contains('R.class')
                            && !filePath.contains('BuildConfig.class')
                    ) {

                        //判断当前目录是否在我们的应用包里面
                        int index = filePath.indexOf(packageName.replace('.', File.separator))
                        boolean isMyPackage = index != -1
                        if (isMyPackage) {
                            int end = filePath.length() - 6
                            String className = filePath.substring(index, end).replace('\\', '.').replace('/', '.')
                            //开始修改 class 文件
                            CtClass c = pool.getCtClass(className)
                            if (c.isFrozen()) {
                                c.defrost()
                            }
                            //这是构造方法里增加方法
//                            CtConstructor[] constructors = c.getDeclaredConstructors()
//                            if (constructors==null||constructors.length==0){
//                                //手动创建一个构造函数
//                                CtConstructor constructor = new CtConstructor(new CtClass[0],c)
//                                constructor.insertBeforeBody(injectStr)
//                                c.addConstructor(constructor)
//                            }else {
//                                constructors[0].insertBeforeBody(injectStr)
//                            }

                            for (CtMethod currentMethod : c.getDeclaredMethods()) {
                                if (currentMethod.name == 'onClick') {
                                    if (currentMethod.signature == ('(Landroid/view/View;)V')) {
                                        currentMethod.insertAfter("com.ssy.demo0628.Utils.onMyClick(\$1);")
                                    }
                                }
                            }


                            c.writeFile(path)
                            c.detach()
                        }

                    }
            }
        }
    }

4、添加时间监听和打印

class TimingsListener implements TaskExecutionListener, BuildListener {

    public Clock clock
    private timings = []

    @Override
    void buildStarted(Gradle gradle) {

    }

    @Override
    void settingsEvaluated(Settings settings) {

    }

    @Override
    void projectsLoaded(Gradle gradle) {

    }

    @Override
    void projectsEvaluated(Gradle gradle) {

    }

    /**
     *
     * 类似这样:
     BUILD SUCCESSFUL in 22s

     57 actionable tasks: 57 executed
     Task timings:
     146ms  :app:generateDebugBuildConfig
     86ms  :gradlescriptdemo:compileGroovy
     273ms  :app:mergeDebugResources
     169ms  :app:processDebugManifest
     248ms  :app:processDebugResources
     1181ms  :app:compileDebugJavaWithJavac
     439ms  :app:transformClassesWith-->WQTransform<--ForDebug
     415ms  :app:transformClassesWithDexForDebug
     451ms  :app:packageDebug
     180ms  :app:mergeReleaseResources
     69ms  :app:processReleaseResources
     130ms  :app:compileReleaseJavaWithJavac
     172ms  :buildsrc:compileGroovy
     1197ms  :app:lintVitalRelease
     86ms  :app:packageRelease
     4246ms  :app:lint
     6528ms  :app:mockableAndroidJar

     *
     * @param buildResult
     */
    @Override
    void buildFinished(BuildResult buildResult) {
        println "Task timings:"
        for (timing in timings) {
            if (timing[0] >= 50) {
                printf "%7sms  %s\n", timing
            }
        }
    }

    @Override
    void beforeExecute(Task task) {
        clock = Clock.systemDefaultZone()


    }

    @Override
    void afterExecute(Task task, TaskState state) {
        def ms = clock.millis()
        timings.add([ms, task.path])
        task.project.logger.warn "${task.path} took ${ms}ms"
        if(task.outputs.files.files) {
            task.project.logger.warn "taskName:${task.name}"
            task.project.logger.warn "outputs.files.files: ------------start----------------- "
            task.outputs.files.files.each {
                task.project.logger.warn "${it.absolutePath} "
            }
            task.project.logger.warn "outputs.files.files: ---------------end------------------ "
        }
    }
}

5、配置module build.gradle

plugins {
    id 'groovy'
    id 'maven'
}

group 'com.ssy'
version '1.0.2'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
    google()
    jcenter()
}

dependencies {
    //Gradle Plugin 依赖
    implementation gradleApi()
    //本地发布 Plugin
    implementation localGroovy()
    //因为我们需要在android 项目中使用,所以需要android的tool build库
    implementation 'com.android.tools.build:gradle:3.1.3'

    compile 'commons-io:commons-io:2.5'
    implementation 'commons-io:commons-io:2.5'
    implementation 'org.javassist:javassist:3.20.0-GA'
}
uploadArchives {
    repositories {
        mavenDeployer {
            //设置插件的GAV参数
            pom.groupId = 'com.ssy'
            pom.version = '1.0.2'
            //文件发布到下面目录
            repository(url: uri('../repo'))
        }
    }
}

点击 uploadArchives

image.png

repo 目录就生成在了我们的工程里。

image.png

二、使用

1、在 project 下的 build.gradle 添加我们自己的 classpath。

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {

    repositories {
        maven {
            url uri('repo')
        }
        google()
        jcenter()

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.3'
        //这个就是 仓库中指定我们需要的版本
        classpath 'com.ssy:TestPlugin:1.0.2'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

2、在 app 的 build.gradle 使用我们的plugin。

image.png

3、编译运行就好了。