Android程序員的硬通貨——ASM字節碼插樁

做者:享學課堂Lance老師java

轉載請聲明出處!android

1、什麼是插樁

QQ空間曾經發布的《熱修復解決方案》中利用 Javaassist庫實現向類的構造函數中插入一段代碼解決 CLASS_ISPREVERIFIED問題。包括了Instant Run的實現以及參照Instant Run實現的熱修復美團Robus等都利用到了插樁技術。web

插樁就是將一段代碼插入或者替換本來的代碼。字節碼插樁顧名思義就是在咱們編寫的源碼編譯成字節碼(Class)後,在Android下生成dex以前修改Class文件,修改或者加強原有代碼邏輯的操做。api

咱們須要查看方法執行耗時,若是每個方法都須要本身手動去加入這些內容,當不須要時也須要一個個刪去相應的代碼。一個、兩個方法還好,若是有10個、20個得多麻煩!因此能夠利用註解來標記須要插樁的方法,結合編譯後操做字節碼來幫助咱們自動插入,當不須要時關掉插樁便可。這種AOP思想讓咱們只須要關注插樁代碼自己。bash

2、字節碼操做框架

上面咱們提到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

3、ASM的使用

因爲 ASM具備相對於 Javassist更好的性能以及更高的靈活行,咱們這篇文章以使用ASM爲主。在真正利用到Android中以前,咱們能夠先在 Java程序中完成對字節碼的修改測試。函數

3.一、在AS中引入ASM

ASM能夠直接從 jcenter()倉庫中引入,因此咱們能夠進入:bintray.com/進行搜索性能

點擊圖中標註的工件進入,能夠看到最新的正式版本爲:7.1。

所以,咱們能夠在AS中加入:

同時,須要注意的是:咱們使用 testImplementation引入,這表示咱們只能在Java的單元測試中使用這個框架,對咱們Android中的依賴關係沒有任何影響。

AS中使用gradle的Android工程會自動建立Java單元測試與Android單元測試。測試代碼分別在test與androidTest。

3.二、準備待插樁Class

在 test/java下面建立一個Java類:

public class InjectTest {
       public static void main(String[] args) {
    }
}
複製代碼

因爲咱們操做的是字節碼插樁,因此能夠進入 test/java下面使用 javac對這個類進行編譯生成對應的class文件。

javac InjectTest.java
複製代碼

3.三、執行插樁

由於 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中對應的 visitMethodvisitAnnotationvisitField這些 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();會包含兩個指令: INVOKESTATICLSTORE

再回到 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數據。

4、Android中的實現

在Android中實現,咱們須要考慮的第一個問題是如何得到全部的Class文件來判斷是否須要插樁。Transform就是幹這件事情的。

關注我,還有更多技術乾貨分享~

相關文章
相關標籤/搜索