支付寶 App 構建優化解析:通過安裝包重排布優化 Android 端啟動性能
1. 前言
本章節我們將圍繞《支付寶 App 構建優化解析》另啟新系列,細分拆解客戶端在“代碼管理”、“證書管理”、“版本管理”、“構建打包”等維度的具體實現方案展開討論,帶領大家進一步了解支付寶在 App 構建模塊下的持續優化。
本節將主要記錄通過對支付寶 Android Apk 文件的重新布局,來改善 IO 性能的過程。
2. 背景
支付寶 App 在 Android 平臺上,由于大量業務快速上線,Android 長尾機型等原因,造成啟動階段及部分核心鏈路上,性能體驗不理想,進而影響用戶的使用的感受。 從純業務角度,可以通過優化 UI 布局,優化代碼結構,優化 bundle 加載等方式,對性能體驗有所改善。作為工程技術團隊,按照傳統思維來看,似乎無法對性能優化做多少貢獻。經過一些方案調研后,我們嘗試通過對編譯產物的優化,干預構建流程,以提升 App 性能。
3. 原理
布局前后,Apk 中實際的文件并沒有本質改變,只有位置發生了變化。那么為什么這樣的調整會有性能造成影響?這個原理要追溯到 Linux 的文件系統機制。
如下圖所示,Linux 底層文件系統中 VFS 上次 App 進程之間,存在一層 pagecache,pagecache 由內存中的物理 page 組成,其內容對應磁盤上的 block。Pagecache 的大小是動態變化的,可以擴大,也可以在內存不足時縮小。Cache 緩存的存儲設備被稱為后備存儲(backing store),一個 page 通常包含多個 block,這些 block 不一定是連續的。
當內核發起一個讀請求時(例如進程發起 read() 請求),首先會檢查請求的數據是否緩存到了 pagecache 中。如果有,那么直接從內存中讀取,不需要訪問磁盤,這被稱為 cache命中(cache hit)。如果 cache 中沒有請求的數據,即 cache 未命中(cache miss),就必須從磁盤中讀取數據。
然后內核將讀取的數據緩存到 cache 中,這樣后續的讀請求就可以命中 cache 了。Page 可以只緩存一個文件部分的內容,不需要把整個文件都緩存進來。對磁盤的數據進行緩存從而提高性能主要是基于兩個因素:
第一,磁盤訪問的速度比內存慢好幾個數量級(毫秒和納秒的差距)。
第二是被訪問過的數據,有很大概率會被再次訪問。
結合 Android 系統實際來看,上層 App 每次讀取磁盤時,文件系統默認會按 16 * 4k block 去磁盤讀取數據,并把數據放到 pagecache 中。如果下次讀取文件已經在 pagecache 中,則不會發生真實的磁盤 IO,而是直接從 pagecache中 讀取,大大提升讀的速度。有緩存就有回收,pagecache 的另一個重要工作是釋放 page,從而釋放內存空間。Cache 回收的任務是選擇合適的 page 釋放,并且如果 page 是 dirty 的,需要將 page 寫回到磁盤中再釋放。
理想的做法是釋放距離下次訪問時間最久的 page,但是很明顯,這是不現實的。基于 LRU改進的 Two-List 是 Linux 使用的策略。這個回收策略非常類似業務開發領域,常見的圖片加載的緩存策略。LRU 算法是選擇最近一次訪問時間最靠前的 page,即干掉最近沒被光顧過的 page。原始 LRU 算法存在的問題是,有些文件只會被訪問一次,但是按照 LRU 的算法,即使這些文件以后再也不會被訪問了,但是如果它們是剛剛被訪問的,就不會被選中。
Two-List 策略維護了兩個list,active list 和 inactive list。在 active list 上的 page 被認為是 hot 的,不能釋放。只有 inactive list 上的 page 可以被釋放的。首次緩存的數據的 page 會被加入到 inactive list 中,已經在 inactive list 中的 page 如果再次被訪問,就會移入 active list 中。兩個鏈表都使用了偽 LRU 算法維護,新的 page 從尾部加入,移除時從頭部移除,就像隊列一樣。
如果 active list 中 page 的數量遠大于 inactive list,那么 active list 頭部的頁面會被移入 inactive list 中,從而維持兩個表的平衡。簡單的說,通過文件重布局的目的,就是將啟動階段需要用到的文件在 APK 文件中排布在一起,盡可能的利用 pagecache 機制,用最少的磁盤 IO 次數,讀取盡可能多的啟動階段需要的文件,減少 IO 開銷,從而達到提升啟動性能的目的。
4. 落地方案
在了解原理之后,就需要考慮怎么用工程化的方案在支付寶 App 上落地,主要從以下三個流程來設計方案并落地。
度量:
重布局的前提必須是精確的度量,定位到那些可以調整,需要調整的文件。這個過程需要足夠的準確,否則會導致重布局之后的效果不佳。 度量的最終目的是要,統計到支付寶啟動階段,哪些文件加載了,并且是發生真實的磁盤IO,還是命中了 pagecache 緩存。我們提供了一個度量工具,通過修改 kernel 源碼,dump 出文件系統的 IO 行為,在特定的 Android ROM 上打個補丁,用來統計啟動時刻文件行為。部分數據如下:
數據中,第一列的數據表示發生 IO 行為的文件,第二列表示該文件中此偏移量對應的部分發生了 IO 行為。
第一列表示發生 IO 的位置,如果為 0,則表示發生了真實的磁盤 IO;如果為 1,則表示從pagecache 緩存中讀取了內容。
通過數據可以發現,Apk 中部分文件,實際上是發生了磁盤 IO,可以嘗試將啟動階段, Apk 中所用到的文件排布到一起,期望通過少量的 IO,就將所有的文件全部讀到。之后的工作,需要通過解析 zip 包結構,將上述結果中,文件偏移量對應到詳細的文件名。首先需要得到安裝包中的文件排布情況,可以通過類似 010 Editor 的工具得到,為了工程化的考慮,也可以參考 zip 格式定義通過腳本分析 zip 文件實現。
然后通過解析結果和先前的統計結果對應分析,就能找到 zip 中哪些文件,在啟動階段被讀到,為重布局提供數據支撐。
重布局:
在得到一個啟動階段的文件列表后,第二步工作,就是根據這個文件列表,在構建打包階段,在 Apk 中把這部分文件排布在一起。這里需要修改 7z 壓縮工具的源碼。支付寶構建流程,為了提升壓縮效率,減少包大小,使用 7z 工具進行最后壓縮出 Apk 的過程。這里在簡單闡述下,重排布的原因,無論是那種壓縮工具,zip 中文件順序是文件系統的默認順序,即按照阿拉伯數字和字母順序。如果想指定文件排在一起,必然要打破這種規則。 修改 7z 源碼的過程,簡單思路如下,擴展一個命令行參數,我們使用了上箭頭'^'(表意性強,提前的意思),可以傳入 list.txt,然后 7z 執行輸出文件流時候,按照 list 中的文件順序,改變最后的輸出順序,從而達到重排布的目的。例如如下命令,就是將 source 目錄中,所有文件壓縮,并且把 list 中指定文件排布在 zip 包的開始位置。
7z a -tzip archive.zip source* ^list.txt
通過這種方式,就實現了文件重排布的簡單過程,當然在支付寶的構建流程中,較為復雜,中間還涉及到重打包,重簽名等一系列流程。后續內容會提到。 這里有一個小插曲,在剛開始調整文件順序時,我們通過測量發現效果并不好。后來發現了原因,原先我們調整的文件列表,只是度量階段發現,所有發生磁盤 IO 的文件,把他們排布到一起,錯誤的認為,只要他們調整了,整體 IO 情況就會改善。可是忽略了“此消彼長”的問題,如果只調整這些文件,那么原先排布在這些文件后面,利用預讀機制進緩存 cache 的文件,如果在啟動階段用到,可能會發生新的磁盤 IO。正確的調整方式,應該能精確按時間順序統計啟動階段的所有文件,排布在一起,這樣發生少量 IO,就能全部讀到 cache 中。 簡單看下某一次實驗主 Apk 中文件調整前后的效果如下,幾個和配置相關的移到文件頭部。
調整前
調整后
回歸測試:
按照所以計劃將文件全部調整完畢后,就到了驗證效果的環節。主要有以下幾種驗證方式和思路:
線下錄屏,然后拆解視頻幀,測直觀的啟動時間。
線下使用工具度量 IO 情況,觀察啟動階段磁盤 IO 數量是否減少,量化一個“cache miss 率”的概念。
線下通過埋點的方案,通過腳本,多次模擬冷啟動,取平均值測量,消除可能誤差,觀察趨勢。
線上灰度在其他優化和代碼類似情況下,只通過調整 IO,比較兩個版本的啟動時間變化。 在重布局方案實驗階段,使用一二兩種方案較多,后續工程化落地和常態化優化時,應采用三四種方案。
5. 演進
通過上述落地方案,在線下以及某些線上灰度版本中完成初步實驗后,我們考慮工程化,常態化的進行這件事情。在工程化之前,先對度量流程進行了擴充,探索出了一種較為簡單的度量手段。
度量優化:
原先的度量方案,具備較深的技術含量,在這個方案中,需要對 Linux 底層文件系統非要熟悉和了解,并且還需具備修改源碼的能力,此方案是由其他資深專家指導下實現,短期內,團隊暫時無法獨立這個方案。 為了讓整體方案可控,我們想到了直接在 Android 源碼的資源加載流程中記錄日志,然后通過日志直接分析,這樣啟動階段文件加載一目了然,當然缺陷也很明顯,無法通過判斷文件讀取是通過磁盤 IO 還是 pagecache 緩存。 干預資源加載記錄,要不通過 hook 方式,要不就是直接改 framework,刷個 ROM,考慮到工程化自動化測試的因素,采用了修改 framework 的方式,方便后續有測試平臺,直接使用特定手機跑腳本執行即可。 以 Android 7.0 版本為例,主要修改 drawable 相關流程和 xml 相關流程。其他版本如果做測試度量機型的化,修改方式類似。
xml 加載流程修改,在解析 xml 文件流程,直接打日志。
drawable 修改
刷入 ROM,替換修改后 framework 后,冷啟動支付寶,清楚緩存,通過日志過濾即可得到完整啟動文件加載列表。
工程化:
所以單點能力都基本具備單點能力都具備后,需要找到一個能盡可能自動化的方案。具體流程圖如下。 后續對于 ReApk (優化Apk)流程,可以擴展其他的構建構建產物優化方案。
6. 結果與展望
目前整體方案,已上線支付寶錢包 Android App,該單項,啟動性能,在整體全量用戶下有 5% 左右的優化效果,低端機上效果較明顯,根據不同機型,能有10%左右的啟動性能優化效果。
Facebook 的工具鏈優化方案 Redex,對于 dex 的優化,從度量到回歸測試,開源出了一整套解決方案,對于 zip 的重布局,希望未來能將此整套方案,做到盡可能的“開箱即用”,賦能公司內外更多的 App。
7. 小結
通過本節內容,我們初步了解了支付寶在 Android 客戶端如何通過安裝包重排布來優化 IO 性能。由于篇幅限制,很多技術要點我們無法一一展開。而相應的技術內核,我們同樣應用在了 mPaaS 并對外輸出,歡迎大家上手體驗:
https://tech.antfin.com/docs/2/49549
關于 Android 端啟動性能優化的設計思路和具體實踐,同樣期待你們的反饋,歡迎一起探討交流。
本文轉載自異步社區。
軟件開發 移動開發 Android
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。