1. 程式人生 > >Android效能監控實現原理

Android效能監控實現原理

涉及知識點: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檔案進行修改,從而做到嵌入我們的監控程式碼。


androidbuilder.jpg

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

。此方法的第二個引數就是class的byte陣列,於是我們只需要在進入processClass方法的時候用ASM工具對class進行改造並替換掉第二個引數,最後生成的apk就是我們改造過後的了。

新的難點: 要讓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.

由於我們要修改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傳入。

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

4. Java Bytecode

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

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

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

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

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

常見指令:

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

位元組碼

// 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。
  • 小而快、非常可靠。
  • 擁有龐大的使用者社群,可以為新使用者