javassist 動態修改字節碼

一、gradle Transform 接收一個輸入input,同時需要有一組輸出,作為下一個Transform的輸入。

(1)最簡單的一個Transform實現,需要實現
將輸入數據input,原樣不動輸出到output
(2)Transform處理的結果,會位于工程目錄?/build?/?intermediates?/transform文件夾下。
如下圖XXX目錄即為自定義的一個Transfrom。
由圖可知除XXX外,還經過了dexBuilder、dexMerger、mergeJavaRes、mergeJniLibs、StripDebugSymbol等多個Transform處理

image.png

二、自定義gradle插件實例

1、自定義gradle插件的build.gradle

apply plugin: 'groovy'
apply plugin: 'maven'
apply plugin: 'java'
apply plugin: 'maven-publish'

dependencies {
//    implementation fileTree(dir: 'libs', include: ['*.jar'])

    compile gradleApi()//gradle sdk
    compile localGroovy()//groovy sdk


    //build tools
    compile 'com.android.tools.build:gradle:3.1.2'
    //transform
    compile 'com.android.tools.build:transform-api:1.5.0'
    //javassit
    compile 'javassist:javassist:3.12.1.GA'
    //commons-io
    compile 'commons-io:commons-io:2.5'
}
repositories {
    jcenter()
    google()//加在這里
}


SecondPlugin.groovy 自定義插件,內部為android注冊了一個ReClassTransform 接口。

package com.feifei.second

import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.feifei.second.transform.ReClassTransform
import org.gradle.api.Plugin
import org.gradle.api.Project

public class SecondPlugin implements Plugin<Project>{

    void apply(Project project){
        System.out.println("==========")
        System.out.println("feifei  第二個內部用插件")
        System.out.println("==========")

        project.extensions.create("pluginExt",PluginExtension)
        project.pluginExt.extensions.create("nestExt", PluginNestExtension)
        project.task('customTask',type:CustomTask)

        def isApp = project.plugins.getPlugin(AppPlugin)

        if(isApp){
            def android =  project.extensions.getByType(AppExtension)
            android.registerTransform(new ReClassTransform(project))
        }
    }
}

最原始的Transform實現。
ReClassTransfrom.groovy

package com.feifei.second.transform
import com.android.build.api.transform.*
import com.android.build.api.transform.Context
import com.android.build.api.transform.DirectoryInput
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.TransformInput
import com.android.build.api.transform.TransformOutputProvider
import com.android.utils.FileUtils
import org.apache.commons.codec.digest.DigestUtils
import org.gradle.api.Project
import org.gradle.internal.impldep.org.apache.ivy.util.FileUtil
import org.gradle.jvm.tasks.Jar
import com.android.build.gradle.internal.pipeline.TransformManager

import javax.xml.crypto.dsig.TransformException

public class ReClassTransform extends Transform{

    private Project mProject;

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

    //transform的名稱
    /**
     * 最終運行的名字為 transformClassWith+getName()+For+{BuildType}+{ProductFlavor}
     * 如 transformClassWithXXXForDebug
     * @return
     */
    @Override
    String getName() {
        return "XXX"
    }

    /**
     * 需要處理的數據類型,有兩種枚舉類型
     * CLASSES和RESOURCES,CLASSES代表處理的java的class文件;RESOURCES代表要處理java的資源.
     * @return
     */
    @Override
    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          由當前變量(包括依賴項)測試的代碼
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }
//指明當前Transform是否支持增量編譯
    @Override
    boolean isIncremental() {
        return false
    }

    /**
     * Transform中的核心方法,
     *
     * @param context 。
     * @param inputs  傳過來的輸入流, 其中有兩種格式,一種是jar包格式一種是目錄格式
     * @param referencedInputs
     * @param outputProvider  獲取到輸出目錄,最后將修改的文件復制到輸出目錄,這一步必須做不然編譯會報錯
     * @param isInCremental
     * @throws IOException
     * @throws TransformException
     */
    @Override
    public void transform(Context context,
                          Collection<TransformInput> inputs,
            Collection<TransformInput> referencedInputs,
            TransformOutputProvider outputProvider,
            boolean isInCremental
    ) throws IOException, TransformException{

        welecome()

        inputs.each { TransformInput input->

            //遍歷目錄
            input.directoryInputs.each { DirectoryInput directoryInput ->

                println "direction = "+directoryInput.file.getAbsolutePath()
                //獲取輸出目錄
                def dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)

                //對于目錄中的class文件原樣輸出
                FileUtils.copyDirectory(directoryInput.file,dest)
            }

            //遍歷jar文件,對jar不操作,但是要輸出到out目錄
            input.jarInputs.each { JarInput jarInput->

                // 將jar文件 重命名輸出文件(同目錄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)
            }

        }

        end()

    }


    def welecome(){
        println "----welcome to ReClassTransform"
    }

    def end(){
        println "----ReClassTransform end"
    }
}

執行./gradlew :test_gradle_use_plugin:assembleDebug
時的輸出內容。

> Task :test_gradle_use_plugin:transformClassesWithXXXForDebug 
----welcome to ReClassTransform
jar = /Users/feifei/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.0.aar/ad39ea76672d18218cf29f42ea94a4d7/jars/classes.jar
jar = /Users/feifei/.gradle/caches/modules-2/files-2.1/com.android.support.constraint/constraint-layout-solver/1.1.0/931532e953a477f876f2de18c2e7f16eee01078f/constraint-layout-solver-1.1.0.jar
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/intermediates/classes/debug
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/tmp/kotlin-classes/debug
----ReClassTransform end

2、利用向文件中寫入字符串的形式直接生成類文件

Hostconfig.groovy
增加HostConfig的調用

package com.feifei.second.hostconfig


public class HostConfig {
    static def void createHostConfig(variant,config){

def content = """
package com.sogou.teemo.test_use_gradle_plugin;
public class TheHostConfig{
    public static final String ip = "${config.param1}";
    public static final String port = "5050"; 

}

"""

        File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
        println "feifei createHostConfig outputDir:"+outputDir.getAbsolutePath()
        def javaFile = new File(outputDir, "TheHostConfig.java")
        javaFile.write(content,'UTF-8')

    }
}

SecondPlugin.groovy

package com.feifei.second

import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.api.ApplicationVariant
import com.android.repository.impl.meta.Archive
import com.feifei.second.hostconfig.HostConfig
import com.feifei.second.transform.ReClassTransform
import org.gradle.api.Plugin
import org.gradle.api.Project

public class SecondPlugin implements Plugin<Project>{

    void apply(Project project){
        System.out.println("==========")
        System.out.println("feifei  第二個內部用插件")
        System.out.println("==========")

        project.extensions.create("pluginExt",PluginExtension)
        project.pluginExt.extensions.create("nestExt", PluginNestExtension)
        project.task('customTask',type:CustomTask)

        def isApp = project.plugins.getPlugin(AppPlugin)

        if(isApp){
            def android =  project.extensions.getByType(AppExtension)
            android.registerTransform(new ReClassTransform(project))



            android.applicationVariants.all { variants->

                def variantData =  variants.variantData
                def scope = variantData.scope

                println "feifei current scope:"+scope

                //scope.getTaskName 的作用 就是結合當前scope 拼接人物名
                def taskName = scope.getTaskName("CreateHostConfig")
                def createTask = project.task(taskName)

                println "feifei CreateHostConfigTaskName:"+taskName

                //自定義task 增加action
                createTask.doLast {
                    HostConfig.createHostConfig(variants,project.pluginExt)
                }

                String generateBuildConfigTaskName = scope.getGenerateBuildConfigTask().name
                def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
                println "feifei  generateBuildConfigTaskName:"+generateBuildConfigTaskName

                if(generateBuildConfigTask){
                    createTask.dependsOn generateBuildConfigTask
                    generateBuildConfigTask.finalizedBy(createTask)//執行完generateBuildConfigTask之后,執行createTask任務
                }
            }
        }
    }
}

執行 ./gradlew clean :test_gradle_use_plugin:assembleDebug
輸出如下:

 Configure project :test_gradle_use_plugin 
==========
feifei  第二個內部用插件
==========
feifei current scope:VariantScopeImpl{debug}
feifei CreateHostConfigTaskName:CreateHostConfigDebug
feifei  generateBuildConfigTaskName:generateDebugBuildConfig
feifei current scope:VariantScopeImpl{release}
feifei CreateHostConfigTaskName:CreateHostConfigRelease
feifei  generateBuildConfigTaskName:generateReleaseBuildConfig

> Task :test_gradle_use_plugin:transformClassesWithXXXForDebug 
----welcome to ReClassTransform
jar = /Users/feifei/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.0.aar/5ae74cdeff58ee396218df991052866b/jars/classes.jar
jar = /Users/feifei/.gradle/caches/modules-2/files-2.1/com.android.support.constraint/constraint-layout-solver/1.1.0/931532e953a477f876f2de18c2e7f16eee01078f/constraint-layout-solver-1.1.0.jar
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/intermediates/classes/debug
direction = /Users/feifei/Desktop/TM/Github/MyExampleCode/test_gradle_use_plugin/build/tmp/kotlin-classes/debug
----ReClassTransform end




生成類文件的位置:

image.png

3、利用javassist 向現有類中動態插入代碼

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

新建
CodeInjects.groovy 用于想MainActivity中動態插入代碼

package com.feifei.second.codeinject

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

public class CodeInjects {
    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())

        pool.importPackage("android.os.Bundle");
        pool.importPackage(" android.app.Activity")

        File dir = new File(path)
        if(dir.isDirectory()){
            //遍歷目錄
            dir.eachFileRecurse {File file->
                String filePath = file.absolutePath
                println("CodeInjects filePath:"+filePath)
                if(file.getName().equals("MainActivity.class")){

                    //獲取MainActivity.class
                    CtClass ctClass = pool.getCtClass("com.sogou.teemo.test_use_gradle_plugin.MainActivity");
                    println("CodeInjects ctClass = "+ctClass)

                    if(ctClass.isFrozen()){
                        ctClass.defrost()
                    }

                    //獲取到onCreate方法
                    CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate");
                    println("CodeInjects 方法名 = " + ctMethod)

                    String insetBeforeStr = """ android.widget.Toast.makeText(this,"插件中自動生成的代碼",android.widget.Toast.LENGTH_SHORT).show();
                                            """

                    ctMethod.insertAfter(insetBeforeStr)

                    ctClass.writeFile(path)

                    ctClass.detach()//釋放

                }
            }
        }


    }


}

ReClassTransform.groovy中,遍歷class文件時,調用CodeInjects.inject(directoryInput.file.absolutePath,mProject)。過濾出MainActivity.class并動態修改onCreate()方法

  /**
     * Transform中的核心方法,
     *
     * @param context 。
     * @param inputs  傳過來的輸入流, 其中有兩種格式,一種是jar包格式一種是目錄格式
     * @param referencedInputs
     * @param outputProvider  獲取到輸出目錄,最后將修改的文件復制到輸出目錄,這一步必須做不然編譯會報錯
     * @param isInCremental
     * @throws IOException
     * @throws TransformException
     */
    @Override
    public void transform(Context context,
                          Collection<TransformInput> inputs,
            Collection<TransformInput> referencedInputs,
            TransformOutputProvider outputProvider,
            boolean isInCremental
    ) throws IOException, TransformException{

        welecome()

        inputs.each { TransformInput input->

            //遍歷目錄
            input.directoryInputs.each { DirectoryInput directoryInput ->

                println "direction = "+directoryInput.file.getAbsolutePath()

                CodeInjects.inject(directoryInput.file.absolutePath,mProject)
                //獲取輸出目錄
                def dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)

                //對于目錄中的class文件原樣輸出
                FileUtils.copyDirectory(directoryInput.file,dest)
            }

            //遍歷jar文件,對jar不操作,但是要輸出到out目錄
            input.jarInputs.each { JarInput jarInput->

                // 將jar文件 重命名輸出文件(同目錄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)
            }

        }

        end()

    }

將test_gradle_use_plugin-debug.apk 反編譯后,如下圖所示:


image.png

Github: 查看buildSrc 和test_gradle_use_plugin 兩個module

四、相關知識背景

1、Transfrom API

基于Gradle的Transform API,在編譯期的構建任務流中,class轉為dex之前,插入一個Transform,并在此Transform流中,基于Javassist實現對字節碼文件的注入。
[圖片上傳失敗...(image-317838-1563938953106)]
http://google.github.io/android-gradle-dsl/javadoc/current/

2、javassist

Javassist是一個動態類庫,可以用來檢查、”動態”修改以及創建 Java類.其功能與jdk自帶的反射功能類似,但比反射功能更強大.

  • ClassPool:javassist的類池,使用ClassPool 類可以跟蹤和控制所操作的類,它的工作方式與 JVM 類裝載器非常相似。
  • CtClass: CtClass提供了檢查類數據(如字段和方法)以及在類中添加新字段、方法和構造函數、以及改變類、父類和接口的方法。不過,Javassist 并未提供刪除類中字段、方法或者構造函數的任何方法。
  • CtField:用來訪問域
  • CtMethod :用來訪問方法
  • CtConstructor:用來訪問構造器
  • insertClassPath:為ClassPool添加搜索路徑,否則ClassPool 無法找打對應的類
     classPool.insertClassPath(new ClassClassPath(String.class));
            classPool.insertClassPath(new ClassClassPath(Person.class));
            classPool.insertClassPath("/Users/feifei/Desktop/1");
  • classPool.get(className);加載一個類

  • classPool.makeClass(className);//創建一個類

  • CtClass.addField();CtClass.addMethod(); 添加方法和屬性

  CtField ageField = new CtField(CtClass.intType,"age",stuClass);
            stuClass.addField(ageField);
    CtMethod setMethod = CtMethod.make("public void setAge(int age) { this.age = age;}",stuClass);

            stuClass.addMethod(getMethod);
  • Class<?>clazz = stuClass.toClass();將CtCLass對象轉化為JVM對象

創建一個類,并寫入到本地文件

 public static void testCreateClass(){

        System.out.println("testCreateClass");
        //創建ClassPool
        ClassPool classPool = ClassPool.getDefault();

        //添加類路徑
//        classPool.insertClassPath(new ClassClassPath(this.getClass()));
        classPool.insertClassPath(new ClassClassPath(String.class));
        //創建類
        CtClass stuClass = classPool.makeClass("com.feifei.Student");

        //加載類
        //classPool.get(className)
        try {
            //添加屬性
            CtField idField = new CtField(CtClass.longType,"id",stuClass);
            stuClass.addField(idField);

            CtField nameField = new CtField(classPool.get("java.lang.String"),"name",stuClass);
            stuClass.addField(nameField);

            CtField ageField = new CtField(CtClass.intType,"age",stuClass);
            stuClass.addField(ageField);


            //添加方法
            CtMethod getMethod = CtMethod.make("public int getAge(){return this.age;}",stuClass);
            CtMethod setMethod = CtMethod.make("public void setAge(int age) { this.age = age;}",stuClass);

            stuClass.addMethod(getMethod);
            stuClass.addMethod(setMethod);

            //toClass 將CtClass 轉換為java.lang.class
            Class<?>clazz = stuClass.toClass();
            System.out.println("testCreateClass clazz:"+clazz);

            System.out.println("testCreateClas ------ 屬性列表 -----");
            Field[] fields = clazz.getDeclaredFields();
            for(Field field:fields){
                System.out.println("testCreateClass"+field.getType()+"\t"+field.getName());
            }

            System.out.println("testCreateClass ------ 方法列表 -----");

            Method[] methods = clazz.getDeclaredMethods();
            for(Method method:methods){
                System.out.println("feifei  "+method.getReturnType()+"\t"+method.getName()+"\t"+ Arrays.toString(method.getParameterTypes()));
            }

            stuClass.writeFile("/Users/feifei/Desktop/1");
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }catch (Exception e){
            e.printStackTrace();
        }finally {

            //將stuClass 從ClassPool 移除
            if(stuClass != null){
                stuClass.detach();
            }
        }

    }

修改一個類的父類


package com.example.myjavassist;

public class Person {
}

public static void testSetSuperClass(){

        System.out.println("testSetSuperClass");
        //創建ClassPool
        ClassPool classPool = ClassPool.getDefault();


        try {
            //添加類路徑
            classPool.insertClassPath(new ClassClassPath(String.class));
            classPool.insertClassPath(new ClassClassPath(Person.class));
            classPool.insertClassPath("/Users/feifei/Desktop/1");

            // 加載類
            //創建類
            CtClass stuClass = classPool.get("com.feifei.Student");
            CtClass personClass = classPool.get("com.example.myjavassist.Person");

            if(stuClass.isFrozen()){
                stuClass.freeze();
            }
            stuClass.setSuperclass(personClass);

            //toClass 將CtClass 轉換為java.lang.class
            Class<?>clazz = stuClass.toClass();
            System.out.println("testSetSuperClass ------ 屬性列表 -----");
            Field[] fields = clazz.getDeclaredFields();
            for(Field field:fields){
                System.out.println("testCreateClass"+field.getType()+"\t"+field.getName());
            }

            System.out.println("testSetSuperClass ------ 方法列表 -----");

            Method[] methods = clazz.getDeclaredMethods();
            for(Method method:methods){
                System.out.println("testSetSuperClass  "+method.getReturnType()+"\t"+method.getName()+"\t"+ Arrays.toString(method.getParameterTypes()));
            }

            stuClass.writeFile("/Users/feifei/Desktop/1");
            personClass.writeFile("/Users/feifei/Desktop/1");

        } catch (NotFoundException | CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {

        }
    }
image.png

方法重命名、復制方法、新建方法,添加方法體。


package com.example.myjavassist;

public class Calculator {

    public void getSum(long n) {
        long sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        System.out.println("n="+n+",sum="+sum);
    }

}


 public static void testInsertMethod(){

        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = null;
        try {
            ctClass = pool.get("com.example.myjavassist.Calculator");

            //獲取類中現有的方法
            String getSumName = "getSum";
            CtMethod methodOld = ctClass.getDeclaredMethod(getSumName);


            String methodNewName = getSumName+"$impl";
            //修改原有方法的方法名
            methodOld.setName(methodNewName);


            //創建一個新的方法getSumName,并將舊方法 復制成新方法中.
            CtMethod newMethod = CtNewMethod.copy(methodOld,getSumName,ctClass,null);

            //設置新newMethod的方法體
            StringBuffer body = new StringBuffer();
            body.append("{\nlong start = System.currentTimeMillis();\n");
            // 調用原有代碼,類似于method();($$)表示所有的參數
            body.append(methodNewName + "($$);\n");
            body.append("System.out.println(\"Call to method " + methodNewName
                    + " took \" +\n (System.currentTimeMillis()-start) + " + "\" ms.\");\n");
            body.append("}");

            newMethod.setBody(body.toString());

            //為類新添加方法
            ctClass.addMethod(newMethod);

            Calculator calculator =(Calculator)ctClass.toClass().newInstance();
            calculator.getSum(10000);

            //將類輸出到文件
            ctClass.writeFile("/Users/feifei/Desktop/1");

        } catch (NotFoundException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            if(ctClass!=null){
                ctClass.detach();
            }
        }
    }
image.png

Github: 選擇 myjavaassit module

五、參考文章

http://www.lxweimin.com/p/a6be7cdcfc65

http://www.lxweimin.com/p/a9b3aaba8e45

https://blog.csdn.net/top_code/article/details/51708043

http://www.javassist.org/tutorial/tutorial2.html

javassit github:
https://github.com/jboss-javassist/javassist

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容