最近項目中產品要求接入神策埋點,神策最大的宣傳點應該就是所謂無痕全埋點。對於這種"無痕"或者"無感知",大部分Android
老鳥的第一反應確定就是插樁吧。幾年前在以前公司作系統開發時,開發過一套相似Android dumpsys
的內部dump
系統,經過串口命令輸入參數,獲取當前應用的相關指標,當時提供給應用開發的sdk就用到了AspectJ
,經過統一插樁的方式,自動插入相關代碼,減小應用開發的接入工做量和出錯量。java
經過下載神策的插件並查看源碼,的確和咱們預測同樣,在編譯期經過插樁方式調用sdk
的相關方法,進行數據採集上報工做,和咱們以前項目使用的AspectJ
的原理基本同樣,只不過它使用的是ASM框架。android
什麼是ASM
,ASM
是一個通用的Java
字節碼操做和分析框架。 它能夠用於修改現有類或直接以二進制形式動態生成類。 ASM
提供了一些常見的字節碼轉換和分析算法,能夠從中構建自定義複雜轉換和代碼分析工具。 ASM
提供與其餘Java
字節碼框架相似的功能,但專一於性能。 由於它的設計和實現儘量小並且快,因此它很是適合在動態系統中使用(但固然也能夠以靜態方式使用,例如在編譯器中)git
都是AOP
框架,ASM
更加註重的是性能,因此成爲了各大公司涉及插樁需求項目的首選框架。github
首先咱們來看一張經典的Android
編譯流程圖,ASM的原理是修改字節碼,也就是修改編譯生成的class
文件,因此對應Android
編譯流程中的切入時機就是.classFiles和dex
之間:
算法
本文不會對ASM
框架作深刻的分析和高階的使用,僅僅經過一個簡單的例子介紹下整一個插樁流程和關注點:api
Android Studio
中建立一個項目,刪除app module
,新建一個Java library
:Java
目錄,便於執行命令。接着在build.gradle
中添加ASM
的依賴:apply plugin: 'java-library'
apply plugin: 'kotlin'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // asm遠程依賴 implementation 'org.ow2.asm:asm:7.1' implementation 'org.ow2.asm:asm-commons:7.1' } sourceCompatibility = "1.8"
targetCompatibility = "1.8"
複製代碼
此時,前期準備工做就完成了。首先說明一下本案例的目標:在一個testAsm
的方法中插入一行代碼System.out.println("Hello Asm");
,因爲須要經過命令編譯,爲了方便,咱們的target
目標文件使用Java
編寫,流程代碼使用Kotlin
編寫。markdown
1.建立一個目標文件Target.java
和一個標記須要插樁的註解InjectHello.java
:app
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@interface InjectHello {
}
public class Target {
@InjectHello
private void testAsm() {
}
}
複製代碼
Target.java
所在目錄下打開命令行,執行javac
命令,生成對應的Target.class
文件:Target.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
public class Target {
public Target() {
}
@InjectHello
private void testAsm() {
}
}
複製代碼
接下來咱們正式開始編寫流程代碼 1.建立一個入口類和main
方法:框架
class
文件ClassReader
實例,讀取文件流ClassWriter
實例,用於寫文件reader.accept
進行文件訪問和修改class
文件的修改寫入class Main {
companion object {
@JvmStatic
fun main(args: Array<String>) {
val targetFile = "${System.getProperty("user.dir")}/example/src/main/java/Target.class"
println("目標文件:$targetFile")
val startTime = System.currentTimeMillis()
val fis = FileInputStream(targetFile)
val reader = ClassReader(fis)// 讀取文件
val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES)// 寫文件
println("開始修改文件")
// HelloClassVisitor爲自定義的類訪問器
reader.accept(HelloClassVisitor(writer), ClassReader.EXPAND_FRAMES)// 訪問class文件並修改
val bytes = writer.toByteArray()// 獲取修改後的流
val fos = FileOutputStream(targetFile)
println("寫入文件:$targetFile")
fos.write(bytes)// 將修改後字節流寫入文件
fos.flush()
println("關閉文件流")
fis.close()
fos.close()
println("本次插樁耗時:${System.currentTimeMillis() - startTime} ms")
}
}
}
複製代碼
2.建立一個自定義class
訪問器HelloClassVisitor
:ide
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("訪問方法:$name, 描述符:$descriptor")
return HelloMethodVisitor(visitMethod, access, name, descriptor)
}
}
複製代碼
3.咱們先建立一個自定義的Method
方法訪問器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.這時候咱們先暫停,看下這兩個重寫方法,從方法名咱們能夠看出,一個是訪問方法註解時的回調,一個是進入方法時的回調。還記的咱們的目標嗎,就是往標記了註解的方法中插入一行打印代碼,因此這兩個方法就是咱們須要實現自定義邏輯的地方。
5.咱們先暫停繼續編寫代碼,在AS
中搜索安裝一個插件ASM Bytecode Outline
便於查看字節碼和ASM
代碼文件:
6.咱們先編寫一個目標文件,也就是咱們但願插入打印後的文件Target2.java
:
// 咱們指望插入打印後的代碼:
public class Target2 {
@InjectHello
private void testAsm() {
System.out.println("Hello Asm");
}
}
複製代碼
7.安裝好插件後,選擇咱們的Target2
,右鍵點擊Show Bytecode outline
使用插件查看對應的字節碼:
Java字節碼:
ASM代碼:
8.咱們把bytecode
中修改後的testAsm
方法部分代碼拷貝出來以下,這就是咱們最終指望的產物:
// 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.如今咱們對照這這個目標class
文件,開始實現咱們的HelloMethodVisitor
,有以下兩種api
實現方式:
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("訪問方法:$name -註解:$descriptor")
if (descriptor!! == Type.getDescriptor(InjectHello::class.java)) {
println("標記了註解:$descriptor, 須要處理")
isInjectHello = true
}
return super.visitAnnotation(descriptor, visible)
}
/** * ==========================> * Java方法: * private void testAsm() { * System.out.println("Hello Asm");// 準備插入的代碼 * } * ==========================> * 對應字節碼: * // 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 * 對應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()
// 此處爲方法開頭
if (isInjectHello) {
println("開始插入代碼: [ System.out.println(\"Hello Asm\"); ]")
// **********方法1************
// 對應->GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"))
// 對應->LDC "Hello Asm"
visitLdcInsn("Hello Asm")
// 對應->INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
invokeVirtual(Type.getType("Ljava/io/PrintStream;"), Method("println", "(Ljava/lang/String;)V"))
// **********方法2************
// 直接從ASMified中複製代碼,和方法1是等價的:
// 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.好了,一套基礎的ASM
插樁流程已經準備好了,如今讓咱們執行下Main.Companion#main
的入口方法,發現控制檯打印出以下:
目標文件:C:\Users\seagazer\Desktop\Asm/example/src/main/java/Target.class
開始修改文件
訪問方法:<init>, 描述符:()V
訪問方法:testAsm, 描述符:()V
訪問方法:testAsm -註解:LInjectHello;
標記了註解:LInjectHello;, 須要處理
開始插入代碼: [ System.out.println("Hello Asm"); ]
寫入文件:C:\Users\seagazer\Desktop\Asm/example/src/main/java/Target.class
關閉文件流
本次插樁耗時:36 ms
Process finished with exit code 0
複製代碼
11.最後,讓咱們再看下最新生成的Target.class
文件內容,對比上面的Target.class
文件,說明咱們成功插入了System.out.println("Hello Asm");
這行代碼:
![]() |
![]() |
---|
好了,到此咱們已經達成了咱們的目標,最後附上完整的代碼: github.com/seagazer/as…
最後總結一下,目前不管是ASM,AspectJ
或者其餘AOP
框架,在Android
上的主要應用都是在插件transform
中對代碼進行修改,好比統一的插樁對方法進行耗時統計,點擊事件的防抖處理等等,能夠減小對原有代碼的侵入性,提供統一的管理,下降修改工做量和出錯機率。 在使用第三方框架的時候,要多些思考,若是咱們本身實現,會怎麼去設計方案,或者選擇使用什麼方式或者框架去實現,而不是簡單的引入,完成業務需求,這樣,才能保證自身能力的持續提高。