Android Gradle Transform 创建自定义插件

本博文是demo源码,详情参考下面链接

手把手教你实现一个 Gradle Tansform 实例

一文学会Android Gradle Transform基础使用

Gradle系列 (上篇) —Android自定义Gradle插件并在项目中使用

Gradle系列 (中篇) —在自定义Gradle插件中使用javassist往class中注入代码

1. 创建Android Project项目

2. 创建Android Library Module,取名plugin

2.1 目录如下:

Android Gradle Transform 创建自定义插件_第1张图片

  • 用publishToMavenLocal命令把插件plugin发布到本地maven库

Android Gradle Transform 创建自定义插件_第2张图片

2.2 插件源文件:

2.2.1 build.gradle文件内容:

plugins {
    id 'groovy'
    id 'maven-publish'
}

dependencies {
    implementation gradleApi() //gradle sdk
    implementation localGroovy() //groovy sdk

    implementation 'com.android.tools.build:gradle:7.1.3'
    implementation 'commons-codec:commons-codec:1.15'
    implementation 'commons-io:commons-io:2.11.0'
    implementation 'org.javassist:javassist:3.29.0-GA'
}

publishing {
    publications {
        myLibrary(MavenPublication) {
            groupId = 'com.df'  //groupId ,自行定义,组织名或公司名
            artifactId = 'df.android' //artifactId,自行定义,项目名或模块名
            version = '1.0.0' //插件版本号
            from components.java
        }
    }

    repositories {
        // 本地仓库位于USER_HOME/.m2/repository
        mavenLocal()
        // 其他maven仓库
//        maven { url uri('/Users/h__d/Desktop/1') }
    }
}

2.2.2 com.jokerwan.android.properties

implementation-class=com.jokerwan.demo.plugin.JokerWanPlugin

2.2.3 JokerWanPlugin

package com.jokerwan.demo.plugin

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class JokerWanPlugin implements Plugin<Project> {
    void apply(Project project) {
        AppExtension appExtension = project.extensions.findByType(AppExtension.class)
        appExtension.registerTransform(new JokerWanTransform(project))

//        project.android.registerTransform(new JokerWanTransform(project))
    }
}

2.2.4 JokerWanTransform

package com.jokerwan.demo.plugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project

class JokerWanTransform extends Transform {

    private static Project project
    private static final String NAME = "JokerWanAutoTrack"

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

    @Override
    String getName() {
        return NAME
    }

    /**
     * 需要处理的数据类型,有两种枚举类型
     * CLASSES 代表处理的 java 的 class 文件,RESOURCES 代表要处理 java 的资源
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
     * 1. EXTERNAL_LIBRARIES        只有外部库
     * 2. PROJECT                   只有项目内容
     * 3. PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
     * 4. PROVIDED_ONLY             只提供本地或远程依赖项
     * 5. SUB_PROJECTS              只有子项目。
     * 6. SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
     * 7. TESTED_CODE               由当前变量(包括依赖项)测试的代码
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    static void printCopyRight() {
        println()
        println("******************************************************************************")
        println("******                                                                  ******")
        println("******                欢迎使用 JokerWanTransform 编译插件               ******")
        println("******                                                                  ******")
        println("******************************************************************************")
        println()
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException,
            InterruptedException, IOException {
        printCopyRight()

        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()

        // Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
        transformInvocation.inputs.each { TransformInput input ->
            //遍历jar文件 对jar不操作,但是要输出到out路径
            input.jarInputs.each { JarInput jarInput ->
                // 处理jar
                processJarInput(jarInput, outputProvider)
            }

            //遍历文件夹
            input.directoryInputs.each { DirectoryInput directoryInput ->
                // 处理源码文件
                processDirectoryInput(directoryInput, outputProvider)
            }
        }

    }

    void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
        // 重命名输出文件(同目录copyFile会冲突)
        def jarName = jarInput.name
        def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
        if (jarName.endsWith(".jar")) {
            jarName = jarName.substring(0, jarName.length() - 4)
        }

        File dest = outputProvider.getContentLocation(
                jarName + md5Name,
                jarInput.getContentTypes(),
                jarInput.getScopes(),
                Format.JAR
        )

        println("*****************")
        println("     jarInput: " + jarInput.file.absolutePath + "  ")
        println("     jarName: " + jarName + "  ")
        println("     dest: " + dest.absolutePath + "  ")
        println("*****************")

        // TODO do some transform

        // 将 input 的目录复制到 output 指定目录
        FileUtils.copyFile(jarInput.getFile(), dest)
    }

    void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        String path = directoryInput.file.absolutePath
        println("[processDirectoryInput] Begin to inject: $path")

        // 执行注入逻辑
        InjectByJavassit.inject(path, project)

        // 获取output目录
        File dest = outputProvider.getContentLocation(
                directoryInput.getName(),
                directoryInput.getContentTypes(),
                directoryInput.getScopes(),
                Format.DIRECTORY
        )
        println("[processDirectoryInput] Begin to inject: ${dest.absolutePath}")

        // 将 input 的目录复制到 output 指定目录
        FileUtils.copyDirectory(directoryInput.getFile(), dest)
    }
}

2.2.5 InjectByJavassit

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

class InjectByJavassit {

    static void inject(String path, Project project) {
        try {
            File dir = new File(path)
            if (dir.isDirectory()) {
                dir.eachFileRecurse { File file ->
                    if (file.name.endsWith('Activity.class')) {
                        doInject(project, file, path)
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace()
        }
    }

    private static void doInject(Project project, File clsFile, String originPath) {
        println("[Inject] DoInject clsFile: $clsFile.absolutePath")
        println("[Inject] DoInject originPath: $originPath")
        String cls = new File(originPath).relativePath(clsFile).replace('/', '.')
        cls = cls.substring(0, cls.lastIndexOf('.class'))
        println("[Inject] Cls: $cls")

        ClassPool pool = ClassPool.getDefault()
        // 将当前路径加入类池,不然找不到这个类
        pool.appendClassPath(originPath)
        // project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
        // 为了能找到android相关的所有类,添加project.android.bootClasspath 加入android.jar,
        pool.appendClassPath(project.android.bootClasspath[0].toString())
        // 引入android.os.Bundle包,因为onCreate方法参数有Bundle
        pool.importPackage('android.os.Bundle')

        CtClass ctClass = pool.getCtClass(cls)
        // 解冻
        if (ctClass.isFrozen()) {
            ctClass.defrost()
        }
        // 获取方法
        CtMethod ctMethod = ctClass.getDeclaredMethod('onCreate')

        String toastStr = 'android.widget.Toast.makeText(this, "I am the injected code", android.widget.Toast.LENGTH_SHORT).show();'

        // 方法尾插入
        ctMethod.insertAfter(toastStr) // 在方法开始注入代码
//        ctMethod.insertAfter(injectCode) // 在方法结尾注入代码
//        ctMethod.insertAt(18, injectCode) // 在class文件的某一行插入代码,前提是class包含行号信息

        ctClass.writeFile(originPath)// 根据CtClass生成.class文件;

        /**
         * 将该class从ClassPool中删除
         *
         * ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,
         * API中给出的解决方案是 有意识的调用CtClass的detach()方法以释放内存。
         */
        ctClass.detach()
    }
}

3. 项目文件

3.1 settings.gradle文件内容:

pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
        google()
        mavenLocal() // 本地库
    }
}
dependencyResolutionManagement {
//    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "TransformDemo"
include ':app'
include ':plugin'

3.2 build.gradle文件内容:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    dependencies {
        classpath "com.df:df.android:1.0.0"
    }
}

plugins {
    id 'com.android.application' version '7.2.1' apply false
    id 'com.android.library' version '7.2.1' apply false
}

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

4. App module 文件

4.1 build.gradle文件内容:

plugins {
    id 'com.android.application'
    id 'com.jokerwan.android'
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.df.transformdemo"
        minSdk 21
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.6.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

你可能感兴趣的:(android,gradle,android,transform)