QQ空間曾經發布的熱修復解決方案中利用Javaassist
庫實現向類的構造函數中插入一段代碼解決CLASS_ISPREVERIFIED 問題。包括了Instant Run的實現以及參照Instant Run實現的熱修復美團Robus等都利用到了插樁技術。html
插樁就是將一段代碼插入或者替換本來的代碼。字節碼插樁顧名思義就是在咱們編寫的源碼編譯成字節碼(Class)後,在Android下生成dex以前修改Class文件,修改或者加強原有代碼邏輯的操做。java
咱們須要查看方法執行耗時,若是每個方法都須要本身手動去加入這些內容,當不須要時也須要一個個刪去相應的代碼。1個、兩個方法還好,若是有10個、20個得多麻煩!因此能夠利用註解來標記須要插樁的方法,結合編譯後操做字節碼來幫助咱們自動插入,當不須要時關掉插樁便可。這種AOP思想讓咱們只須要關注插樁代碼自己。android
上面咱們提到QQ空間使用了Javaassist
來進行字節碼插樁,除了Javaassist
以外還有一個應用更爲普遍的ASM
框架一樣也是字節碼操做框架,Instant Run包括AspectJ
就是藉助ASM
來實現各自的功能。web
咱們很是熟悉的JSON格式數據是基於文本的,咱們只須要知道它的規則就可以輕鬆的生成、修改JSON數據。一樣的Class字節碼也有其本身的規則(格式)。操做JSON能夠藉助GSON來很是方便的生成、修改JSON數據。而字節碼Class,一樣能夠藉助Javassist/ASM來實現對其修改。shell
字節碼操做框架的做用在於生成或者修改Class文件,所以在Android中字節碼框架自己是不須要打包進入APK的,只有其生成/修改以後的Class才須要打包進入APK中。它的工做時機在上圖Android打包流程中的生成Class以後,打包dex以前。api
因爲ASM
具備相對於Javassist
更好的性能以及更高的靈活行,咱們這篇文章以使用ASM爲主。在真正利用到Android中以前,咱們能夠先在Java
程序中完成對字節碼的修改測試。markdown
ASM
能夠直接從jcenter()
倉庫中引入,因此咱們能夠進入:bintray.com/進行搜索app
點擊圖中標註的工件進入,能夠看到最新的正式版本爲:7.1。框架
所以,咱們能夠在AS中加入:ide
同時,須要注意的是:咱們使用testImplementation
引入,這表示咱們只能在Java的單元測試中使用這個框架,對咱們Android中的依賴關係沒有任何影響。
AS中使用gradle的Android工程會自動建立Java單元測試與Android單元測試。測試代碼分別在test與androidTest。
在test/java
下面建立一個Java類:
public class InjectTest {
public static void main(String[] args) {
}
}
複製代碼
因爲咱們操做的是字節碼插樁,因此能夠進入test/java
下面使用javac
對這個類進行編譯生成對應的class文件。
javac InjectTest.java
複製代碼
由於main
方法中沒有任何輸出代碼,咱們輸入命令:java InjectTest
執行這個Class不會有任何輸出。那麼咱們接下來利用ASM
,向main
方法中插入一開始圖中的記錄函數執行時間的日誌輸出。
在單元測試中寫入測試方法
/** * 一、準備待分析的class */
FileInputStream fis = new FileInputStream
("xxxxx/test/java/InjectTest.class");
/** * 二、執行分析與插樁 */
//class字節碼的讀取與分析引擎
ClassReader cr = new ClassReader(fis);
// 寫出器 COMPUTE_FRAMES 自動計算全部的內容,後續操做更簡單
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//分析,處理結果寫入cw EXPAND_FRAMES:棧圖以擴展格式進行訪問
cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);
/** * 三、得到結果並輸出 */
byte[] newClassBytes = cw.toByteArray();
File file = new File("xxx/test/java2/");
file.mkdirs();
FileOutputStream fos = new FileOutputStream
("xxx/test/java2/InjectTest.class");
fos.write(newClassBytes);
fos.close();
複製代碼
關於ASM框架自己的設計,咱們這裏先不討論。上面的代碼會獲取上一步生成的class,而後由ASM執行完插樁以後,將結果輸出到test/java2
目錄下。其中關鍵點就在於第2步中,如何進行插樁。
把class數據交給ClassReader
,而後進行分析,相似於XML解析,分析結果會以事件驅動的形式告知給accept
的第一個參數ClassAdapterVisitor
。
public class ClassAdapterVisitor extends ClassVisitor {
public ClassAdapterVisitor(ClassVisitor cv) {
super(Opcodes.ASM7, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("方法:" + name + " 簽名:" + desc);
MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
return new MethodAdapterVisitor(api,mv, access, name, desc);
}
}
複製代碼
分析結果經過ClassAdapterVisitor
得到,一個類中會存在方法、註解、屬性等,所以ClassReader
會將調用ClassAdapterVisitor
中對應的visitMethod
、visitAnnotation
、visitField
這些visitXX
方法。
咱們的目的是進行函數插樁,所以重寫visitMethod
方法,在這個方法中咱們返回一個MethodVisitor
方法分析器對象。一個方法的參數、註解以及方法體須要在MethodVisitor
中進行分析與處理。
package com.enjoy.asminject.example;
import com.enjoy.asminject.ASMTest;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;
/** * AdviceAdapter: 子類 * 對methodVisitor進行了擴展, 能讓咱們更加輕鬆的進行方法分析 */
public class MethodAdapterVisitor extends AdviceAdapter {
private boolean inject;
protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
/** * 分析方法上面的註解 * 在這裏幹嗎??? * <p> * 判斷當前這個方法是否是使用了injecttime,若是使用了,咱們就須要對這個方法插樁 * 沒使用,就無論了。 * * @param desc * @param visible * @return */
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if (Type.getDescriptor(ASMTest.class).equals(desc)) {
System.out.println(desc);
inject = true;
}
return super.visitAnnotation(desc, visible);
}
private int start;
@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//執行完了怎麼辦? 記錄到本地變量中
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
start = newLocal(Type.LONG_TYPE); //建立本地 LONG類型變量
//記錄 方法執行結果給建立的本地變量
storeLocal(start);
}
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
if (inject){
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
int end = newLocal(Type.LONG_TYPE);
storeLocal(end);
getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" +
"/PrintStream;"));
//分配內存 並dup壓入棧頂讓下面的INVOKESPECIAL 知道執行誰的構造方法建立StringBuilder
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
dup();
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("<init>","()V"));
visitLdcInsn("execute:");
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
//減法
loadLocal(end);
loadLocal(start);
math(SUB,Type.LONG_TYPE);
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;"));
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;"));
invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V"));
}
}
}
複製代碼
MethodAdapterVisitor
繼承自AdviceAdapter
,其實就是MethodVisitor
的子類,AdviceAdapter
封裝了指令插入方法,更爲直觀與簡單。
上述代碼中onMethodEnter
進入一個方法時候回調,所以在這個方法中插入指令就是在整個方法最開始加入一些代碼。咱們須要在這個方法中插入long s = System.currentTimeMillis();
。在onMethodExit
中即方法最後插入輸出代碼。
@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//執行完了怎麼辦? 記錄到本地變量中
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
start = newLocal(Type.LONG_TYPE); //建立本地 LONG類型變量
//記錄 方法執行結果給建立的本地變量
storeLocal(start);
}
}
複製代碼
這裏面的代碼怎麼寫?其實就是long s = System.currentTimeMillis();
這句代碼的相對的指令。咱們能夠先寫一份代碼
void test(){
//插入的代碼
long s = System.currentTimeMillis();
/** * 方法實現代碼.... */
//插入的代碼
long e = System.currentTimeMillis();
System.out.println("execute:"+(e-s)+" ms.");
}
複製代碼
而後使用javac
編譯成Class再使用javap -c
查看字節碼指令。也能夠藉助插件來查看,就不須要咱們手動執行各類命令。
安裝完成以後,能夠在須要插樁的類源碼中點擊右鍵:
點擊ASM Bytecode Viewer以後會彈出
因此第20行代碼:long s = System.currentTimeMillis();
會包含兩個指令:INVOKESTATIC
與LSTORE
。
再回到onMethodEnter
方法中
@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//invokeStatic指令,調用靜態方法
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
//建立本地 LONG類型變量
start = newLocal(Type.LONG_TYPE);
//store指令 將方法執行結果從操做數棧存儲到局部變量
storeLocal(start);
}
}
複製代碼
而onMethodExit
也一樣根據指令去編寫代碼便可。最終執行完插樁以後,咱們就能夠得到修改後的class數據。
在Android中實現,咱們須要考慮的第一個問題是如何得到全部的Class文件來判斷是否須要插樁。Transform就是幹這件事情的。