Android AOP三劍客之Javassist

前言

本章節更新的慢了些,最近公司多事之秋,今天靜下心來把AOP最後入門篇補上,做事還要有頭和尾的。

Javassist

Javassist作用是在編譯器間修改class文件,與之相似的ASM(熱修復框架女媧)也有這個功能,可以讓我們直接修改編譯後的class二進制代碼,首先我們得知道什麼時候編譯完成,並且我們要趕在class文件被轉化爲dex文件之前去修改。在Transfrom這個api出來之前,想要在項目被打包成dex之前對class進行操作,必須自定義一個Task,然後插入到predex或者dex之前,在自定義的Task中可以使用javassist或者asm對class進行操作。而Transform則更爲方便,Transfrom會有他自己的執行時機,不需要我們插入到某個Task前面。Tranfrom一經註冊便會自動添加到Task執行序列中,並且正好是項目被打包成dex之前。

傳送門:android-aop-samples

定義JavassistPlugin,固定寫法沒啥說的

public class JavassistPlugin 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 JavassistTransform(project);
    android.registerTransform(classTransform);

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

Transfrom

Gradle是通過一個一個Task執行完成整個流程的,其中肯定也有將所有class打包成dex的task。
(在gradle plugin 1.5 以上和以下版本有些不同)

1.5以下,preDex這個task會將依賴的module編譯後的class打包成jar,然後dex這個task則會將所有class打包成dex
1.5以上,preDex和Dex這兩個task已經消失,取而代之的是TransfromClassesWithDexForDebug

自定義Transfrom

public class JavassistTransform extends Transform {

private Project mProject;

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

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

//需要處理的數據類型,有兩種枚舉類型
//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了----------------")
}

 }

主要說下transform幹嘛的 ???在Transform裏處理Task,通過inputs拿到一些東西,處理完畢之後就輸出outputs,而下一個Task的inputs則是上一個Task的outputs。

MyInjects.inject()插入代碼

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.aop.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_LONG).show();
                                            """
                //在方法開頭插入代碼

                ctMethod.insertBefore(insetBeforeStr);
                ctClass.writeFile(path)
                ctClass.detach()//釋放
            }
        }
    }

}

}

運行效果圖

看下build/intermediates/transforms/HavassustTransform/MainActivity.class

總結

ClassPool、CtClass、CtMethod核心類的使用在這裏展示的很詳細。

1、初始化ClassPool設置
2、通過包名取到對應的CtClass
3、通過CtClass取到CtMethod對應的“OnCreate”方法
4、CtMethodi插入代碼塊,寫文件,釋放,結束整個注入代碼過程。

最後

本章節只是介紹了自定義Transform以及Javassist的三大核心類的使用,通過本章節的學習可以瞭解apk的編譯過程,可以做很多好玩有意義的事情!

作爲老司機,這是彎道超車的必備祕籍,天下武功、唯快不破!

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