《支付寶客戶端架構解析》系列將從支付寶客戶端的架構設計方案入手,細分拆解客戶端在「容器化框架設計」、「網絡優化」、「性能啓動優化」、「自動化日誌收集」、「RPC 組件設計」、「移動應用監控、診斷、定位」等具體實現,帶領你們進一步瞭解支付寶在客戶端架構上的迭代與優化歷程。git
本節將介紹支付寶 Android 客戶端啓動速度優化下的「垃圾回收」具體思路。github
應用啓動時間是移動 App 一個重要的用戶體驗環節,相對於普通的移動 App,支付寶過於龐大,必然會影響啓動速度,一些常規的優化手段在支付寶中已經作得比較完善了,本篇文章嘗試從 GC 的層面來進一步優化支付寶的啓動速度。性能優化
相對於 C 語言來講,Java 語言有一些特性,例如開發人員不用考慮內存的分配和回收,然而,進程內存管理又是必不可少的環節,妥協的結果是 Java 語言的設計者們把對象分配和回收放到了 Java虛擬機,這裏但願明確一個概念:GC 是有代價的,這個代價包括:阻塞 Java 程序的執行,佔用 CPU 資源,佔用額外內存等,谷歌的工程師意識到了 GC 對應用的影響,因此把 GC 的日誌默認輸出到了 Logcat,咱們常常可以看到 Logcat 裏輸出如下幾種 GC 日誌:bash
GC_EXPLICIT
:Dalivk 給開發人員提供的主動觸發 GC 的 API,讀者能夠參看 Google Maps 的設計來體會這個 API 的用法GC_FOR _ALLOCK
:是分配對象失敗時觸發的 GC,這個 GC 會將應用全部的 Java 線程暫停運行,直到 GC 結束。GC_CONCURRENT
:是 Java 虛擬機根據堆的當前狀態觸發的 GC,這個 GC 在 Dalvik 單獨 GC 線程裏運行,在部分時間裏不影響應用 Java 線程的運行。支付寶啓動是一個典型的關鍵路徑場景,咱們但願看到儘量少的 GC_ CONCURRENT
(若是可能,GC_ FOR_ ALLOCK
也應該縮減到最少),然而,經過 Logcat 咱們會看到很是糟糕的 GC 行爲—大量的 GC_ FOR_ ALLOCK
以及觸目驚心的 Java 線程被 WAIT_ FOR_ CONCURRENT_ GC
阻塞,以下圖所示,經過簡單統計這些GC消耗的時間,咱們可以得出GC嚴重影響應用啓動時間的結論。網絡
支付寶是 Android 系統的一個應用程序,如何可以經過影響 Dalvik 的 GC 行爲來縮短啓動時間呢?這個問題能夠分解爲兩步:架構
第一個問題答案是確定的,Android 系統的設計思路是每一個 Android 應用程序都有獨立的 Dalvik 實例,應用啓動後能夠修改本身的進程空間裏的代碼和數據,所以支付寶經過修改內存中的 Dalvik 庫文件 libdvm.so 影響 Dalvik 的行爲。框架
第二個問題的難點在於投入產出比:修改進程空間的代碼和數據是面向二進制,難度遠遠大於源代碼,也就是說稍微複雜的 Dalvik 改進工做是不可能的。ide
基於以上兩點,提出了一種設想:啓動時 GC 抑制,容許堆一直增加,直到開發人員主動中止 GC 抑制或者 OOM 中止 GC 抑制,這是一種"空間換時間"策略,用更多的內存消耗來換取啓動時間的縮短,這種策略可行有兩個前提:一是設備廠商沒有加密內存中的 Dalvik 庫文件,二是設備廠商沒有改動 Google 的 Dalvik 源碼(或者少許的改動),理論上經過白名單的方式能夠覆蓋全部設備,可是實現和維護成本都很是高。模塊化
GC 抑制的前提是 Dalvik 比較熟悉,知道如何改變 GC 的行爲,解決方案大體以下:首先在源碼級別找到抑制GC的修改方法,例如改變跳轉分支,其次,在二進制代碼裏找到 A 分支條件跳轉的"指令指紋",以及用於改變分支的二進制代碼,假設爲 override_A
,應用啓動後掃描內存中的 libdvm.so
,根據"指令指紋"定位到修改位置,而後用 override_A
覆蓋,這裏須要注意的是,"指令指紋"的定義須要有一些編譯器和 arm 指令集知識,實現 GC 抑制主要實現瞭如下 4 個部分:函數
取消 softlimit 檢測的目的是最大限度的分配對象,下圖爲 softlimit 檢查對應的 arm 指令片斷,位於 dvmHeapSourceAlloc
函數中,OXE057 對應於"return NULL"的分支,若是咱們想永遠不進入"return NULL"分支,能夠改變 cmp 指令的結果,在具體實現裏咱們把"0X42"做爲"指令指紋"來識別並且修改成 "cmp r0, r0",這樣就能夠實現取消 softlimit 檢查。
7616c: 42a1 cmp r1, r4
7616e: d901 bls.n 76174 <_Z18dvmHeapSourceAllocj+0x20>
76170: 2400 movs r4, #0
76172: e057 b.n 76224 <_Z18dvmHeapSourceAllocj+0xd0>
76174: f8df 90bc ldr.w r9, [pc, #188] ; 76234 <_Z18dvmHeapSourceAllocj+0xe0>
76178: 6a28 ldr r0, [r5, #32]
7617a: f853 3009 ldr.w r3, [r3, r9]
7617e: 7d1a ldrb r2, [r3, #20]
void* dvmHeapSourceAlloc(size_t n)
{
...
if (heap->bytesAllocated + n > hs->softLimit) {
/*
* This allocation would push us over the soft limit; act as
* if the heap is full.
/
return NULL;
複製代碼
取消 GC 線程喚醒的目的是防止 GC 線程頻繁喚醒致使的線程抖動。下圖是對應的 C++ 代碼和 arm 指令片斷,這段代碼一樣位於 dvmHeapSourceAlloc
函數中。在具體實現裏咱們會依次掃描 libdvm.so
的 dynstr、dynsym、rel.plt 和 plt 區域獲取 pthreadcondsignal@plt 的地址,而後遍歷 dvmHeapSourceAlloc
中的全部分支跳轉,計算跳轉目的地址。
若是發現 pthreadcondsignal@plt 和當前分支跳轉目的地址配置,擦除這條指令便可。
if (heap->bytesAllocated > heap->concurrentStartBytes) {
/
* We have exceeded the allocation threshold. Wake up the
* garbage collector.
*/
dvmSignalCond(&gHs->gcThreadCond);
}
7621c: 6800 ldr r0, [r0, #0]
7621e: 30b4 adds r0, #180 ; 0xb4
76220: f7a9 ed0e blx 1fc40
76224: 4620 mov r0, r4
76226: e8bd 83f8 ldmia.w sp!, {r3, r4, r5, r6, r7, r8, r9, pc}
複製代碼
取消 GC 例程函數採用鉤子技術來實現,咱們將 GC 抑制封裝成了兩個 native 接口 doStartSuppressGC
和 doStopSuppressGC
;而且進一步封裝爲 JNI 接口,便於開發者在 Java 裏調用。通常的應用方式是,開發者經過日誌看到支付寶在某個場景會觸發大量的 GC 且這個 GC 影響用戶體驗(響應時間慢或者動畫卡頓),而後在這個場景先後插入 doStartSuppressGC
和 doStopSuppressGC
。
以支付寶冷啓動場景爲例,咱們在容器 Quinox 的 attachBaseContext
函數裏插入 doStartSuppressGC
,在首頁加載結束時插入 doStopSuppressGC
。
若是僅僅考慮在支付寶啓動過程當中抑制 GC,不須要考慮 OOM 中止 GC 抑制的實現,由於支付寶啓動不足以觸發 OOM。可是咱們但願 GC 抑制成爲一個基礎模塊,可以應用到更多場景中。若是程序在調用 doStopSuppressGC
前觸發了 OOM,則須要在 OOM 發生前中止 GC 抑制。和前面簡單的改變分支跳轉方向不一樣,須要在 OOM 發生前注入一個新的的分支跳轉,這個新分支的代碼由咱們來實現。新分支主要功能是,調用 doStopSuppressGC
,而後去掉注入的新分支,最後跳回 Dalvik 執行 OOM。
實現一樣採用傳統的鉤子技術。在鉤子函數 dvmCollectGarbageInternal
裏:
dvmCollectGarbageInternal
。實現中使用了開源的二進制注入框架:github.com/crmulliner/… 。
這裏須要注意的是,在熱點函數裏使用這個框架提供的 pre_hook
和 post_hook
的性能開銷很是大。
本文裏的設計只會用到一次 pre_hook
,因此不存在性能問題。 看到的這裏讀者可能會問,這種經過「指令指紋」的方式靠譜麼?個人答案是,漏判不影響正確性,誤判理論上存在但機率極小(誤判指「指令指紋」定位到錯誤代碼位置)。即便誤判發生了,咱們還有最後一層保障——基礎架構組同窗實現的容災機制。當誤判致使程序異常沒法完成正常啓動時,重啓支付寶並且在後續的啓動中直接放棄 GC 抑制。
上圖的啓動時間的數據是在內部的 Android 4.x 測試設備上得到的(沒有標註 release 表示 debug 版本)。從圖表上來看,支付寶客戶端的啓動時間縮短了 15%~30%。
經過本節內容,咱們初步瞭解了支付寶在 Android 客戶端啓動性能優化下的「垃圾回收」機制和具體實踐,因爲篇幅限制,不少技術要點咱們沒法一一展開。而相應的技術內核,咱們一樣應用在了 mPaaS 並對外輸出,歡迎你們上手體驗:
關於 Android 端啓動性能優化的設計思路和具體實踐,一樣期待大家的反饋,歡迎一塊兒探討交流。
往期閱讀
《開篇 | 模塊化與解耦式開發在螞蟻金服 mPaaS 深度實踐探討》