不止性能优化,移动端 APM 产品研发技能
2019獨角獸企業重金招聘Python工程師標準>>>
江賽,聽云研發總監,負責聽云移動端產品的研發工作。在 OSC 第 55 期廣州源創會上發表了題為《移動端 APM 產品研發技能》的演講。現場介紹移動端 APM 產品底層技術細節與實現方法, 演示如何通過在代碼中埋點來解決移動 APP 的性能問題 ;分享在實際產品開發中碰到的問題和一些經驗,以及一些技術細節。
一、移動 APM 概況
移動端 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 注入監控一些數據,和監測一些頁面加載的詳細數據。
關于交互行為數據,舉個例子,產品會監控用戶在一個應用里的一些點擊行為,像一系列的滑動,對菜單的選中。比如說點擊一個按鈕以后,如果它的響應時間過長,一般閾值是 3 秒鐘,如果點擊完按鈕 3 秒后才處理完,我們會自動把事件抓取并上報。現在我們還可以做到,當監測到卡頓以后,會自動去把當前的操作截屏(可以做一秒鐘 10 幀的截屏)。通過一秒鐘 10 幀的數據而生成的動畫,也就能看到卡頓的時候所在的頁面。這個產品暫時還沒發布,但技術上已經實現了。現在關鍵的問題是普通的截屏會非常影響性能和耗電,現在能做到 1 幀數據在 5 毫秒左右,效率非常高,截屏速度也非常快。
關于穩定性,穩定性就是崩潰和 ANR(卡頓)相關的。有一些開源項目可以支持這種需求,所以類似崩潰、ANR 這些數據的采集難度不大。
收集了不同的源數據以后,就會接觸到不同的維度,這些維度包括地域、運營商、接入方式、設備、操作系統、應用版本以及其他一些維度數據。根據這些維度數據和一些自定義的相關信息,會做特定的網絡數據監控。通過這個,就可以看到對應的不同源數據在不同的維度組合下的結果,比如可以選擇某一個地方、某一個運營商或者某個設備在某種接入方式上,它的 HTTP 請求效率,這就是基本源數據以及基本數據的應用。
很多應用廠商也嘗試自己抓取這些龐大的數據,但如果用傳統的方式來做,就意味著需要打很多的點,比如說一段代碼,需要在 excute 進入的地方打一個點,出去的地方也打一個點,同時還要把參數抓取下來做參數的解析,這就意味著如果手工來做這種工作,工作量會非常大,因為所有監控的地方都要埋點,而且一旦這段代碼發生變化,也就要重新去修改埋點的代碼,而且重新去埋點,也會導致工作量非常大。
因此做數據采集的時候,我們有一個基本原則:盡量不讓程序員做任何事情。添加一行初始化代碼就夠了。那么如何采集到這些數據?這就是數據采集的基礎,自動埋點技術。這些埋點的操作不需要自己做,會通過程序自動完成。下面介紹幾種自動埋點的方法。
二、APM 實現 —— 自動埋點技術的介紹
主要通過以下的技術手段實現:
下面對每一個技術細節展開進行講述:
對于 ByteCode 的處理,支持 Java ByteCode 的注入以及 Dalvik ByteCode 的注入。在內應用層會提供 Hook 方法來 Hook 分析 C/C++ 代碼,JavaScript 相關的會通過 JS 注入的方式來采集數據。
看起來比較抽象,下面一一展開來描述:
1. 從 Java 源代碼到 Dalvik Bytecode
對于 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 Foopublic int method(int, int);flags: ACC_PUBLICCode:stack=2, locals=4, args_size=30: iload_11: iload_22: imul3: istore_34: iload_35: iconst_26: imul7: ireturnLineNumberTable:line 6: 0line 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。如果有加密,那就通過另外一個話題對流做解密,一般的情況下,傳輸的數據都可以看到。
2. APM 實現 ——?native inline hook
因為 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 的價值所在。
三、總結
今天講的內容比較抽象,講的是研發過程中的一些經驗,技巧和總結。這個技術可能對各位現在的工作不會有直接的幫助,因為太底層,但也希望可以給各位對自己工作的方式帶去一定的思考。無論怎樣,還是需要把底層的知識弄明白,畢竟這對于寫代碼有幫助。
轉載于:https://my.oschina.net/osccreate/blog/795760
總結
以上是生活随笔為你收集整理的不止性能优化,移动端 APM 产品研发技能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL查看数据库相关信息
- 下一篇: 2、MapReduce的job提交启动过