前言
本章節更新的慢了些,最近公司多事之秋,今天靜下心來把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的編譯過程,可以做很多好玩有意義的事情!