Android動態編譯技術 Plugin Transform Javassist操作Class文件 前言 總結

前言

繼續上一章節自定義Gradle插件,利用plugin進一步做一些事情

本章節利用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中繼續完成使用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

接下來創建一個類繼承Transform 並實現其方法

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";
}

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

//    指Transform要操作內容的範圍,官方文檔Scope有7種類型:

//    EXTERNAL_LIBRARIES        只有外部庫
//    PROJECT                       只有項目內容
//    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,上一章節已經有介紹過apply入口

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

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

//我們自定義的
testCreatJavaConfig{
str = "動態生成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);

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

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

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

            //創建一個task
            def createTaskName = scope.getTaskName("CeShi", "MyTestPlugin")
            def createTask = project.task(createTaskName)
            //設置task要執行的任務
            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) {
    //要生成的內容
    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是一個動態類庫,可以用來檢查、”動態”修改以及創建 Java類。其功能與jdk自帶的反射功能類似,但比反射功能更強大
  • ClassPool:javassist的類池,使用ClassPool 類可以跟蹤和控制所操作的類,它的工作方式與 JVM 類裝載器非常相似,
    CtClass: CtClass提供了檢查類數據(如字段和方法)以及在類中添加新字段、方法和構造函數、以及改變類、父類和接口的方法。不過,Javassist 並未提供刪除類中字段、方法或者構造函數的任何方法。
    CtField:用來訪問域
    CtMethod :用來訪問方法
    CtConstructor:用來訪問構造器

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

下面我們利用Transform在MainActivity中動態的插入代碼,先看一下現在的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中使用的是我們上面動態生成的類中的字段,看一下怎麼利用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類中,在這個類中我們傳入了兩個參數,一個是當前變量的文件夾,一個是當前的工程對象,來看一下代碼

 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方法參數有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


運行效果


總結

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


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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章