Android性能監(jiān)控實(shí)現(xiàn)原理


涉及知識(shí)點(diǎn):APM, java Agent, plugin, bytecode, asm, InvocationHandler, smail

一. 背景介紹

APM : 應(yīng)用程序性能管理。 2011年時(shí)國(guó)外的APM行業(yè) NewRelic 和 APPDynamics 已經(jīng)在該領(lǐng)域拔得頭籌,國(guó)內(nèi)近些年來(lái)也出現(xiàn)一些APM廠商,如: 聽(tīng)云, OneAPM, 博睿(bonree) 云智慧,阿里百川碼力。 (據(jù)分析,國(guó)內(nèi)android端方案都是抄襲NewRelic公司的,由于該公司的sdk未混淆,業(yè)界良心)

能做什么: crash監(jiān)控,卡頓監(jiān)控,內(nèi)存監(jiān)控,增加trace,網(wǎng)絡(luò)性能監(jiān)控,app頁(yè)面自動(dòng)埋點(diǎn),等。

二. 方案介紹

性能監(jiān)控其實(shí)就是hook 代碼到項(xiàng)目代碼中,從而做到各種監(jiān)控。常規(guī)手段都是在項(xiàng)目中增加代碼,但如何做到非侵入式的,即一個(gè)sdk即可。

1. 如何hook

切面編程-- AOP。
我們的方案是AOP的一種,通過(guò)修改app class字節(jié)碼的形式將我們項(xiàng)目的class文件進(jìn)行修改,從而做到嵌入我們的監(jiān)控代碼。

androidbuilder.jpg

通過(guò)查看Adnroid編譯流程圖,可以知道編譯器會(huì)將所有class文件打包稱(chēng)dex文件,最終打包成apk。那么我們就需要在class編譯成dex文件的時(shí)候進(jìn)行代碼注入。比如我想統(tǒng)計(jì)某個(gè)方法的執(zhí)行時(shí)間,那我只需要在每個(gè)調(diào)用了這個(gè)方法的代碼前后都加一個(gè)時(shí)間統(tǒng)計(jì)就可以了。關(guān)鍵點(diǎn)就在于編譯dex文件時(shí)候注入代碼,這個(gè)編譯過(guò)程是由dx執(zhí)行,具體類(lèi)和方法為com.android.dx.command.dexer.Main#processClass。此方法的第二個(gè)參數(shù)就是class的byte數(shù)組,于是我們只需要在進(jìn)入processClass方法的時(shí)候用ASM工具對(duì)class進(jìn)行改造并替換掉第二個(gè)參數(shù),最后生成的apk就是我們改造過(guò)后的了。

類(lèi):com.android.dx.command.dexer.Main

新的難點(diǎn): 要讓jvm在執(zhí)行processClass之前先執(zhí)行我們的代碼,必須要對(duì)com.android.dx.command.dexer.Main(以下簡(jiǎn)稱(chēng)為dexer.Main)進(jìn)行改造。如何才能達(dá)到這個(gè)目的?這時(shí)Instrumentation和VirtualMachine就登場(chǎng)了,參考第三節(jié)。

2. hook 到哪里

一期主要是網(wǎng)絡(luò)性能監(jiān)控。如何能截獲到網(wǎng)絡(luò)數(shù)據(jù)
通過(guò)調(diào)研發(fā)現(xiàn)目前有下面集中方案:

  • root手機(jī),通過(guò)adb 命令進(jìn)行截獲。
  • 建立vpn,將所有網(wǎng)絡(luò)請(qǐng)求進(jìn)行截獲。
  • 參考聽(tīng)云,newrelic等產(chǎn)品,針對(duì)特定庫(kù)進(jìn)行代理截獲。

也許還有其他的方式,需要繼續(xù)調(diào)研。

目前我們參考newrelic等公司產(chǎn)品,針對(duì)特定網(wǎng)絡(luò)請(qǐng)求庫(kù)進(jìn)行代理的的方式進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)截獲。比如okhtt3, httpclient, 等網(wǎng)絡(luò)庫(kù)。

三. Java Agent

In general, a javaagent is a JVM “plugin”, a specially crafted .jar file, that utilizes the Instrumentation API that the JVM provides.

http://www.infoq.com/cn/articles/javaagent-illustrated/

由于我們要修改Dexer 的Main類(lèi), 而該類(lèi)是在編譯時(shí)期由java虛擬機(jī)啟動(dòng)的, 所以我們需要通過(guò)agent來(lái)修改dexer Main類(lèi)。

javaagent的主要功能如下:

  • 可以在加載class文件之前作攔截,對(duì)字節(jié)碼做修改
  • 可以在運(yùn)行期對(duì)已加載類(lèi)的字節(jié)碼做變化

JVMTI:JVM Tool Interface,是JVM暴露出來(lái)的一些供用戶(hù)擴(kuò)展的接口集合。JVMTI是基于事件驅(qū)動(dòng)的,JVM每執(zhí)行到一定的邏輯就會(huì)調(diào)用一些事件的回調(diào)接口(如果有的話(huà)),這些接口可以供開(kāi)發(fā)者擴(kuò)展自己的邏輯。

instrument agent: javaagent功能就是它來(lái)實(shí)現(xiàn)的,另外instrument agent還有個(gè)別名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),這個(gè)名字也完全體現(xiàn)了其最本質(zhì)的功能:就是專(zhuān)門(mén)為Java語(yǔ)言編寫(xiě)的插樁服務(wù)提供支持的。

兩種加載agent的方式:

  • 在啟動(dòng)時(shí)加載, 啟動(dòng)JVM時(shí)指定agent類(lèi)。這種方式,Instrumentation的實(shí)例通過(guò)agent class的premain方法被傳入。
  • 在運(yùn)行時(shí)加載,JVM提供一種當(dāng)JVM啟動(dòng)完成后開(kāi)啟agent機(jī)制。這種情況下,Instrumention實(shí)例通過(guò)agent代碼中的的agentmain傳入。

參考例子instrumentation 功能介紹(javaagent)

有了javaagent, 我們就可以在編譯app時(shí)重新修改dex 的Main類(lèi),對(duì)應(yīng)修改processClass方法。

4. Java Bytecode

如何修改class文件? 我們需要了解java字節(jié)碼,然后需要了解ASM開(kāi)發(fā)。通過(guò)ASM編程來(lái)修改字節(jié)碼,從而修改class文件。(也可以使用javaassist來(lái)進(jìn)行修改)

在介紹字節(jié)代碼指令之前,有必要先來(lái)介紹 Java 虛擬機(jī)執(zhí)行模型。我們知道,Java 代碼是 在線程內(nèi)部執(zhí)行的。每個(gè)線程都有自己的執(zhí)行棧,棧由幀組成。每個(gè)幀表示一個(gè)方法調(diào)用:每次 調(diào)用一個(gè)方法時(shí),會(huì)將一個(gè)新幀壓入當(dāng)前線程的執(zhí)行棧。當(dāng)方法返回時(shí),或者是正常返回,或者 是因?yàn)楫惓7祷?會(huì)將這個(gè)幀從執(zhí)行棧中彈出,執(zhí)行過(guò)程在發(fā)出調(diào)用的方法中繼續(xù)進(jìn)行(這個(gè)方 法的幀現(xiàn)在位于棧的頂端)。

每一幀包括兩部分:一個(gè)局部變量部分和一個(gè)操作數(shù)棧部分。局部變量部分包含可根據(jù)索引 以隨機(jī)順序訪問(wèn)的變量。由名字可以看出,操作數(shù)棧部分是一個(gè)棧,其中包含了供字節(jié)代碼指令 用作操作數(shù)的值。

字節(jié)代碼指令
字節(jié)代碼指令由一個(gè)標(biāo)識(shí)該指令的操作碼和固定數(shù)目的參數(shù)組成:

  • 操作碼是一個(gè)無(wú)符號(hào)字節(jié)值——即字節(jié)代碼名
  • 參數(shù)是靜態(tài)值,確定了精確的指令行為。它們緊跟在操作碼之后給出.比如GOTO標(biāo)記 指令(其操作碼的值為 167)以一個(gè)指明下一條待執(zhí)行指令的標(biāo)記作為參數(shù)標(biāo)記。不要 將指令參數(shù)與指令操作數(shù)相混淆:參數(shù)值是靜態(tài)已知的,存儲(chǔ)在編譯后的代碼中,而 操作數(shù)值來(lái)自操作數(shù)棧,只有到運(yùn)行時(shí)才能知道。

參考: https://en.wikipedia.org/wiki/Java_bytecode_instruction_listings

常見(jiàn)指令:

  • const 將什么數(shù)據(jù)類(lèi)型壓入操作數(shù)棧。
  • push 表示將單字節(jié)或短整型的常量壓入操作數(shù)棧。
  • ldc 表示將什么類(lèi)型的數(shù)據(jù)從常量池中壓入操作數(shù)棧。
  • load 將某類(lèi)型的局部變量數(shù)據(jù)壓入操作數(shù)棧頂。
  • store 將操作數(shù)棧頂?shù)臄?shù)據(jù)存入指定的局部變量中。
  • pop 從操作數(shù)棧頂彈出數(shù)據(jù)
  • dup 復(fù)制棧頂?shù)臄?shù)據(jù)并將復(fù)制的值也壓入棧頂。
  • swap 互換棧頂?shù)臄?shù)據(jù)
  • invokeVirtual 調(diào)用實(shí)例方法
  • invokeSepcial 調(diào)用超類(lèi)構(gòu)造方法,實(shí)例初始化,私有方法等。
  • invokeStatic 調(diào)用靜態(tài)方法
  • invokeInterface 調(diào)用接口
  • getStatic
  • getField
  • putStatic
  • putField
  • New

查看demo:
Java源代碼

public static void print(String param) {
    System.out.println("hello " + param);
    new TestMain().sayHello();
}

public void sayHello() {
    System.out.println("hello agent");
}

字節(jié)碼

// access flags 0x9
  public static print(Ljava/lang/String;)V
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "hello "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    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
    
    NEW com/paic/agent/test/TestMain
    DUP
    INVOKESPECIAL com/paic/agent/test/TestMain.<init> ()V
    INVOKEVIRTUAL com/paic/agent/test/TestMain.sayHello ()V
    RETURN

    
public sayHello()V
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "hello agent"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    RETURN

5. ASM 開(kāi)發(fā)

由于程序分析、生成和轉(zhuǎn)換技術(shù)的用途眾多,所以人們針對(duì)許多語(yǔ)言實(shí)現(xiàn)了許多用于分析、 生成和轉(zhuǎn)換程序的工具,這些語(yǔ)言中就包括 Java 在內(nèi)。ASM 就是為 Java 語(yǔ)言設(shè)計(jì)的工具之一, 用于進(jìn)行運(yùn)行時(shí)(也是脫機(jī)的)類(lèi)生成與轉(zhuǎn)換。于是,人們?cè)O(shè)計(jì)了 ASM1庫(kù),用于處理經(jīng)過(guò)編譯 的 Java 類(lèi)。

ASM 并不是惟一可生成和轉(zhuǎn)換已編譯 Java 類(lèi)的工具,但它是最新、最高效的工具之一,可 從 http://asm.objectweb.org 下載。其主要優(yōu)點(diǎn)如下:

  • 有一個(gè)簡(jiǎn)單的模塊API,設(shè)計(jì)完善、使用方便。
  • 文檔齊全,擁有一個(gè)相關(guān)的Eclipse插件。
  • 支持最新的 Java 版本——Java 7。
  • 小而快、非??煽?。
  • 擁有龐大的用戶(hù)社區(qū),可以為新用戶(hù)??供支持。
  • 源許可開(kāi)放,幾乎允許任意使用。
ASM_transfer.png

核心類(lèi): ClassReader, ClassWriter, ClassVisitor

參考demo:

{   
    // print 方法的ASM代碼
    mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "print", "(Ljava/lang/String;)V", null, null);
    mv.visitCode();
 
    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
    mv.visitInsn(DUP);
    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
    mv.visitLdcInsn("hello ");
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
    mv.visitVarInsn(ALOAD, 0);
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
  
    mv.visitTypeInsn(NEW, "com/paic/agent/test/TestMain");
    mv.visitInsn(DUP);
    mv.visitMethodInsn(INVOKESPECIAL, "com/paic/agent/test/TestMain", "<init>", "()V", false);
    mv.visitMethodInsn(INVOKEVIRTUAL, "com/paic/agent/test/TestMain", "sayHello", "()V", false);
   
    mv.visitInsn(RETURN);
    mv.visitEnd();
}


{
   //sayHello 的ASM代碼
    mv = cw.visitMethod(ACC_PUBLIC, "sayHello", "()V", null, null);
    mv.visitCode();
    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    mv.visitLdcInsn("hello agent");
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

    mv.visitInsn(RETURN);
    mv.visitEnd();
}

6. 實(shí)現(xiàn)原理

1. Instrumentation和VirtualMachine

VirtualMachine有個(gè)loadAgent方法,它指定的agent會(huì)在main方法前啟動(dòng),并調(diào)用agent的agentMain方法,agentMain的第二個(gè)參數(shù)是Instrumentation,這樣我們就能夠給Instrumentation設(shè)置ClassFileTransformer來(lái)實(shí)現(xiàn)對(duì)dexer.Main的改造,同樣也可以用ASM來(lái)實(shí)現(xiàn)。一般來(lái)說(shuō),APM工具包括三個(gè)部分,plugin、agent和具體的業(yè)務(wù)jar包。這個(gè)agent就是我們說(shuō)的由VirtualMachine啟動(dòng)的代理。而plugin要做的事情就是調(diào)用loadAgent方法。對(duì)于Android Studio而言,plugin就是一個(gè)Gradle插件。 實(shí)現(xiàn)gradle插件可以用intellij創(chuàng)建一個(gè)gradle工程并實(shí)現(xiàn)Plugin< Project >接口,然后把tools.jar(在jdk的lib目錄下)和agent.jar加入到Libraries中。在META-INF/gradle-plugins目錄下創(chuàng)建一個(gè)properties文件,并在文件中加入一行內(nèi)容“implementation-class=插件類(lèi)的全限定名“。artifacs配置把源碼和META-INF加上,但不能加tools.jar和agent.jar。(tools.jar 在 jdk中, 不過(guò)一般需要自己拷貝到工程目錄中的, agent.jar開(kāi)發(fā)完成后放到plugin工程中用于獲取jar包路徑)。

2. ClassFileTransformer

agent的實(shí)現(xiàn)相對(duì)plugin則復(fù)雜很多,首先需要提供agentmain(String args, Instrumentation inst)方法,并給Instrumentation設(shè)置ClassFileTransformer,然后在transformer里改造dexer.Main。當(dāng)jvm成功執(zhí)行到我們?cè)O(shè)置的transformer時(shí),就會(huì)發(fā)現(xiàn)傳進(jìn)來(lái)的class根本就沒(méi)有dexer.Main??拥剡@是。。。前面提到了,執(zhí)行dexer.Main的是dx.bat,也就是說(shuō),它和plugin根本不在一個(gè)進(jìn)程里。

3. ProcessBuilder

dx.bat其實(shí)是由ProcessBuilder的start方法啟動(dòng)的,ProcessBuilder有一個(gè)command成員,保存的是啟動(dòng)目標(biāo)進(jìn)程攜帶的參數(shù),只要我們給dx.bat帶上-javaagent參數(shù)就能給dx.bat所在進(jìn)程指定我們的agent了。于是我們可以在執(zhí)行start方法前,調(diào)用command方法獲取command,并往其中插入-javaagent參數(shù)。參數(shù)的值是agent.jar所在的路徑,可以使用agent.jar其中一個(gè)class類(lèi)實(shí)例的getProtectionDomain().getCodeSource().getLocation().toURI().getPath()獲得。可是到了這里我們的程序可能還是無(wú)法正確改造class。如果我們把改造類(lèi)的代碼單獨(dú)放到一個(gè)類(lèi)中,然后用ASM生成字節(jié)碼調(diào)用這個(gè)類(lèi)的方法來(lái)對(duì)command參數(shù)進(jìn)行修改,就會(huì)發(fā)現(xiàn)拋出了ClassDefNotFoundError錯(cuò)誤。這里涉及到了ClassLoader的知識(shí)。

4. ClassLoader和InvocationHandler

關(guān)于ClassLoader的介紹很多,這里不再贅述。ProcessBuilder類(lèi)是由Bootstrap ClassLoader加載的,而我們自定義的類(lèi)則是由AppClassLoader加載的。Bootstrap ClassLoader處于AppClassLoader的上層,我們知道,上層類(lèi)加載器所加載的類(lèi)是無(wú)法直接引用下層類(lèi)加載器所加載的類(lèi)的。但如果下層類(lèi)加載器加載的類(lèi)實(shí)現(xiàn)或繼承了上層類(lèi)加載器加載的類(lèi)或接口,上層類(lèi)加載器加載的類(lèi)獲取到下層類(lèi)加載的類(lèi)的實(shí)例就可以將其強(qiáng)制轉(zhuǎn)型為父類(lèi),并調(diào)用父類(lèi)的方法。這個(gè)上層類(lèi)加載器加載的接口,部分APM使用InvocationHandler。還有一個(gè)問(wèn)題,ProcessBuilder怎么才能獲取到InvocationHandler子類(lèi)的實(shí)例呢?有一個(gè)比較巧妙的做法,在agent啟動(dòng)的時(shí)候,創(chuàng)建InvocationHandler實(shí)例,并把它賦值給Logger的treeLock成員。treeLock是一個(gè)Object對(duì)象,并且只是用來(lái)加鎖的,沒(méi)有別的用途。但treeLock是一個(gè)final成員,所以記得要修改其修飾,去掉final。Logger同樣也是由Bootstrap ClassLoader加載,這樣ProcessBuilder就能通過(guò)反射的方式來(lái)獲取InvocationHandler實(shí)例了。(詳見(jiàn):核心代碼例子)

上層類(lèi)加載器所加載的類(lèi)是無(wú)法直接引用下層類(lèi)加載器所加載的類(lèi)的

層次 加載器 類(lèi)
上層 BootStrapClassLoader ProcessBuilder
下層 AppClassLoader ProcessBuilderMethodVisitor操作的自定義類(lèi)

這一句話(huà)的理解: 我們的目的是通過(guò)ProcessBuilderMethodVisitor將我們的代碼(自定義修改類(lèi))寫(xiě)入ProcessBuilder.class中去讓BootStrapClassLoader類(lèi)加載器進(jìn)行加載,而此時(shí), BootStrapClassLoader是無(wú)法引用到我們自定義的類(lèi)的,因?yàn)槲覀冏远x的類(lèi)是AppClassLoader加載的。

但如果下層類(lèi)加載器加載的類(lèi)實(shí)現(xiàn)或繼承了上層類(lèi)加載器加載的類(lèi)或接口,上層類(lèi)加載器加載的類(lèi)獲取到下層類(lèi)加載的類(lèi)的實(shí)例就可以將其強(qiáng)制轉(zhuǎn)型為父類(lèi),并調(diào)用父類(lèi)的方法。

層次 加載器 類(lèi)
上層 BootStrapClassLoader Looger
下層 AppClassLoader InvocationDispatcher

這句話(huà)的理解: 這里我們可以看到自定義類(lèi)InvocationDispatcher是由AppClassLoader加載的, 我們?cè)谶\(yùn)行RewriterAgent(AppClassLoader加載)類(lèi)時(shí),通過(guò)反射的方式將InvocationDispatcher對(duì)象放入Looger(由于引用了Looger.class,所以此時(shí)logger已經(jīng)被BootStrapClassLoader加載)類(lèi)的treelock對(duì)象中,即下層類(lèi)加載器加載的類(lèi)實(shí)現(xiàn)了上層類(lèi)加載器加載的類(lèi);當(dāng)我們通過(guò)ProcessBuilderMethodVisitor類(lèi)處理ProcessBuilder.class文件時(shí),可以通過(guò)Logger提取成員變量,插入對(duì)應(yīng)的調(diào)用邏輯。當(dāng)運(yùn)行到ProcessBuilder時(shí),再通過(guò)這段代碼動(dòng)態(tài)代理的方式調(diào)用對(duì)應(yīng)的業(yè)務(wù)。可以將其強(qiáng)制轉(zhuǎn)型為父類(lèi),并調(diào)用父類(lèi)的方法 ,請(qǐng)參考http://stackoverflow.com/questions/1504633/what-is-the-point-of-invokeinterface, 這里詳細(xì)介紹了invokeInterface 和 invokeVirtual 的區(qū)別。

5. CallSiteReplace 和 WrapReturn

實(shí)現(xiàn)上我們目前主要做這兩種, 一種是代碼調(diào)用替換, 另一種是代碼包裹返回。主要是提前寫(xiě)好對(duì)應(yīng)規(guī)則的替換代碼, 生成配置文件表, 在agent中visit每一個(gè)class代碼, 遇到對(duì)應(yīng)匹配調(diào)用時(shí)將進(jìn)行代碼替換。

7. 核心代碼

ProcessBuilderMethodVisitor
DexClassTransformer#createDexerMainClassAdapter
InvocationDispatcher
BytecodeBuilder

public BytecodeBuilder loadInvocationDispatcher() {
        this.adapter.visitLdcInsn(Type.getType(TransformConstant.INVOCATION_DISPATCHER_CLASS));
        this.adapter.visitLdcInsn(TransformConstant.INVOCATION_DISPATCHER_FILED_NAME);
        this.adapter.invokeVirtual(Type.getType(Class.class), new Method("getDeclaredField", "(Ljava/lang/String;)Ljava/lang/reflect/Field;"));
        this.adapter.dup();
        this.adapter.visitInsn(Opcodes.ICONST_1);
        this.adapter.invokeVirtual(Type.getType(Field.class), new Method("setAccessible", "(Z)V"));
        this.adapter.visitInsn(Opcodes.ACONST_NULL);
        this.adapter.invokeVirtual(Type.getType(Field.class), new Method("get", "(Ljava/lang/Object;)Ljava/lang/Object;"));
        return this;
    }

解析

順序 指令 描述
8 InvocationDispatcher object invokeVirtual 調(diào)用get方法返回具體實(shí)例對(duì)象
7 null ACONST_NULL null 入棧
6 Field object invokeVirtual 調(diào)用setAccessible,改為可訪問(wèn)的,目前棧中只剩一個(gè)對(duì)象
5 true ICONST_1 1 即為true,入棧
4 Field object dup 拷貝一份,目前棧中只剩兩個(gè)對(duì)象
3 Field object invokeVirtual 調(diào)用getDeclaredField 獲取treeLock存儲(chǔ)的Field
2 treelock ldc treelock 入棧
1 Logger.class Type ldc Logger.class type 入棧

WrapMethodClassVisitor#MethodWrapMethodVisitor

private boolean tryReplaceCallSite(int opcode, String owner, String name, String desc, boolean itf) {
            Collection<ClassMethod> replacementMethods = this.context.getCallSiteReplacements(owner, name, desc);
            if (replacementMethods.isEmpty()) {
                return false;
            }
            ClassMethod method = new ClassMethod(owner, name, desc);
            Iterator<ClassMethod> it = replacementMethods.iterator();
            if (it.hasNext()) {
                ClassMethod replacementMethod = it.next();
                boolean isSuperCallInOverride = (opcode == Opcodes.INVOKESPECIAL) && !owner.equals(this.context.getClassName())
                        && this.name.equals(name) && this.desc.equals(desc);
                //override 方法
                if (isSuperCallInOverride) {
                    this.log.info(MessageFormat.format("[{0}] skipping call site replacement for super call in overriden method : {1}:{2}",
                            this.context.getFriendlyClassName(), this.name, this.desc));
                    return false;
                }

                Method originMethod = new Method(name, desc);
                //處理init方法, 構(gòu)造對(duì)象, 調(diào)用替換的靜態(tài)方法來(lái)替換init。
                if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) {
                    //調(diào)用父類(lèi)構(gòu)造方法
                    if (this.context.getSuperClassName() != null && this.context.getSuperClassName().equals(owner)) {
                        this.log.info(MessageFormat.format("[{0}] skipping call site replacement for class extending {1}",
                                this.context.getFriendlyClassName(), this.context.getFriendlySuperClassName()));
                        return false;
                    }
                    this.log.info(MessageFormat.format("[{0}] tracing constructor call to {1} - {2}", this.context.getFriendlyClassName(), method.toString(), owner));
                    //開(kāi)始處理創(chuàng)建對(duì)象的邏輯
                    //保存參數(shù)到本地
                    int[] arguments = new int[originMethod.getArgumentTypes().length];
                    for (int i = arguments.length -1 ; i >= 0; i--) {
                        arguments[i] = this.newLocal(originMethod.getArgumentTypes()[i]);
                        this.storeLocal(arguments[i]);
                    }
                    //由于init 之前會(huì)有一次dup,及創(chuàng)建一次, dup一次, 此時(shí)如果執(zhí)行了new 和 dup 操作樹(shù)棧中會(huì)有兩個(gè)對(duì)象。
                    this.visitInsn(Opcodes.POP);
                    if (this.newInstructionFound && this.dupInstructionFound) {
                        this.visitInsn(Opcodes.POP);
                    }
                    //載入?yún)?shù)到操作數(shù)棧
                    for (int arg : arguments) {
                        this.loadLocal(arg);
                    }
                    //使用要替換的方法,執(zhí)行靜態(tài)方法進(jìn)行對(duì)象創(chuàng)建
                    super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc(), false);
                    //如果此時(shí)才調(diào)用了dup,也需要pop, (這一部分的場(chǎng)景暫時(shí)還沒(méi)有構(gòu)造出來(lái), 上面的邏輯為通用的)
                    if (this.newInstructionFound && !this.dupInstructionFound) {
                        this.visitInsn(Opcodes.POP);
                    }
                } else if (opcode == Opcodes.INVOKESTATIC) {
                    //替換靜態(tài)方法
                    this.log.info(MessageFormat.format("[{0}] replacing call to {1} with {2}", this.context.getFriendlyClassName(), method.toString(), replacementMethod.toString()));
                    super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc(), false);
                } else {
                    // 其他方法調(diào)用, 使用新方法替換舊方法的調(diào)用。 先判斷創(chuàng)建的對(duì)象是否為null,
                    Method newMethod = new Method(replacementMethod.getMethodName(), replacementMethod.getMethodDesc());
                    this.log.info(MessageFormat.format("[{0}] replacing call to {1} with {2}", this.context.getFriendlyClassName(), method.toString(), replacementMethod.toString()));
                    //從操作數(shù)棧上取原始參數(shù)類(lèi)型到本地變量中
                    int[] originArgs = new int[originMethod.getArgumentTypes().length];
                    for (int i = originArgs.length -1 ; i >= 0; i--) {
                        originArgs[i] = this.newLocal(originMethod.getArgumentTypes()[i]);
                        this.storeLocal(originArgs[i]);
                    }
                    //操作數(shù)棧中只剩操作對(duì)象了, 需要dup, 拷貝一份作為檢查新method的第一個(gè)參數(shù)。
                    this.dup();
                    //檢查操作數(shù)棧頂對(duì)象類(lèi)型是否和新method的第一個(gè)參數(shù)一致。
                    this.instanceOf(newMethod.getArgumentTypes()[0]);

                    Label isInstanceOfLabel = new Label();
                    //instanceof 結(jié)果不等于0 則跳轉(zhuǎn)到 isInstanceofLabel,執(zhí)行替換調(diào)用
                    this.visitJumpInsn(Opcodes.IFNE, isInstanceOfLabel);
                    //否則執(zhí)行原始調(diào)用
                    for (int arg : originArgs) {
                        this.loadLocal(arg);
                    }
                    super.visitMethodInsn(opcode, owner, name, desc, itf);

                    Label endLabel  = new Label();
                    //跳轉(zhuǎn)到結(jié)束label
                    this.visitJumpInsn(Opcodes.GOTO, endLabel);

                    this.visitLabel(isInstanceOfLabel);
                    //處理替換的邏輯
                    //load 參數(shù), 第一個(gè)為 obj, 后面的為原始參數(shù)
                    this.checkCast(newMethod.getArgumentTypes()[0]);
                    for (int arg: originArgs) {
                        this.loadLocal(arg);
                    }
                    super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc(), false);
                    //結(jié)束
                    this.visitLabel(endLabel);
                }
                this.context.markModified();
                return true;
            }
            return false;
        }

解析
詳細(xì)見(jiàn)tryReplaceCallSite注釋即可。

8. 驗(yàn)證

將生成的apk反編譯,查看class 字節(jié)碼。我們一般會(huì)通過(guò)JD-GUI來(lái)查看。我們來(lái)查看一下sample生成的結(jié)果:

private void testOkhttpCall()
  {
    OkHttpClient localOkHttpClient = new OkHttpClient.Builder().build();
    Object localObject = new Request.Builder().url("https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey");
    if (!(localObject instanceof Request.Builder))
    {
      localObject = ((Request.Builder)localObject).build();
      if ((localOkHttpClient instanceof OkHttpClient)) {
        break label75;
      }
    }
    label75:
    for (localObject = localOkHttpClient.newCall((Request)localObject);; localObject = OkHttp3Instrumentation.newCall((OkHttpClient)localOkHttpClient, (Request)localObject))
    {
      ((Call)localObject).enqueue(new Callback()
      {
        public void onFailure(Call paramAnonymousCall, IOException paramAnonymousIOException)
        {
         
        }
        
        public void onResponse(Call paramAnonymousCall, Response paramAnonymousResponse)
          throws IOException
        {
          
        }
      });
      return;
      localObject = OkHttp3Instrumentation.build((Request.Builder)localObject);
      break;
    }
  }

上面的代碼估計(jì)沒(méi)有幾個(gè)人能夠看懂, 尤其for循環(huán)里面的邏輯。其實(shí)是由于不同的反編譯工具造成的解析問(wèn)題導(dǎo)致的,所以看起來(lái)邏輯混亂,無(wú)法符合預(yù)期。

想用查看真實(shí)的結(jié)果, 我們來(lái)看下反編譯后的smail。
詳細(xì)smail指令參考http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html

.method private testOkhttpCall()V
    .locals 6
    .prologue
    .line 35
    const-string v3, "https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey"
    .line 36
    .local v3, "url":Ljava/lang/String;
    new-instance v4, Lokhttp3/OkHttpClient$Builder;
    invoke-direct {v4}, Lokhttp3/OkHttpClient$Builder;-><init>()V
    invoke-virtual {v4}, Lokhttp3/OkHttpClient$Builder;->build()Lokhttp3/OkHttpClient;
    move-result-object v1
//new OkHttpClient.Builder().build(); 即為okhttpclient,放到 v1 中
    .line 37
    .local v1, "okHttpClient":Lokhttp3/OkHttpClient;
    new-instance v4, Lokhttp3/Request$Builder;
    invoke-direct {v4}, Lokhttp3/Request$Builder;-><init>()V
    invoke-virtual {v4, v3}, Lokhttp3/Request$Builder;->url(Ljava/lang/String;)Lokhttp3/Request$Builder;
    move-result-object v4
    //new Request.Builder().url(url)執(zhí)行了這一段語(yǔ)句,將結(jié)果放到了v4中。
    instance-of v5, v4, Lokhttp3/Request$Builder;
    if-nez v5, :cond_0
    invoke-virtual {v4}, Lokhttp3/Request$Builder;->build()Lokhttp3/Request;
    move-result-object v2
    .line 38
    .local v2, "request":Lokhttp3/Request;
    //判斷v4中存儲(chǔ)的是否為Request.Builder類(lèi)型,如果是則跳轉(zhuǎn)到cond_0, 否則執(zhí)行Request.Builder.build()方法,將結(jié)果放到v2中.
    :goto_0
    instance-of v4, v1, Lokhttp3/OkHttpClient;
    if-nez v4, :cond_1
    invoke-virtual {v1, v2}, Lokhttp3/OkHttpClient;->newCall(Lokhttp3/Request;)Lokhttp3/Call;
    move-result-object v0
    .line 39
    .end local v1    # "okHttpClient":Lokhttp3/OkHttpClient;
    .local v0, "call":Lokhttp3/Call;
    //goto_0 標(biāo)簽:判斷v1 中的值是否為 OKHttpclient 類(lèi)型, 如果是跳轉(zhuǎn)為cond_1 , 否則調(diào)用OKHttpclient.newCall, 并將結(jié)果放到v0 中。
    :goto_1
    new-instance v4, Lcom/paic/apm/sample/MainActivity$1;
    invoke-direct {v4, p0}, Lcom/paic/apm/sample/MainActivity$1;-><init>(Lcom/paic/apm/sample/MainActivity;)V
    invoke-interface {v0, v4}, Lokhttp3/Call;->enqueue(Lokhttp3/Callback;)V
    .line 51
    return-void
    //goto_1 標(biāo)簽: 執(zhí)行 v0.enqueue(new Callback());并return;
    .line 37
    .end local v0    # "call":Lokhttp3/Call;
    .end local v2    # "request":Lokhttp3/Request;
    .restart local v1    # "okHttpClient":Lokhttp3/OkHttpClient;
    :cond_0
    check-cast v4, Lokhttp3/Request$Builder;
    invoke-static {v4}, Lcom/paic/agent/android/instrumentation/okhttp3/OkHttp3Instrumentation;->build(Lokhttp3/Request$Builder;)Lokhttp3/Request;
    move-result-object v2
    goto :goto_0
    //cond_0:標(biāo)簽: 執(zhí)行com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.build(v4), 并將結(jié)果放到v2中,并goto 到 goto_0
    .line 38
    .restart local v2    # "request":Lokhttp3/Request;
    :cond_1
    check-cast v1, Lokhttp3/OkHttpClient;
    .end local v1    # "okHttpClient":Lokhttp3/OkHttpClient;
    invoke-static {v1, v2}, Lcom/paic/agent/android/instrumentation/okhttp3/OkHttp3Instrumentation;->newCall(Lokhttp3/OkHttpClient;Lokhttp3/Request;)Lokhttp3/Call;
    move-result-object v0
    goto :goto_1
    //cond_1 標(biāo)簽: 執(zhí)行com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.newCall(v1, v2), 并將結(jié)果放到v0中, goto 到goto_1 
.end method

解析后的偽代碼


String v3 = "https://test3-fbtoam.pingan.com.cn:15443/btoa/portal/common/getPublicKey";
object v1 = new OkhttpClient.Builder().build();
object v4 = new Reqeust.Builder().url(v3);
object v2 ;
object v0 ;

if (v4 instanceof Request.Builder) {
    cond_0:
    v2 = com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.build(v4); 
} else {
    v2 = (Request.Builder)v4.build();
}

goto_0:
if (v1 instanceof OkHttpClient) {
    cond_1:
    v0 = com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.newCall(v1, v2);
} else {
    v0 = v1.newCall(v2); // v0 is Call
}

goto_1:
v4 = new Callback();
v0.enqueue(v4);
return;

查看偽代碼, 符合預(yù)期結(jié)果。驗(yàn)證完畢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • title: Android AOP之字節(jié)碼插樁author: 陶超description: 實(shí)現(xiàn)數(shù)據(jù)收集SDK時(shí)...
    陶菜菜閱讀 38,689評(píng)論 40 182
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,040評(píng)論 25 709
  • 歡迎收聽(tīng)本期的一本正經(jīng)胡說(shuō)八道廣播電臺(tái),我是你們的好朋友,李子。本期的主題是時(shí)間的重量,老規(guī)矩,如果你開(kāi)心,就當(dāng)它...
    濯吳閱讀 1,818評(píng)論 0 3
  • 今年的清明在淫雨霏霏中度過(guò)。陰冷的天和飄灑的細(xì)雨頻添了更多的思親滋味。 晨起絲絲的雨讓我掛起了免起牌,對(duì)床...
    往事如煙之浩然閱讀 497評(píng)論 0 1
  • 【戰(zhàn)果快報(bào)】7月23日,大隊(duì)部署城區(qū)24處重點(diǎn)路口執(zhí)勤崗,出動(dòng)警力98人,查處交通違法1128起,其中查處違法停車(chē)...
    04a7ef3d0d6b閱讀 1,071評(píng)論 5 4

友情鏈接更多精彩內(nèi)容