Android 效能監控實現原理

zyq_neuq發表於2016-12-25

涉及知識點:APM, Java Agent, plugin, bytecode, asm, InvocationHandler, smail

一. 背景介紹

APM : 應用程式效能管理。 2011年時國外的APM行業 NewRelic 和 APPDynamics 已經在該領域拔得頭籌,國內近些年來也出現一些APM廠商,如: 聽雲, OneAPM, 博睿(bonree) 雲智慧,阿里百川碼力。 (據分析,國內android端方案都是抄襲NewRelic公司的,由於該公司的sdk未混淆,業界良心)

能做什麼: crash監控,卡頓監控,記憶體監控,增加trace,網路效能監控,app頁面自動埋點,等。

二. 方案介紹

效能監控其實就是hook 程式碼到專案程式碼中,從而做到各種監控。常規手段都是在專案中增加程式碼,但如何做到非侵入式的,即一個sdk即可。

1. 如何hook

切面程式設計-- AOP。

我們的方案是AOP的一種,通過修改app class位元組碼的形式將我們專案的class檔案進行修改,從而做到嵌入我們的監控程式碼。

通過檢視Adnroid編譯流程圖,可以知道編譯器會將所有class檔案打包稱dex檔案,最終打包成apk。那麼我們就需要在class編譯成dex檔案的時候進行程式碼注入。比如我想統計某個方法的執行時間,那我只需要在每個呼叫了這個方法的程式碼前後都加一個時間統計就可以了。關鍵點就在於編譯dex檔案時候注入程式碼,這個編譯過程是由dx執行,具體類和方法為com.android.dx.command.dexer.Main#processClass。此方法的第二個引數就是class的byte陣列,於是我們只需要在進入processClass方法的時候用ASM工具對class進行改造並替換掉第二個引數,最後生成的apk就是我們改造過後的了。

類:com.android.dx.command.dexer.Main

新的難點: 要讓jvm在執行processClass之前先執行我們的程式碼,必須要對com.android.dx.command.dexer.Main(以下簡稱為dexer.Main)進行改造。如何才能達到這個目的?這時Instrumentation和VirtualMachine就登場了,參考第三節。

2. hook 到哪裡

一期主要是網路效能監控。如何能截獲到網路資料

通過調研發現目前有下面集中方案:

  • root手機,通過adb 命令進行截獲。
  • 建立vpn,將所有網路請求進行截獲。
  • 參考聽雲,newrelic等產品,針對特定庫進行代理截獲。也許還有其他的方式,需要繼續調研。目前我們參考newrelic等公司產品,針對特定網路請求庫進行代理的的方式進行網路資料截獲。比如okhtt3, httpclient, 等網路庫。

三. 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類, 而該類是在編譯時期由java虛擬機器啟動的, 所以我們需要通過agent來修改dexer Main類。

javaagent的主要功能如下:

  • 可以在載入class檔案之前作攔截,對位元組碼做修改
  • 可以在執行期對已載入類的位元組碼做變化

JVMTI:JVM Tool Interface,是JVM暴露出來的一些供使用者擴充套件的介面集合。JVMTI是基於事件驅動的,JVM每執行到一定的邏輯就會呼叫一些事件的回撥介面(如果有的話),這些介面可以供開發者擴充套件自己的邏輯。

instrument agent: javaagent功能就是它來實現的,另外instrument agent還有個別名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),這個名字也完全體現了其最本質的功能:就是專門為Java語言編寫的插樁服務提供支援的。

兩種載入agent的方式:

  • 在啟動時載入, 啟動JVM時指定agent類。這種方式,Instrumentation的例項通過agent class的premain方法被傳入。
  • 在執行時載入,JVM提供一種當JVM啟動完成後開啟agent機制。這種情況下,Instrumention例項通過agent程式碼中的的agentmain傳入。

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

有了javaagent, 我們就可以在編譯app時重新修改dex 的Main類,對應修改processClass方法。

4. Java Bytecode

如何修改class檔案? 我們需要了解java位元組碼,然後需要了解ASM開發。通過ASM程式設計來修改位元組碼,從而修改class檔案。(也可以使用javaassist來進行修改)

在介紹位元組程式碼指令之前,有必要先來介紹 Java 虛擬機器執行模型。我們知道,Java 程式碼是 線上程內部執行的。每個執行緒都有自己的執行棧,棧由幀組成。每個幀表示一個方法呼叫:每次 呼叫一個方法時,會將一個新幀壓入當前執行緒的執行棧。當方法返回時,或者是正常返回,或者 是因為異常返回,會將這個幀從執行棧中彈出,執行過程在發出呼叫的方法中繼續進行(這個方 法的幀現在位於棧的頂端)。

每一幀包括兩部分:一個區域性變數部分和一個運算元棧部分。區域性變數部分包含可根據索引 以隨機順序訪問的變數。由名字可以看出,運算元棧部分是一個棧,其中包含了供位元組程式碼指令 用作運算元的值。

位元組程式碼指令

位元組程式碼指令由一個標識該指令的操作碼和固定數目的引數組成:

  • 操作碼是一個無符號位元組值——即位元組程式碼名
  • 引數是靜態值,確定了精確的指令行為。它們緊跟在操作碼之後給出.比如GOTO標記 指令(其操作碼的值為 167)以一個指明下一條待執行指令的標記作為引數標記。不要 將指令引數與指令運算元相混淆:引數值是靜態已知的,儲存在編譯後的程式碼中,而 運算元值來自運算元棧,只有到執行時才能知道。

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

常見指令:

  • const 將什麼資料型別壓入運算元棧。
  • push 表示將單位元組或短整型的常量壓入運算元棧。
  • ldc 表示將什麼型別的資料從常量池中壓入運算元棧。
  • load 將某型別的區域性變數資料壓入運算元棧頂。
  • store 將運算元棧頂的資料存入指定的區域性變數中。
  • pop 從運算元棧頂彈出資料
  • dup 複製棧頂的資料並將複製的值也壓入棧頂。
  • swap 互換棧頂的資料
  • invokeVirtual 呼叫例項方法
  • invokeSepcial 呼叫超類構造方法,例項初始化,私有方法等。
  • invokeStatic 呼叫靜態方法
  • invokeInterface 呼叫介面
  • getStatic
  • getField
  • putStatic
  • putField
  • New

Java原始碼

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

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

位元組碼

// 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 開發

由於程式分析、生成和轉換技術的用途眾多,所以人們針對許多語言實現了許多用於分析、 生成和轉換程式的工具,這些語言中就包括 Java 在內。ASM 就是為 Java 語言設計的工具之一, 用於進行執行時(也是離線的)類生成與轉換。於是,人們設計了 ASM1庫,用於處理經過編譯 的 Java 類。

ASM 並不是惟一可生成和轉換已編譯 Java 類的工具,但它是最新、最高效的工具之一,可 從 http://asm.objectweb.org 下載。其主要優點如下:

  • 有一個簡單的模組API,設計完善、使用方便。
  • 文件齊全,擁有一個相關的Eclipse外掛。
  • 支援最新的 Java 版本——Java 7。
  • 小而快、非常可靠。
  • 擁有龐大的使用者社群,可以為新使用者提供支援。
  • 源許可開放,幾乎允許任意使用。

核心類: 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. 實現原理

1. Instrumentation和VirtualMachine

VirtualMachine有個loadAgent方法,它指定的agent會在main方法前啟動,並呼叫agent的agentMain方法,agentMain的第二個引數是Instrumentation,這樣我們就能夠給Instrumentation設定ClassFileTransformer來實現對dexer.Main的改造,同樣也可以用ASM來實現。一般來說,APM工具包括三個部分,plugin、agent和具體的業務jar包。這個agent就是我們說的由VirtualMachine啟動的代理。而plugin要做的事情就是呼叫loadAgent方法。對於Android Studio而言,plugin就是一個Gradle外掛。 實現gradle外掛可以用intellij建立一個gradle工程並實現Plugin< Project >介面,然後把tools.jar(在jdk的lib目錄下)和agent.jar加入到Libraries中。在META-INF/gradle-plugins目錄下建立一個properties檔案,並在檔案中加入一行內容“implementation-class=外掛類的全限定名“。artifacs配置把原始碼和META-INF加上,但不能加tools.jar和agent.jar。(tools.jar 在 jdk中, 不過一般需要自己拷貝到工程目錄中的, agent.jar開發完成後放到plugin工程中用於獲取jar包路徑)。

2. ClassFileTransformer

agent的實現相對plugin則複雜很多,首先需要提供agentmain(String args, Instrumentation inst)方法,並給Instrumentation設定ClassFileTransformer,然後在transformer裡改造dexer.Main。當jvm成功執行到我們設定的transformer時,就會發現傳進來的class根本就沒有dexer.Main。坑爹呢這是。。。前面提到了,執行dexer.Main的是dx.bat,也就是說,它和plugin根本不在一個程式裡。

3. ProcessBuilder

dx.bat其實是由ProcessBuilder的start方法啟動的,ProcessBuilder有一個command成員,儲存的是啟動目標程式攜帶的引數,只要我們給dx.bat帶上-javaagent引數就能給dx.bat所在程式指定我們的agent了。於是我們可以在執行start方法前,呼叫command方法獲取command,並往其中插入-javaagent引數。引數的值是agent.jar所在的路徑,可以使用agent.jar其中一個class類例項的getProtectionDomain().getCodeSource().getLocation().toURI().getPath()獲得。可是到了這裡我們的程式可能還是無法正確改造class。如果我們把改造類的程式碼單獨放到一個類中,然後用ASM生成位元組碼呼叫這個類的方法來對command引數進行修改,就會發現丟擲了ClassDefNotFoundError錯誤。這裡涉及到了ClassLoader的知識。

4. ClassLoader和InvocationHandler

關於ClassLoader的介紹很多,這裡不再贅述。ProcessBuilder類是由Bootstrap ClassLoader載入的,而我們自定義的類則是由AppClassLoader載入的。Bootstrap ClassLoader處於AppClassLoader的上層,我們知道,上層類載入器所載入的類是無法直接引用下層類載入器所載入的類的。但如果下層類載入器載入的類實現或繼承了上層類載入器載入的類或介面,上層類載入器載入的類獲取到下層類載入的類的例項就可以將其強制轉型為父類,並呼叫父類的方法。這個上層類載入器載入的介面,部分APM使用InvocationHandler。還有一個問題,ProcessBuilder怎麼才能獲取到InvocationHandler子類的例項呢?有一個比較巧妙的做法,在agent啟動的時候,建立InvocationHandler例項,並把它賦值給Logger的treeLock成員。treeLock是一個Object物件,並且只是用來加鎖的,沒有別的用途。但treeLock是一個final成員,所以記得要修改其修飾,去掉final。Logger同樣也是由Bootstrap ClassLoader載入,這樣ProcessBuilder就能通過反射的方式來獲取InvocationHandler例項了。(詳見:核心程式碼例子)

上層類載入器所載入的類是無法直接引用下層類載入器所載入的類的

層次 載入器
上層 BootStrapClassLoader ProcessBuilder
下層 AppClassLoader ProcessBuilderMethodVisitor操作的自定義類

這一句話的理解: 我們的目的是通過ProcessBuilderMethodVisitor將我們的程式碼(自定義修改類)寫入ProcessBuilder.class中去讓BootStrapClassLoader類載入器進行載入,而此時, BootStrapClassLoader是無法引用到我們自定義的類的,因為我們自定義的類是AppClassLoader載入的。

但如果下層類載入器載入的類實現或繼承了上層類載入器載入的類或介面,上層類載入器載入的類獲取到下層類載入的類的例項就可以將其強制轉型為父類,並呼叫父類的方法。

層次 載入器
上層 BootStrapClassLoader Looger
下層 AppClassLoader InvocationDispatcher

這句話的理解: 這裡我們可以看到自定義類InvocationDispatcher是由AppClassLoader載入的, 我們在執行RewriterAgent(AppClassLoader載入)類時,通過反射的方式將InvocationDispatcher物件放入Looger(由於引用了Looger.class,所以此時logger已經被BootStrapClassLoader載入)類的treelock物件中,即下層類載入器載入的類實現了上層類載入器載入的類;當我們通過ProcessBuilderMethodVisitor類處理ProcessBuilder.class檔案時,可以通過Logger提取成員變數,插入對應的呼叫邏輯。當執行到ProcessBuilder時,再通過這段程式碼動態代理的方式呼叫對應的業務。可以將其強制轉型為父類,並呼叫父類的方法 ,請參考http://stackoverflow.com/questions/1504633/what-is-the-point-of-invokeinterface, 這裡詳細介紹了invokeInterface 和 invokeVirtual 的區別。

5. CallSiteReplace 和 WrapReturn

實現上我們目前主要做這兩種, 一種是程式碼呼叫替換, 另一種是程式碼包裹返回。主要是提前寫好對應規則的替換程式碼, 生成配置檔案表, 在agent中visit每一個class程式碼, 遇到對應匹配呼叫時將進行程式碼替換。

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 呼叫get方法返回具體例項物件
7 null ACONST_NULL null 入棧
6 Field object invokeVirtual 呼叫setAccessible,改為可訪問的,目前棧中只剩一個物件
5 true ICONST_1 1 即為true,入棧
4 Field object dup 拷貝一份,目前棧中只剩兩個物件
3 Field object invokeVirtual 呼叫getDeclaredField 獲取treeLock儲存的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方法, 構造物件, 呼叫替換的靜態方法來替換init。
                if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) {
                    //呼叫父類構造方法
                    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));
                    //開始處理建立物件的邏輯
                    //儲存引數到本地
                    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 之前會有一次dup,及建立一次, dup一次, 此時如果執行了new 和 dup 操作樹棧中會有兩個物件。
                    this.visitInsn(Opcodes.POP);
                    if (this.newInstructionFound && this.dupInstructionFound) {
                        this.visitInsn(Opcodes.POP);
                    }
                    //載入引數到運算元棧
                    for (int arg : arguments) {
                        this.loadLocal(arg);
                    }
                    //使用要替換的方法,執行靜態方法進行物件建立
                    super.visitMethodInsn(Opcodes.INVOKESTATIC, replacementMethod.getClassName(), replacementMethod.getMethodName(), replacementMethod.getMethodDesc(), false);
                    //如果此時才呼叫了dup,也需要pop, (這一部分的場景暫時還沒有構造出來, 上面的邏輯為通用的)
                    if (this.newInstructionFound && !this.dupInstructionFound) {
                        this.visitInsn(Opcodes.POP);
                    }
                } else if (opcode == Opcodes.INVOKESTATIC) {
                    //替換靜態方法
                    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 {
                    // 其他方法呼叫, 使用新方法替換舊方法的呼叫。 先判斷建立的物件是否為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()));
                    //從運算元棧上取原始引數型別到本地變數中
                    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]);
                    }
                    //運算元棧中只剩操作物件了, 需要dup, 拷貝一份作為檢查新method的第一個引數。
                    this.dup();
                    //檢查運算元棧頂物件型別是否和新method的第一個引數一致。
                    this.instanceOf(newMethod.getArgumentTypes()[0]);

                    Label isInstanceOfLabel = new Label();
                    //instanceof 結果不等於0 則跳轉到 isInstanceofLabel,執行替換呼叫
                    this.visitJumpInsn(Opcodes.IFNE, isInstanceOfLabel);
                    //否則執行原始呼叫
                    for (int arg : originArgs) {
                        this.loadLocal(arg);
                    }
                    super.visitMethodInsn(opcode, owner, name, desc, itf);

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

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

解析

詳細見tryReplaceCallSite註釋即可。

8. 驗證

將生成的apk反編譯,檢視class 位元組碼。我們一般會通過JD-GUI來檢視。我們來檢視一下sample生成的結果:

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;
    }
  }

上面的程式碼估計沒有幾個人能夠看懂, 尤其for迴圈裡面的邏輯。其實是由於不同的反編譯工具造成的解析問題導致的,所以看起來邏輯混亂,無法符合預期。

想用檢視真實的結果, 我們來看下反編譯後的smail。

詳細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)執行了這一段語句,將結果放到了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中儲存的是否為Request.Builder型別,如果是則跳轉到cond_0, 否則執行Request.Builder.build()方法,將結果放到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 標籤:判斷v1 中的值是否為 OKHttpclient 型別, 如果是跳轉為cond_1 , 否則呼叫OKHttpclient.newCall, 並將結果放到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 標籤: 執行 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:標籤: 執行com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.build(v4), 並將結果放到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 標籤: 執行com.paic.agent.android.instrumentation.okhttp3.OkHttp3Instrumentation.newCall(v1, v2), 並將結果放到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;

檢視虛擬碼, 符合預期結果。驗證完畢。

相關文章