Android動態(tài)編譯技術 Plugin Transform Javassist操作Class文件

前言

繼續(xù)上一章節(jié)自定義Gradle插件,利用plugin進一步做一些事情

本章節(jié)利用Google提供的Transform API 在編譯的過程中操作.class文件。

先說一下Transform是什么

gradle從1.5開始,gradle插件包含了一個叫Transform的API,這個API允許第三方插件在class文件轉為為dex文件前操作編譯好的class文件,這個API的目標是簡化自定義類操作,而不必處理Task,并且在操作上提供更大的靈活性。并且可以更加靈活地進行操作。
官方文檔:http://google.github.io/android-gradle-dsl/javadoc/
我們接著在上面的demo中繼續(xù)完成使用Transform API,

在我們自定義的gradle插件的build.gradle中引入transform的包,下面會進行代碼注入,就一起引入的其他包

compile 'com.android.tools.build:transform-api:1.5.0'
compile 'javassist:javassist:3.12.1.GA'
compile 'commons-io:commons-io:2.5'

項目地址:TransformPlugin

接下來創(chuàng)建一個類繼承Transform 并實現(xiàn)其方法

package zxy.com.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



public class MyClassTransform extends Transform {

private Project mProject;

public MyClassTransform(Project p) {
    this.mProject = p;
}

//transform的名稱
//transformClassesWithMyClassTransformForDebug 運行時的名字
//transformClassesWith + getName() + For + Debug或Release
@Override
public String getName() {
    return "MyClassTransform";
}

//需要處理的數(shù)據(jù)類型,有兩種枚舉類型
//CLASSES和RESOURCES,CLASSES代表處理的java的class文件,RESOURCES代表要處理java的資源
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
    return TransformManager.CONTENT_CLASS;
}

//    指Transform要操作內(nèi)容的范圍,官方文檔Scope有7種類型:

//    EXTERNAL_LIBRARIES        只有外部庫
//    PROJECT                       只有項目內(nèi)容
//    PROJECT_LOCAL_DEPS            只有項目的本地依賴(本地jar)
//    PROVIDED_ONLY                 只提供本地或遠程依賴項
//    SUB_PROJECTS              只有子項目。
//    SUB_PROJECTS_LOCAL_DEPS   只有子項目的本地依賴項(本地jar)。
//    TESTED_CODE                   由當前變量(包括依賴項)測試的代碼
@Override
public Set<QualifiedContent.Scope> getScopes() {
    return TransformManager.SCOPE_FULL_PROJECT;
}

//指明當前Transform是否支持增量編譯
@Override
public boolean isIncremental() {
    return false;
}

//    Transform中的核心方法,
//    inputs中是傳過來的輸入流,其中有兩種格式,一種是jar包格式一種是目錄格式。
//    outputProvider 獲取到輸出目錄,最后將修改的文件復制到輸出目錄,這一步必須做不然編譯會報錯
@Override
public void transform(Context context,
                      Collection<TransformInput> inputs,
                      Collection<TransformInput> referencedInputs,
                      TransformOutputProvider outputProvider,
                      boolean isIncremental) throws IOException, TransformException, InterruptedException {
    System.out.println("你愁啥----------------進入transform了--------------")
    //遍歷input
    inputs.each { TransformInput input ->
        //遍歷文件夾
        input.directoryInputs.each { DirectoryInput directoryInput ->
            //注入代碼
            MyInjects.inject(directoryInput.file.absolutePath, mProject)

            // 獲取output目錄
            def dest = outputProvider.getContentLocation(directoryInput.name,
                    directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

            // 將input的目錄復制到output指定目錄
            FileUtils.copyDirectory(directoryInput.file, dest)
        }

        ////遍歷jar文件 對jar不操作,但是要輸出到out路徑
        input.jarInputs.each { JarInput jarInput ->
            // 重命名輸出文件(同目錄copyFile會沖突)
            def jarName = jarInput.name
            println("jar = " + jarInput.file.getAbsolutePath())
            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            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)
        }
    }
    System.out.println("瞅你咋地--------------結束transform了----------------")
}

}

在我們自定義的gradle插件的apply方法中注冊自定義的Transform,上一章節(jié)已經(jīng)有介紹過apply入口

def android = project.extensions.getByType(AppExtension)
//注冊一個Transform
def classTransform = new MyClassTransform(project);
android.registerTransform(classTransform);

BuildConfig這個類大家并不陌生,在項目里會用到,大家知道這個類可以增加我們自定義的屬性嗎,可是你知道怎么生成的么?

//我們自定義的
testCreatJavaConfig{
str = "動態(tài)生成java類的字符串"
}

然后回到我們的自定義的Plugin中,貼一下整個代碼

import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* @author:xinyu.zhou
*/
public class MyPlugin implements Plugin<Project> {

void apply(Project project) {
    System.out.println("------------------開始----------------------");
    System.out.println("這是我們的自定義插件!");
    //AppExtension就是build.gradle中android{...}這一塊
    def android = project.extensions.getByType(AppExtension)

    //注冊一個Transform
    def classTransform = new MyClassTransform(project);
    android.registerTransform(classTransform);

    //創(chuàng)建一個Extension,名字叫做testCreatJavaConfig 里面可配置的屬性參照MyPlguinTestClass
    project.extensions.create("testCreatJavaConfig", MyPlguinTestClass)

    //生產(chǎn)一個類
    if (project.plugins.hasPlugin(AppPlugin)) {
        //獲取到Extension,Extension就是 build.gradle中的{}閉包
        android.applicationVariants.all { variant ->
                //獲取到scope,作用域
                def variantData = variant.variantData
            def scope = variantData.scope

            //拿到build.gradle中創(chuàng)建的Extension的值
            def config = project.extensions.getByName("testCreatJavaConfig");

            //創(chuàng)建一個task
            def createTaskName = scope.getTaskName("CeShi", "MyTestPlugin")
            def createTask = project.task(createTaskName)
            //設置task要執(zhí)行的任務
            createTask.doLast {
                //生成java類
                createJavaTest(variant, config)
            }
            //設置task依賴于生成BuildConfig的task,然后在生成BuildConfig后生成我們的類
            String generateBuildConfigTaskName = variant.getVariantData().getScope().getGenerateBuildConfigTask().name
            def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
            if (generateBuildConfigTask) {
                createTask.dependsOn generateBuildConfigTask
                generateBuildConfigTask.finalizedBy createTask
            }
        }

    }
    System.out.println("------------------結束了嗎----------------------");
}

static def void createJavaTest(variant, config) {
    //要生成的內(nèi)容
    def content = """package com.zxy.plugin;

 

    public class MyPlguinTestClass {
        public static final String str = "${config.str}";
    }
    """;
    //獲取到BuildConfig類的路徑
    File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()

    def javaFile = new File(outputDir, "MyPlguinTestClass.java")

    javaFile.write(content, 'UTF-8');
}
 }

class MyPlguinTestClass {
def str = "默認值";
}

編譯一下看一下效果



可以看到我在app目錄下的build.gradle文件里配置的testCreatJavaConfig 生效了,可以取到str的值

接下來要使用javassist,簡單介紹下

  • Javassist是一個動態(tài)類庫,可以用來檢查、”動態(tài)”修改以及創(chuàng)建 Java類。其功能與jdk自帶的反射功能類似,但比反射功能更強大
  • ClassPool:javassist的類池,使用ClassPool 類可以跟蹤和控制所操作的類,它的工作方式與 JVM 類裝載器非常相似,
    CtClass: CtClass提供了檢查類數(shù)據(jù)(如字段和方法)以及在類中添加新字段、方法和構造函數(shù)、以及改變類、父類和接口的方法。不過,Javassist 并未提供刪除類中字段、方法或者構造函數(shù)的任何方法。
    CtField:用來訪問域
    CtMethod :用來訪問方法
    CtConstructor:用來訪問構造器

想了解更多請自行查閱資料

下面我們利用Transform在MainActivity中動態(tài)的插入代碼,先看一下現(xiàn)在的MainAcitivity

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    TextView textView= findViewById(R.id.tv);
    textView.setText(com.zxy.plugin.MyPlguinTestClass.str);
}
}

可以看到上面的setText中使用的是我們上面動態(tài)生成的類中的字段,看一下怎么利用Transform插入代碼,先看一下Transform中代碼

//    Transform中的核心方法,
//    inputs中是傳過來的輸入流,其中有兩種格式,一種是jar包格式一種是目錄格式。
//    outputProvider 獲取到輸出目錄,最后將修改的文件復制到輸出目錄,這一步必須做不然編譯會報錯
@Override
public void transform(Context context,
                      Collection<TransformInput> inputs,
                      Collection<TransformInput> referencedInputs,
                      TransformOutputProvider outputProvider,
                      boolean isIncremental) throws IOException, TransformException, InterruptedException {
    System.out.println("你愁啥----------------進入transform了--------------")
    //遍歷input
    inputs.each { TransformInput input ->
        //遍歷文件夾
        input.directoryInputs.each { DirectoryInput directoryInput ->
            //注入代碼
            MyInjects.inject(directoryInput.file.absolutePath, mProject)

            // 獲取output目錄
            def dest = outputProvider.getContentLocation(directoryInput.name,
                    directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

            // 將input的目錄復制到output指定目錄
            FileUtils.copyDirectory(directoryInput.file, dest)
        }

        ////遍歷jar文件 對jar不操作,但是要輸出到out路徑
        input.jarInputs.each { JarInput jarInput ->
            // 重命名輸出文件(同目錄copyFile會沖突)
            def jarName = jarInput.name
            println("jar = " + jarInput.file.getAbsolutePath())
            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            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)
        }
    }
    System.out.println("瞅你咋地--------------結束transform了----------------")
}

生成代碼在MyInjects類中,在這個類中我們傳入了兩個參數(shù),一個是當前變量的文件夾,一個是當前的工程對象,來看一下代碼

 public class MyInjects {
//初始化類池
private final static ClassPool pool = ClassPool.getDefault();

public static void inject(String path,Project project) {
    //將當前路徑加入類池,不然找不到這個類
    pool.appendClassPath(path);
    //project.android.bootClasspath 加入android.jar,不然找不到android相關的所有類
    pool.appendClassPath(project.android.bootClasspath[0].toString());
    //引入android.os.Bundle包,因為onCreate方法參數(shù)有Bundle
    pool.importPackage("android.os.Bundle");

    File dir = new File(path);
    if (dir.isDirectory()) {
        //遍歷文件夾
        dir.eachFileRecurse { File file ->
            String filePath = file.absolutePath
            println("filePath = " + filePath)
            if (file.getName().equals("MainActivity.class")) {

                //獲取MainActivity.class
                CtClass ctClass = pool.getCtClass("com.zxy.plugin.MainActivity");
                println("ctClass = " + ctClass)
                //解凍
                if (ctClass.isFrozen())
                    ctClass.defrost()

                //獲取到OnCreate方法
                CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")

                println("方法名 = " + ctMethod)

                String insetBeforeStr = """ android.widget.Toast.makeText(this,"WTF emmmmmmm.....我是被插了的Toast代碼~!!",android.widget.Toast.LENGTH_SHORT).show();
                                            """
                //在方法開頭插入代碼
                ctMethod.insertBefore(insetBeforeStr);
                ctClass.writeFile(path)
                ctClass.detach()//釋放
            }
        }
    }

}
}

通過反編譯可以看到我們成功的注入了一個Toast


運行效果


總結

還是那句話,本章節(jié)是讓我們了解plugin和javassist結合使用入門,很多插件化等技術都會用到javassist,需要我們更多的深入了解和探索,無論是自定義gradle還是注入代碼這些技術都是通往大牛之路的必備技能,有描述錯誤的地方歡迎童鞋們指出。


點贊加關注是給我最大的鼓勵!

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 前言 replugin-plugin-gradle 是 RePlugin 插件框架中提供給replugin插件用的...
    osan閱讀 7,274評論 8 33
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,671評論 25 709
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,500評論 19 139
  • 三八節(jié),林子去參加單位活動了。海子接到武侯法院的電話,讓去拿李凱起訴的傳票,涉案金額400萬本金以及利息。很快海子...
    海洋星芃閱讀 161評論 0 0
  • 畫家用畫詮釋世界,雕刻家用作品詮釋世界,每個人都有自己詮釋世界的方法,你是怎么詮釋自己世界的呢?
    自由小自由閱讀 210評論 0 1

友情鏈接更多精彩內(nèi)容