做者:享學課堂Lance老師java
轉載請聲明出處!android
QQ空間曾經發布的《熱修復解決方案》中利用 Javaassist
庫實現向類的構造函數中插入一段代碼解決 CLASS_ISPREVERIFIED問題。包括了Instant Run的實現以及參照Instant Run實現的熱修復美團Robus等都利用到了插樁技術。web
插樁就是將一段代碼插入或者替換本來的代碼。字節碼插樁顧名思義就是在咱們編寫的源碼編譯成字節碼(Class)後,在Android下生成dex以前修改Class文件,修改或者加強原有代碼邏輯的操做。api
咱們須要查看方法執行耗時,若是每個方法都須要本身手動去加入這些內容,當不須要時也須要一個個刪去相應的代碼。一個、兩個方法還好,若是有10個、20個得多麻煩!因此能夠利用註解來標記須要插樁的方法,結合編譯後操做字節碼來幫助咱們自動插入,當不須要時關掉插樁便可。這種AOP思想讓咱們只須要關注插樁代碼自己。bash
上面咱們提到QQ空間使用了 Javaassist
來進行字節碼插樁,除了 Javaassist
以外還有一個應用更爲普遍的 ASM
框架一樣也是字節碼操做框架,Instant Run包括 AspectJ
就是藉助 ASM
來實現各自的功能。app
咱們很是熟悉的JSON格式數據是基於文本的,咱們只須要知道它的規則就可以輕鬆的生成、修改JSON數據。一樣的Class字節碼也有其本身的規則(格式)。操做JSON能夠藉助GSON來很是方便的生成、修改JSON數據。而字節碼Class,一樣能夠藉助Javassist/ASM來實現對其修改。框架
字節碼操做框架的做用在於生成或者修改Class文件,所以在Android中字節碼框架自己是不須要打包進入APK的,只有其生成/修改以後的Class才須要打包進入APK中。它的工做時機在上圖Android打包流程中的生成Class以後,打包dex以前。ide
因爲 ASM
具備相對於 Javassist
更好的性能以及更高的靈活行,咱們這篇文章以使用ASM爲主。在真正利用到Android中以前,咱們能夠先在 Java
程序中完成對字節碼的修改測試。函數
ASM
能夠直接從 jcenter()
倉庫中引入,因此咱們能夠進入:bintray.com/進行搜索性能
點擊圖中標註的工件進入,能夠看到最新的正式版本爲:7.1。
所以,咱們能夠在AS中加入:
同時,須要注意的是:咱們使用 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
方法中沒有任何輸出代碼,咱們輸入命令:javaInjectTest
執行這個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
進入一個方法時候回調,所以在這個方法中插入指令就是在整個方法最開始加入一些代碼。咱們須要在這個方法中插入 longs=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);
}
}
複製代碼
這裏面的代碼怎麼寫?其實就是 longs=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行代碼: longs=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就是幹這件事情的。
關注我,還有更多技術乾貨分享~