最近項(xiàng)目中產(chǎn)品要求接入神策埋點(diǎn)氮惯,神策最大的宣傳點(diǎn)應(yīng)該就是所謂無(wú)痕全埋點(diǎn)叮雳。對(duì)于這種"無(wú)痕"或者"無(wú)感知",大部分Android
老鳥(niǎo)的第一反應(yīng)肯定就是插樁吧妇汗。幾年前在之前公司做系統(tǒng)開(kāi)發(fā)時(shí)帘不,開(kāi)發(fā)過(guò)一套類(lèi)似Android dumpsys
的內(nèi)部dump
系統(tǒng),通過(guò)串口命令輸入?yún)?shù)杨箭,獲取當(dāng)前應(yīng)用的相關(guān)指標(biāo)寞焙,當(dāng)時(shí)提供給應(yīng)用開(kāi)發(fā)的sdk就用到了AspectJ
,通過(guò)統(tǒng)一插樁的方式告唆,自動(dòng)插入相關(guān)代碼棺弊,減少應(yīng)用開(kāi)發(fā)的接入工作量和出錯(cuò)量晶密。
通過(guò)下載神策的插件并查看源碼,的確和我們預(yù)測(cè)一樣模她,在編譯期通過(guò)插樁方式調(diào)用sdk
的相關(guān)方法稻艰,進(jìn)行數(shù)據(jù)采集上報(bào)工作,和我們之前項(xiàng)目使用的AspectJ
的原理基本一樣侈净,只不過(guò)它使用的是ASM框架尊勿。
什么是ASM
,ASM
是一個(gè)通用的Java
字節(jié)碼操作和分析框架畜侦。 它可以用于修改現(xiàn)有類(lèi)或直接以二進(jìn)制形式動(dòng)態(tài)生成類(lèi)元扔。 ASM
提供了一些常見(jiàn)的字節(jié)碼轉(zhuǎn)換和分析算法,可以從中構(gòu)建自定義復(fù)雜轉(zhuǎn)換和代碼分析工具旋膳。 ASM
提供與其他Java
字節(jié)碼框架類(lèi)似的功能澎语,但專(zhuān)注于性能。 因?yàn)樗脑O(shè)計(jì)和實(shí)現(xiàn)盡可能小而且快验懊,所以它非常適合在動(dòng)態(tài)系統(tǒng)中使用(但當(dāng)然也可以以靜態(tài)方式使用擅羞,例如在編譯器中)
都是AOP
框架,ASM
更加注重的是性能义图,所以成為了各大公司涉及插樁需求項(xiàng)目的首選框架减俏。
首先我們來(lái)看一張經(jīng)典的Android
編譯流程圖,ASM的原理是修改字節(jié)碼碱工,也就是修改編譯生成的class
文件娃承,所以對(duì)應(yīng)Android
編譯流程中的切入時(shí)機(jī)就是.classFiles和dex
之間:
本文不做對(duì)ASM
框架的深入分析,僅僅通過(guò)一個(gè)例子介紹下整一個(gè)插樁流程:
-
首先在
Android Studio
中創(chuàng)建一個(gè)項(xiàng)目怕篷,刪除app module
历筝,新建一個(gè)Java library
:
建立好之后,刪除包名匙头,只保留
Java
目錄漫谷,便于執(zhí)行命令仔雷。接著在build.gradle
中添加ASM
的依賴(lài):
apply plugin: 'java-library'
apply plugin: 'kotlin'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// asm遠(yuǎn)程依賴(lài)
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
此時(shí)蹂析,前期準(zhǔn)備工作就完成了。首先說(shuō)明一下本案例的目標(biāo):在一個(gè)
testAsm
的方法中插入一行代碼System.out.println("Hello Asm");
碟婆,由于需要通過(guò)命令編譯电抚,為了方便,我們的target
目標(biāo)文件使用Java
編寫(xiě)竖共,流程代碼使用Kotlin
編寫(xiě)蝙叛。1.創(chuàng)建一個(gè)目標(biāo)文件
Target.java
和一個(gè)標(biāo)記需要插樁的注解InjectHello.java
:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@interface InjectHello {
}
public class Target {
@InjectHello
private void testAsm() {
}
}
- 此時(shí)我們先在
Target.java
所在目錄下打開(kāi)命令行,執(zhí)行javac
命令公给,生成對(duì)應(yīng)的Target.class
文件:
接下來(lái)我們正式開(kāi)始編寫(xiě)流程代碼
1.創(chuàng)建一個(gè)入口類(lèi)和main
方法:
- 1.讀取剛剛編譯生成的
class
文件 - 2.創(chuàng)建一個(gè)
ClassReader
實(shí)例借帘,讀取文件流 - 3.創(chuàng)建一個(gè)
ClassWriter
實(shí)例蜘渣,用于寫(xiě)文件 - 4.調(diào)用
reader.accept
進(jìn)行文件訪(fǎng)問(wèn)和修改 - 5.將修改后的字節(jié)流寫(xiě)入目標(biāo)文件
- 6.完成對(duì)
class
文件的修改寫(xiě)入
class Main {
companion object {
@JvmStatic
fun main(args: Array<String>) {
val targetFile = "${System.getProperty("user.dir")}/example/src/main/java/Target.class"
println("目標(biāo)文件:$targetFile")
val startTime = System.currentTimeMillis()
val fis = FileInputStream(targetFile)
val reader = ClassReader(fis)// 讀取文件
val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES)// 寫(xiě)文件
println("開(kāi)始修改文件")
// HelloClassVisitor為自定義的類(lèi)訪(fǎng)問(wèn)器
reader.accept(HelloClassVisitor(writer), ClassReader.EXPAND_FRAMES)// 訪(fǎng)問(wèn)class文件并修改
val bytes = writer.toByteArray()// 獲取修改后的流
val fos = FileOutputStream(targetFile)
println("寫(xiě)入文件:$targetFile")
fos.write(bytes)// 將修改后字節(jié)流寫(xiě)入文件
fos.flush()
println("關(guān)閉文件流")
fis.close()
fos.close()
println("本次插樁耗時(shí):${System.currentTimeMillis() - startTime} ms")
}
}
}
2.創(chuàng)建一個(gè)自定義class
訪(fǎng)問(wèn)器HelloClassVisitor
:
class HelloClassVisitor(visitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, visitor) {
override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
val visitMethod = super.visitMethod(access, name, descriptor, signature, exceptions)
println("訪(fǎng)問(wèn)方法:$name, 描述符:$descriptor")
return HelloMethodVisitor(visitMethod, access, name, descriptor)
}
}
3.我們先創(chuàng)建一個(gè)自定義的Method
方法訪(fǎng)問(wèn)器HelloMethodVisitor
,這里使用AdviceAdapter
:
class HelloMethodVisitor(methodVisitor: MethodVisitor, access: Int, name: String?, descriptor: String?) :
AdviceAdapter(Opcodes.ASM7, methodVisitor, access, name, descriptor) {
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
return super.visitAnnotation(descriptor, visible)
}
override fun onMethodEnter() {
super.onMethodEnter()
}
}
4.這時(shí)候我們先暫停肺然,看下這兩個(gè)重寫(xiě)方法蔫缸,從方法名我們可以看出,一個(gè)是訪(fǎng)問(wèn)方法注解時(shí)的回調(diào)际起,一個(gè)是進(jìn)入方法時(shí)的回調(diào)拾碌。還記的我們的目標(biāo)嗎,就是往標(biāo)記了注解的方法中插入一行打印代碼街望,所以這兩個(gè)方法就是我們需要實(shí)現(xiàn)自定義邏輯的地方校翔。
5.我們先暫停繼續(xù)編寫(xiě)代碼,在AS
中搜索安裝一個(gè)插件ASM Bytecode Outline
便于查看字節(jié)碼和ASM
代碼文件:
6.我們先編寫(xiě)一個(gè)目標(biāo)文件灾前,也就是我們希望插入打印后的文件Target2.java
:
// 我們期望插入打印后的代碼:
public class Target2 {
@InjectHello
private void testAsm() {
System.out.println("Hello Asm");
}
}
7.選擇我們的Target2
防症,右鍵點(diǎn)擊Show Bytecode outline
使用插件查看對(duì)應(yīng)的字節(jié)碼:
8.我們把修改后的testAsm
方法部分拷貝出來(lái)如下,這就是我們最終期望的產(chǎn)物:
// access flags 0x2
private testAsm()V
@LInjectHello;() // invisible
L0
LINENUMBER 8 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Hello Asm"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 9 L1
RETURN
L2
LOCALVARIABLE this LTarget2; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
9.現(xiàn)在我們對(duì)照這這個(gè)目標(biāo)class
文件哎甲,開(kāi)始實(shí)現(xiàn)我們的HelloMethodVisitor
:
class HelloMethodVisitor(methodVisitor: MethodVisitor, access: Int, name: String?, descriptor: String?) :
AdviceAdapter(Opcodes.ASM7, methodVisitor, access, name, descriptor) {
private var isInjectHello = false
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
println("訪(fǎng)問(wèn)方法:$name -注解:$descriptor")
if (descriptor!! == Type.getDescriptor(InjectHello::class.java)) {
println("標(biāo)記了注解:$descriptor, 需要處理")
isInjectHello = true
}
return super.visitAnnotation(descriptor, visible)
}
/**
* ==========================>
* Java方法:
* private void testAsm() {
* System.out.println("Hello Asm");// 準(zhǔn)備插入的代碼
* }
* ==========================>
* 對(duì)應(yīng)字節(jié)碼:
* // access flags 0x2
* private testAsm()V
* L0
* LINENUMBER 7 L0
* GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
* LDC "Hello Asm"
* INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
* L1
* LINENUMBER 8 L1
* RETURN
* L2
* LOCALVARIABLE this LTarget2; L0 L2 0
* MAXSTACK = 2
* MAXLOCALS = 1
* 對(duì)應(yīng)ASM代碼:
* mv = cw.visitMethod(ACC_PRIVATE, "testAsm", "()V", null, null);
* {
* av0 = mv.visitAnnotation("LInjectHello;", false);
* av0.visitEnd();
* }
* mv.visitCode();
* Label l0 = new Label();
* mv.visitLabel(l0);
* mv.visitLineNumber(8, l0);
* mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
* mv.visitLdcInsn("Hello Asm");
* mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
* Label l1 = new Label();
* mv.visitLabel(l1);
* mv.visitLineNumber(9, l1);
* mv.visitInsn(RETURN);
* Label l2 = new Label();
* mv.visitLabel(l2);
* mv.visitLocalVariable("this", "LTarget2;", null, l0, l2, 0);
* mv.visitMaxs(2, 1);
* mv.visitEnd();
*/
override fun onMethodEnter() {
super.onMethodEnter()
// 此處為方法開(kāi)頭
if (isInjectHello) {
println("開(kāi)始插入代碼: [ System.out.println(\"Hello Asm\"); ]")
// **********方法1************
// 對(duì)應(yīng)->GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"))
// 對(duì)應(yīng)->LDC "Hello Asm"
visitLdcInsn("Hello Asm")
// 對(duì)應(yīng)->INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
invokeVirtual(Type.getType("Ljava/io/PrintStream;"), Method("println", "(Ljava/lang/String;)V"))
// **********方法2************
// 直接從ASMified中復(fù)制代碼告希,和方法1是等價(jià)的:
// mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// mv.visitLdcInsn("Hello Asm");
//mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}
10.好了,現(xiàn)在我們執(zhí)行下Main.Companion#main
的入口方法烧给,發(fā)現(xiàn)控制臺(tái)打印出如下:
目標(biāo)文件:C:\Users\seagazer\Desktop\Asm/example/src/main/java/Target.class
開(kāi)始修改文件
訪(fǎng)問(wèn)方法:<init>, 描述符:()V
訪(fǎng)問(wèn)方法:testAsm, 描述符:()V
訪(fǎng)問(wèn)方法:testAsm -注解:LInjectHello;
標(biāo)記了注解:LInjectHello;, 需要處理
開(kāi)始插入代碼: [ System.out.println("Hello Asm"); ]
寫(xiě)入文件:C:\Users\seagazer\Desktop\Asm/example/src/main/java/Target.class
關(guān)閉文件流
本次插樁耗時(shí):36 ms
Process finished with exit code 0
11.最后燕偶,讓我們?cè)倏聪?code>Target.class文件內(nèi)容,對(duì)比上面的Target.class
文件础嫡,說(shuō)明我們成功插入了System.out.println("Hello Asm");
好了指么,到此我們已經(jīng)達(dá)成了我們的目標(biāo),最后附上完整的代碼:https://github.com/seagazer/asm
最后總結(jié)一下榴鼎,目前不論是ASM伯诬,AspectJ
或者其他AOP
框架,在Android
上的主要應(yīng)用都是在插件transform
中對(duì)代碼進(jìn)行修改巫财,比如統(tǒng)一的插樁對(duì)方法進(jìn)行耗時(shí)統(tǒng)計(jì)盗似,點(diǎn)擊事件的防抖處理等等,可以減少對(duì)原有代碼的侵入性平项,提供統(tǒng)一的管理赫舒,降低修改工作量和出錯(cuò)概率。
在使用第三方框架的時(shí)候闽瓢,要多些思考接癌,如果我們自己實(shí)現(xiàn)政模,會(huì)怎么去設(shè)計(jì)方案竟宋,或者選擇使用什么方式或者框架去實(shí)現(xiàn)盈蛮,而不是簡(jiǎn)單的引入,完成業(yè)務(wù)需求娱挨,這樣删壮,才能保證自身能力的持續(xù)提升撞芍。