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之前。
傳送門: ofollow,noindex">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的編譯過程,可以做很多好玩有意義的事情!