一、ASM簡(jiǎn)介
ASM是一個(gè)字節(jié)碼操作框架,可用來(lái)動(dòng)態(tài)生成字節(jié)碼或者對(duì)現(xiàn)有的類進(jìn)行增強(qiáng)怜浅。ASM可以直接生成二進(jìn)制的class字節(jié)碼,也可以在class被加載進(jìn)虛擬機(jī)前動(dòng)態(tài)改變其行為童芹,比如方法執(zhí)行前后插入代碼、添加成員變量鲤拿、修改父類假褪、添加接口等等。
插樁就是將一段代碼插入或者替換原本的代碼皆愉。字節(jié)碼插樁就是在編寫的java源碼編譯成class字節(jié)碼后嗜价,在Android下生成dex之前修改class文件艇抠,修改或者增強(qiáng)原有代碼邏輯的操作幕庐。
二、引入ASM庫(kù)
可以訪問ASM官網(wǎng)家淤,https://asm.ow2.io/index.html异剥,更新asm的版本,目前最新版本9.3
在app/build.gradle下引入asm庫(kù)
dependencies {
/**
* 使用 testImplementation引入絮重,這表示我們只能在Java的單元測(cè)試中使用這個(gè)框架冤寿,
* 對(duì)我們Android中的依賴關(guān)系沒有任何影響
*/
testImplementation 'org.ow2.asm:asm:9.3'
testImplementation 'org.ow2.asm:asm-commons:9.3'
}
引入asm庫(kù)之后,就在src\test目錄下編寫測(cè)試代碼
三青伤、使用ASM進(jìn)行字節(jié)碼插樁
應(yīng)用場(chǎng)景:通過字節(jié)碼插樁計(jì)算方法的執(zhí)行時(shí)間督怜。
3.1 首先編寫測(cè)試類InjectTest.java
package com.xyaty.asmdemo;
/**
* DESC : 測(cè)試類
*/
public class InjectTest {
public static void main(String[] args) throws InterruptedException {
//模擬方法執(zhí)行的時(shí)間
Thread.sleep(1000);
}
public void methodA() {
System.out.println("methodA");
}
}
3.2 接下來(lái)對(duì)InjectTest.java文件通過javac命令進(jìn)行編譯成InjectTest.class
由于我們操作的是字節(jié)碼插樁,也就是class文件狠角,所以需要進(jìn)入 test/java下面使用 javac對(duì)這個(gè)java類進(jìn)行編譯生成對(duì)應(yīng)的class文件号杠,具體操作是:在Android studio底部Terminal窗口,通過cd進(jìn)入到test/java目錄下丰歌,然后執(zhí)行以下命令:
D:\work\plugin\ASMDemo\app\src\test\java>javac com\xyaty\asmdemo\InjectTest.java
執(zhí)行上面的命令編譯后姨蟋,就會(huì)在test/java下面生成對(duì)應(yīng)的InjectTest.class文件,這個(gè)class文件就是待插樁的文件立帖。
生成的InjectTest.class文件如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.xyaty.asmdemo;
public class InjectTest {
public InjectTest() {
}
public static void main(String[] var0) throws InterruptedException {
Thread.sleep(1000L);
}
public void methodA() {
System.out.println("methodA");
}
}
3.3 期望實(shí)現(xiàn)的效果就是利用ASM完成對(duì)InjectTest.class字節(jié)碼的插樁
public static void main(String[] args) throws InterruptedException {
//方法開始的時(shí)間
long start = System.currentTimeMillis();
Thread.sleep(1000);
//方法結(jié)束的時(shí)間
long end = System.currentTimeMillis();
//輸出方法執(zhí)行花費(fèi)的時(shí)間
System.out.println("execute: "+(end - start)+"ms");
}
3.4 編寫測(cè)試類InjectUnitTest.java執(zhí)行插樁
package com.xyaty.asmdemo;
import org.junit.Test;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
/**
* DESC :
*/
public class InjectUnitTest {
/**
* 單元測(cè)試方法眼溶,右擊test()方法,選擇run test()方法即可查看結(jié)果
*/
@Test
public void test() {
try {
//讀取待插樁的class
FileInputStream fis = new FileInputStream(
new File("src/test/java/com/xyaty/asmdemo/InjectTest.class"));
/**
* 執(zhí)行分析與插樁
* ClassReader是class字節(jié)碼的讀取與分析引擎
*/
ClassReader classReader = new ClassReader(fis);
// ClassWriter寫出器晓勇, COMPUTE_FRAMES表示自動(dòng)計(jì)算棧幀和局部變量表的大小
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
/**
* 執(zhí)行分析堂飞,處理結(jié)果寫入classWriter灌旧, EXPAND_FRAMES表示棧圖以擴(kuò)展格式進(jìn)行訪問
* 執(zhí)行插樁的代碼就在MyClassVisitor中實(shí)現(xiàn)
*/
classReader.accept(new MyClassVisitor(Opcodes.ASM9, classWriter), ClassReader.EXPAND_FRAMES);
//獲得執(zhí)行了插樁之后的字節(jié)碼數(shù)據(jù)
byte[] bytes = classWriter.toByteArray();
// 重新寫入InjectTest.class中(也可以寫入到其他class中,InjectTest1.class)绰筛,完成插樁
FileOutputStream fos = new FileOutputStream(
new File("src/test/java/com/xyaty/asmdemo/InjectTest.class"));
fos.write(bytes);
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("visitMethod==>name="+name);
/**
* 會(huì)輸出以下方法:
* visitMethod==>name=<init>
* visitMethod==>name=main
*/
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(api, methodVisitor, access, name,descriptor);
}
}
/**
* 之所以繼承自AdviceAdapter节榜,是因?yàn)锳dviceAdapter是MethodVisitor的子類,
* AdviceAdapter封裝了指令插入方法别智,更為直觀與簡(jiǎn)單宗苍,
* 要使用其中的onMethodEnter和 onMethodExit方法進(jìn)行字節(jié)碼插樁,
*
* 繼承關(guān)系如下:
* AdviceAdapter extends GeneratorAdapter
* GeneratorAdapter extends LocalVariablesSorter
* LocalVariablesSorter extends MethodVisitor
*/
public class MyMethodVisitor extends AdviceAdapter {
long start;
private int startIdentifier;
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
System.out.println("visitAnnotation===>methodName="+getName()+", descriptor="+descriptor);
return super.visitAnnotation(descriptor, visible);
}
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
/**
* 進(jìn)入方法插入內(nèi)容
*/
@Override
protected void onMethodEnter() {
super.onMethodEnter();
// start = System.currentTimeMillis();
/**
* @Type owner 調(diào)用哪個(gè)類
* @Method method 調(diào)用某個(gè)類的靜態(tài)方法(參數(shù)name: 方法名字薄榛,descriptor:方法中參數(shù)和方法返回值類型)
*/
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
//調(diào)用newLocal創(chuàng)建一個(gè)long類型的變量讳窟,返回一個(gè)int類型索引identifier
startIdentifier = newLocal(Type.LONG_TYPE);
//保存到本地變量索引中,用一個(gè)本地變量接收上一步執(zhí)行的結(jié)果
storeLocal(startIdentifier);
}
/**
* 在方法結(jié)尾插入內(nèi)容
* @param opcode
*/
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
// long end = System.currentTimeMillis();
// System.out.println("execute: "+(end - start)+"ms");
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
//調(diào)用newLocal創(chuàng)建一個(gè)long類型的變量敞恋,返回一個(gè)int類型索引identifier
int endIdentifier = newLocal(Type.LONG_TYPE);
//保存到本地變量索引中丽啡,用一個(gè)本地變量接收上一步執(zhí)行的結(jié)果
storeLocal(endIdentifier);
//獲取System的靜態(tài)字段out,類型為PrintStream
getStatic(Type.getType("Ljava/lang/System;"),
"out", Type.getType("Ljava/io/PrintStream;"));
/**
* "execute: "+(end - start)+"ms"實(shí)際是內(nèi)部創(chuàng)建StringBuilder來(lái)拼接
* 源碼:NEW java/lang/StringBuilder
* 創(chuàng)建一個(gè)對(duì)象StringBuilder
*/
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
// dup壓入棧頂,讓下面的INVOKESPECIAL 知道執(zhí)行誰(shuí)的構(gòu)造方法創(chuàng)建StringBuilder
dup();
/**
* 源碼:INVOKESPECIAL java/lang/StringBuilder.<init> ()V
* 創(chuàng)建StringBuilder的構(gòu)造方法硬猫,用init來(lái)代替
*/
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),
new Method("<init>", "()V"));
visitLdcInsn("execute: ");
/**
* 源碼:INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
* 調(diào)用append方法
*/
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
/**
* 對(duì)結(jié)束時(shí)間和開始時(shí)間進(jìn)行減法操作
* LLOAD 3 先加載結(jié)束時(shí)間
* LLOAD 1 后加載開始時(shí)間
* LSUB 執(zhí)行減法操作
*/
loadLocal(endIdentifier);
loadLocal(startIdentifier);
//執(zhí)行減法操作补箍,返回long類型
math(SUB, Type.LONG_TYPE);
/**
* 源碼:INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
* LDC "ms"
*/
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
new Method("append", "(J)Ljava/lang/StringBuilder;"));
//拼接毫秒
visitLdcInsn("ms");
/**
* 源碼:
* INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
* INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
* INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
*/
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
new Method("append", "(Ljava/lang/String;)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"));
}
}
}
在上述代碼中可以看到,
其實(shí)就是在onMethodEnter()方法中插入了:
long start = System.currentTimeMillis();
在onMethodExit()方法中插入了:
long end = System.currentTimeMillis();
System.out.println("execute: "+(end - start)+"ms");
但是使用字節(jié)碼指令卻寫了一大堆代碼啸蜜。
3.5 可以通過android studio的插件ASM(以下三選一)坑雅,查看指令代碼
(1)ASM Bytecode Viewer
(2)ASM Bytecode Viewer Support Kotlin
(3)ASM Bytecode Outline
在AS4.1以上版本使用ASM Bytecode Viewer和ASM Bytecode Outline都報(bào)錯(cuò),可能是AS升級(jí)后沒有做兼容衬横,使用ASM Bytecode Viewer Support Kotlin即可搞定(親測(cè))裹粤。
安裝好ASM之后,重啟AS蜂林,在InjectTest.java中右鍵點(diǎn)擊選項(xiàng)“ASM Bytecode Viewer”查看指令代碼
InjectTest.java文件
package com.xyaty.asmdemo;
/**
* Author :
* Date : 2022/7/21
* DESC :
*/
public class InjectTest {
@ASMTest
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
Thread.sleep(1000);
long end = System.currentTimeMillis();
System.out.println("execute: "+(end - start)+"ms");
}
public void methodA() {
System.out.println("methodA");
}
}
ASM指令代碼截圖:
ASM指令代碼如下:
// class version 51.0 (51)
// access flags 0x21
public class com/xyaty/asmdemo/InjectTest {
// compiled from: InjectTest.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 9 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/xyaty/asmdemo/InjectTest; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V throws java/lang/InterruptedException
@Lcom/xyaty/asmdemo/ASMTest;() // invisible
L0
LINENUMBER 13 L0
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 1
L1
LINENUMBER 14 L1
LDC 1000
INVOKESTATIC java/lang/Thread.sleep (J)V
L2
LINENUMBER 15 L2
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 3
L3
LINENUMBER 16 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "execute: "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LLOAD 3
LLOAD 1
LSUB
INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
LDC "ms"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L4
LINENUMBER 17 L4
RETURN
L5
LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
LOCALVARIABLE start J L1 L5 1
LOCALVARIABLE end J L3 L5 3
MAXSTACK = 6
MAXLOCALS = 5
// access flags 0x1
public methodA()V
L0
LINENUMBER 20 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "methodA"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 21 L1
RETURN
L2
LOCALVARIABLE this Lcom/xyaty/asmdemo/InjectTest; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}
這些指令涉及到了java類型描述符和方法描述遥诉,
①類型描述符
Java代碼中的類型,在字節(jié)碼中有相應(yīng)的表示協(xié)議:
(1)Java基本類型的描述符是單個(gè)字符噪叙,例如Z表示boolean矮锈、C表示char
(2)類的類型的描述符是這個(gè)類的全限定名,前面加上字符L 睁蕾, 后面跟上一個(gè)「;」苞笨,例如String的類型描述符為L(zhǎng)java/lang/String;
(3)數(shù)組類型的描述符是一個(gè)方括號(hào)后面跟有該數(shù)組元素類型的描述符,多維數(shù)組則使用多個(gè)方括號(hào)惫霸。
借助上面的協(xié)議分析猫缭,想要看到字節(jié)碼中參數(shù)的類型,就比較簡(jiǎn)單了壹店。
②方法描述符
方法描述符(方法簽名)是一個(gè)類型描述符列表猜丹,它用一個(gè)字符串描述一個(gè)方法的參數(shù)類型和返回類型。
方法描述符以左括號(hào)開頭硅卢,然后是每個(gè)形參的類型描述符射窒,然后是是右括號(hào)藏杖,接下來(lái)是返回類型的類型描述符,例如脉顿,該方法返回void蝌麸,則是V,要注意的是艾疟,方法描述符中不包含方法的名字或參數(shù)名来吩。
比如:
void m(int i, float f)對(duì)應(yīng)的方法描述符是(IF)V ,表明該方法會(huì)接收一個(gè)int和float型參數(shù)蔽莱,且無(wú)返回值弟疆。
int m(Object o)對(duì)應(yīng)的方法描述符是(Ljava/lang/Object;)I 表示接收Object型參數(shù),返回int盗冷。
int[] m(int i, String s)對(duì)應(yīng)的方法描述符是(ILjava/lang/String;)[I 表示接受int和String怠苔,返回一個(gè)int[]。
Object m(int[] i)對(duì)應(yīng)的方法描述符是 ([I)Ljava/lang/Object; 表示接受一個(gè)int[]仪糖,返回Object柑司。