江賽,聽雲研發總監,負責聽雲移動端產品的研發工做。在 OSC 第 55 期廣州源創會上發表了題爲《移動端 APM 產品研發技能》的演講。現場介紹移動端 APM 產品底層技術細節與實現方法, 演示如何經過在代碼中埋點來解決移動 APP 的性能問題 ;分享在實際產品開發中碰到的問題和一些經驗,以及一些技術細節。java
移動端 APM 產品,從字面上來理解,APM(application performance monitor)就是應用性能相關的監測,可隨着如今產品的邊界愈來愈模糊,監測的範圍不只包括 performance,還包括用戶行爲,以及在穩定性、卡頓、崩潰這些方面的數據都有監測,已經遠遠超過 performance 這一個角度,畢竟產品結構愈來愈大了。程序員
因此對於這樣一個產品,要作數據監控和數據分析,它的基本前提是什麼呢?就是必需要採集大齡的數據,包括一些基本的數據。將這些數據放在不一樣的維度分析。瀏覽器
舉個例子,從網絡的角度來講,有用戶反饋某個產品在某個運營商範圍接入的狀況下,網絡性能不好。這個數據就會直接從報表裏面去體現,由於會採集到一些基本的網絡數據,也會採集到其餘的不一樣的維護數據,而後這些問題就會展示出來。安全
從這張圖來看,數據是咱們產品的一個移動研究方向,並且咱們的產品會支持蘋果、Android 還有 Web 這三端。會採集的數據包括:網絡數據、交互行爲數據、穩定性相關數據和一些其餘的數據(例如採集手機的信號。這些數據會有一些不一樣的應用,好比說運營商,它在部署各類基站的時候,會有一個參考值,就是哪一個地方信號不太好,它會在那裏部署基站,可是怎樣知道信號很差呢?不可能在每個角落都放一臺手機看信號如何。此時咱們的產品就能夠完成這個任務,移動端能夠採集到這些信號,而後根據不一樣的地域來分析手機信號分佈狀況),這就是採集數據的大概內容。服務器
而後往下細分會有更多類別。例如網絡數據,從應用層的數據來看,主要是採集 HTTP/HTTPS 的數據,但又不只僅是 HTTP/HTTPS 數據,好比說一條 HTTP 請求,假如從 Web 上或者是瀏覽器中輸入一個網址,咱們會把全部的 HTTP 請求內容分析出來,例如出去包的長度、回來包的長度和 response 的時間等等。若是出現錯誤的時候,還會把 response 的包和頭部信息打印出來,會把 HTTP 協議請求所有分析一遍,分析字節大小,響應時間,還有錯誤這些狀況。而後還會往下分析,好比 HTTP 請求訪問以前須要作 TCP 連接的所用時間。網絡
這些數據正常狀況下是沒有辦法採集的,須要特定的技術,這個也是今天我要分享的內容 —— 咱們是如何抓取這些底層數據的。架構
還有一個是頁面加載的數據,頁面的加載包含三種數據(頁面加載、瀏覽器渲染和 DOM 加載)。Android 和 iOS 會經過 JS 注入監控一些數據,和監測一些頁面加載的詳細數據。app
關於交互行爲數據,舉個例子,產品會監控用戶在一個應用裏的一些點擊行爲,像一系列的滑動,對菜單的選中。好比說點擊一個按鈕之後,若是它的響應時間過長,通常閾值是 3 秒鐘,若是點擊完按鈕 3 秒後才處理完,咱們會自動把事件抓取並上報。如今咱們還能夠作到,當監測到卡頓之後,會自動去把當前的操做截屏(能夠作一秒鐘 10 幀的截屏)。經過一秒鐘 10 幀的數據而生成的動畫,也就能看到卡頓的時候所在的頁面。這個產品暫時還沒發佈,但技術上已經實現了。如今關鍵的問題是普通的截屏會很是影響性能和耗電,如今能作到 1 幀數據在 5 毫秒左右,效率很是高,截屏速度也很是快。負載均衡
關於穩定性,穩定性就是崩潰和 ANR(卡頓)相關的。有一些開源項目能夠支持這種需求,因此相似崩潰、ANR 這些數據的採集難度不大。函數
收集了不一樣的源數據之後,就會接觸到不一樣的維度,這些維度包括地域、運營商、接入方式、設備、操做系統、應用版本以及其餘一些維度數據。根據這些維度數據和一些自定義的相關信息,會作特定的網絡數據監控。經過這個,就能夠看到對應的不一樣源數據在不一樣的維度組合下的結果,好比能夠選擇某一個地方、某一個運營商或者某個設備在某種接入方式上,它的 HTTP 請求效率,這就是基本源數據以及基本數據的應用。
不少應用廠商也嘗試本身抓取這些龐大的數據,但若是用傳統的方式來作,就意味着須要打不少的點,好比說一段代碼,須要在 excute 進入的地方打一個點,出去的地方也打一個點,同時還要把參數抓取下來作參數的解析,這就意味着若是手工來作這種工做,工做量會很是大,由於全部監控的地方都要埋點,並且一旦這段代碼發生變化,也就要從新去修改埋點的代碼,並且從新去埋點,也會致使工做量很是大。
所以作數據採集的時候,咱們有一個基本原則:儘可能不讓程序員作任何事情。添加一行初始化代碼就夠了。那麼如何採集到這些數據?這就是數據採集的基礎,自動埋點技術。這些埋點的操做不須要本身作,會經過程序自動完成。下面介紹幾種自動埋點的方法。
主要經過如下的技術手段實現:
下面對每個技術細節展開進行講述:
對於 ByteCode 的處理,支持 Java ByteCode 的注入以及 Dalvik ByteCode 的注入。在內應用層會提供 Hook 方法來 Hook 分析 C/C++ 代碼,JavaScript 相關的會經過 JS 注入的方式來採集數據。
看起來比較抽象,下面一一展開來描述:
對於 Android 程序員來講,大部分代碼都是用 Java 寫的,拓展名是 .java 的文件。但真正打包編譯完之後,會生成 apk 文件。若是你把它解壓會看到有一個 dex 文件,由於如今的包愈來愈大了,可能會有多個 dex 文件,那麼這些 .java 文件是怎麼變成 dex 文件的,這個過程是如何的?
編譯的過程是首先從 .java 文件到 class 文件,而後 class 文件再到 dex 文件。.java 文件到 class 文件是經過 javac 編譯,而後再經過 Android SDK 下的一個工具 dx 將 class 文件編譯成 dex 文件。
在 Android 的虛擬機裏面,正常狀況下編譯完之後,Java 虛擬機裏面執行的是 .class 文件(即 Java Bytecode),可是在 Android 的 Dalvik 虛擬機或者 ART 裏,不能直接執行 Java Bytecode,所以須要將 Java Bytecode 作一次轉換,轉成 Dalvik Bytecode。該過程就是使用 dx 這個工具轉換的,並且是在編譯的時候完成。其實就是不一樣的格式表述,.class 文件只是用了另一種字節碼的格式來表述。這個東西看似很簡單,但若是瞭解編譯的過程,就能夠作不少的事情。 class 文件生成了之後,尚未轉成 dex 文件這一步,就能夠經過 ASM 技術,對 Java Bytecode 進行改寫,從而插入要監控的代碼。
下面經過一個實際的例子來說述。
先來看代碼:
Example Java source: Foo.java class Foo { public static void main(String[] args) { System.out.println("Hello, world"); } public int method(int i1, int i2) { int i3 = i1 * i2; return i3 * 2; } }
這段代碼的功能很簡單,裏面有一個方法,傳進來兩個參數,先將這兩個參數相乘,再把結果除以 2 返回。經過 javac 把它編譯成 Java Bytecode,而後用 javap 能夠看到 Java Bytecode 的指令。這是一個很簡單的 Java Bytecode 指令,取得兩個參數,而後作乘積。imul 指令就是 Java Bytecode 的一個基本指令,以後就是把兩個參數壓棧,imul 指令會 pop 出棧底的兩個數。
$ javac Foo.java $ javap -v Foo public int method(int, int); flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=3 0: iload_1 1: iload_2 2: imul 3: istore_3 4: iload_3 5: iconst_2 6: imul 7: ireturn LineNumberTable: line 6: 0 line 7: 4
能夠看到,方法的名字和參數都沒變。其實 Java Bytecode 和 Dalvik Bytecode 很大的一個區別就在這裏,Java Bytecode 須要藉助堆棧來模擬這種操做(乘法、除法),經過棧來臨時存放這些變量,但在 Dalvik Bytecode 裏就不是經過棧來實現,而是經過寄存器實現。看一個棧的操做示例:
Stack Before After value1 result value2 ... ... ... (imul指令對棧的操做)
先是傳入兩個變量 value1 和 value2,imul 執行完之後就把結果加到棧裏邊,這就是一個典型的棧操做。
由於 Java Bytecode 沒有辦法在安卓手機上運行,所以須要將 Java Bytecode 繼續經過 dx 工具把它編譯成 Dalvik Bytecode。不少時候你們都是經過編譯工具進行編譯,沒有嘗試經過手工進行編譯,建議能夠嘗試一下。經過 dx 就能夠把 class 文件編譯成一個 dex 文件,而後經過 dexdump 命令,把 dex 文件 dump 出來。能夠看到,剛纔的 Java Bytecode 裏幾行乘法指令,在這就就變成了一行指令。
能夠看到,首先指令長度變小了,第二 Dalvike Bytecode 引入了寄存器的概念。而 Java Bytecode 的函數調用所有是經過棧來模擬的。這種方式對代碼性能,以及代碼結構大小有影響,並且寄存器自己的性能要比棧高不少。
再看一下,剛剛那三行代碼兩次 pop 操做,一次乘積,一次 push 操做,如今變成這樣一個操做。就是這個指令,通過目標計算器,源計算器,操做完之後,存在源計算機,如今變成這種形式。
下面來看一下 Java Bytecode 與 Dalvik Bytecode 的對比:
Java Bytecode 和 Dalvik Bytecode 有什麼區別?前者用的是棧,後者用的是寄存器。
這些對於自動插碼技術有什麼做用?前面提到的指令級插碼又有什麼做用?其實這些是基本工做,首先要對Java Bytecode 很是的熟悉,以後要了解整個編譯過程。
這個代碼就是經過動做分析 Java Bytecode 注入的,反編譯出來就是這樣。咱們須要分析一些關鍵的方法,還有特定方法,找到函數的頭和尾,插入須要的代碼,第一步爲獲取開始時間;第二,獲取完成的時間,以後進行上報。像作一些錯誤處理,會對異常進行捕捉,這樣就能夠自動分析你的 Bytecode 來作注入。
還有一個特殊的狀況,就是須要監控的是這個調用,或者說監控這個調用的反饋值,這些狀況都會出現。但全部的變化都是基於對 Bytecode 上下文的理解,而後插入對應的指令。這個技術不是咱們首創的,ASM 技術已經有不少年了,各位能夠去看一些開源的 ASM 項目。
還有一個技術,Java Bytecode 注入是咱們產品如今主要的注入方法,可是也還有不少其餘注入的方法,下面要講的就是另一種的方式 —— 經過 .smali 注入,具體的邏輯以下圖所示:
經過一些 smali 反編譯工具,轉成 smali 文件,靜態分析這些文件,分析完之後會作代碼的注入,而後從新打包,再加一個簽名就能夠了。smali 不是 Android 官方的 Bytecode,是一個開源的 Bytecode。
這些你們都不陌生,作 APP 開發不少時候會用這些工具幫助分析一些事情。一樣你也能夠借鑑一些新的思路,經過這種方式分析 APK。認爲存在惡意行爲就分析。另外還能夠作動態調試,把一些參數打印出來。
好比說寫了一個工程,能夠作一個定製,寫一個簡單的SDK。分析一個 APP 的時候,須要分析其網絡行爲,就把 SDK 注入進去,而後打包,以後看網絡訪問過程中訪問的什麼主機、IP。若是有加密,那就經過另一個話題對流作解密,通常的狀況下,傳輸的數據均可以看到。
由於 Android 中不少代碼不必定是用 Java 寫的,也能夠用 C/C++ 寫。這種代碼不能經過 Bytecode 的方式來注入。看下面這張圖
這是一個普通的調用關係,調用者調用被調用者執行,執行完之後返回。這是正常的處理流程。但若是要監測這個被調用的方法,想要拿到參數,以及這個方法執行多長時間,還想知道這個返回值,如何實現?邏輯上很簡單,把被調用方法頭幾行指令作修改。把指令改爲 JMP 指令,JMP 到這個監控方法裏面,經過 hook 的方式作跳轉。這裏作參數、相關函數的記錄,作完之後再從新按照這個軌跡返回。
如何作到這一步呢?首先,把頭幾行作跳轉。這須要對 ARM 指令,對各類架構比較熟悉才能作到。大部分程序員都學過彙編指令,但遇到的時候以爲很複雜。實際上並不複雜,只是接觸的少,其實 ARM 32 指令很少。根據後面 3 位,4 位能夠作區分。還有一些分值指令,數學預算指令。那麼,分析這些指令的時候,首先對於指令架構要很熟悉,並且,要知道源計算機,目標計算器在哪裏。好比說,最終跳轉指令的時候,要知道跳轉怎麼計算,24 位 offset 怎麼跳轉,24 位怎麼轉換爲絕對地址。若是把基本概念弄明白,不要求會寫,就能夠作下面的事情了。
先看一下剛剛說的方法怎麼作到的。
須要改寫這個方法的頭兩行指令,頭兩行指令替換成這樣的指令。PC 指令就是當前運行時的邏輯地址,PC 寄存器。由於 ARM 32 會作一個預加載,這個會指向下兩行指令。若是將 PC 指令減 4,就是變爲 PC 加 4,這個操做是把下一行指令移到 PC 寄存器中。若是改寫 PC 寄存器就實現了跳轉,雖然只有兩行代碼,可是能夠想到這其實要花很長的時間。
這須要瞭解 ARM 指令,知道這個 ARM 指令執行的過程,還要知道經過修改 PC 指令實現跳轉。經過改寫頭兩行指令,就能夠把它跳轉到任何地址。並且這個地址就是 4 字節,32 位,4G 空間。能夠跳轉到任何函數,但這還沒結束。後兩行作了之後,要把頭兩行移到另一個地方。可是,移動指令的時候由於一些指令自己就是依賴 PC 指令,因此要去作指令的修復。所以更多的工做其實就是在修復這些被移走的指令。下面的例子是一個 B 指令修改,是寫實際代碼的一部分。
來看一下這一行代碼是什麼意思。123,2 個 0 是 8 位,8123,高位是 0,0,F。若是是 31 到 32 位,咱們如今取的值是實際上就是取這 4 位,1234,取 4 位的值,經過這一行指令取這個值。而後經過 4 個值區分這些指令類型。取出來了之後,若是這個是 A,能夠看一下 B 指令的方式,1010,這個是 1010,一個是1,就是 BR 指令,跳出去再跳回來。若是無條件這裏就是 0,1010 正好是多少就是 10,就是 A,若是是 B指令。B 指令跳轉依賴寄存器,首先算出來這個地址,把絕對地址存在這裏,頭一行指令在這裏。
若是要真正把這個弄明白,能夠經過編寫 C 代碼作到。若是作到這樣以爲頗有成就感,把系統的 malloc,或者是 new 給 hook 住,能夠監測全部的 native 內存申請和釋放。
將 hook 技術應用在產品上面,發現不少的產品都是依賴這個技術的。好比安全方面,不少產品也是經過這種方式作的。還有經過這種方式來作一些底層資源修改和調度,這個能夠用在不少的方面。由於技術是爲了產品服務的,只要把技術弄明白就能夠了,最終仍是會產品化。這是像我這種作不少年技術的人切身的體會。有時候也是會沉迷在技術裏面,總以爲作一些產品的工做就是浪費時間。如今想一想,並不如此。
最後一點,前面講的這些,都是一些自動嵌碼技術,包括 Java 應用,還有 C++ 應用。數據都是自動採集的。在編譯時插碼,在運行時使用 hook,這些均可以作,由於產品已經很成熟。聽雲如今運行着 5 億終端,有一些大的電商類也已經在用聽雲的 SDK。
舉個例子,想經過聽雲對 TCP 層的監測結果來觀察負載均衡調度狀況,同一個主機有一堆 IP,正常狀況下是沒有辦法拿到這個結果的。咱們不只能夠拿到 DNS 時間,還能夠拿到 DNS 結果,真實 IP 是什麼,經過這些狀況能夠看到負載均衡服務器,即調度出來的結果狀況以及 IP 分佈狀況,另外還有 TCP 三次握手時間,SSL 握手時間等。
這些數據都很是的有用。安卓程序員常常糾結使用哪些網絡庫,是 urlconnection,仍是 okhttp。分別都有什麼優缺點。這個咱們就給大家作了一個強大的技術驗證。
第一個問題,好比說,在程序裏面連着發了 10 個 request。如今 HTTP 訪問的傳輸層都是基於 TCP,但每發一次 request 都要作一次 TCP 鏈接嗎?仔細想一想,對於同一個地址確定沒有必要,這樣作就是浪費時間。而後遇到的就是 TCP 複用技術,經過這種技術,就能夠監測對於一個同一個目標地址發生多少次 TCP connect 操做,這就知道在這個訪問時間內有沒有複用以前的鏈接。因此,就能夠得出一個指標數據,即發生了多少次 TCP 鏈接。
下圖是 APM 產品
經過這種技術能夠監測一些關鍵指標數據,由於採起底層原數據,不少點就會把這個原數據還原出應用場景,客戶想出來的場景比咱們多。這些原數據都是最寶貴的數據,而且最關鍵的是不須要你再去作額外的工做,也是 APM 的價值所在。
今天講的內容比較抽象,講的是研發過程當中的一些經驗,技巧和總結。這個技術可能對各位如今的工做不會有直接的幫助,由於太底層,但也但願能夠給各位對本身工做的方式帶去必定的思考。不管怎樣,仍是須要把底層的知識弄明白,畢竟這對於寫代碼有幫助。