Android記憶體優化
隨著Android生態的多年發展,現在4GB 記憶體的手機都變成了主流,2008 年的手機只有可憐的 140MB 記憶體,而今年的華為Mate 20 Pro 手機的記憶體已經達到了 8GB,在以前低記憶體裝置更容易出現記憶體不足引起的異常和卡頓,我們也可以通過檢視應用中使用者的手機記憶體在 2GB 以下所佔的比例來評估,所以在優化前要先定好自己的目標,這一點非常關鍵。比如針對2GB 以上的裝置,完全是兩種不同的優化思路。
記憶體優化誤區:
1、記憶體佔用越少越好。應用是否佔用了過多的記憶體,跟裝置、系統和當時情況有關,而不是 300MB、400MB 這樣一個絕對的數值。當系統記憶體充足的時候,我們可以多用一些獲得更好的效能。當系統記憶體不足的時候,希望可以做到“用時分配,及時釋放”,當系統記憶體出現壓力時,能夠迅速釋放各種快取來減少系統壓力。
- 在 Android 3.0 之前,Bitmap 物件放在Java 堆,而畫素資料是放在 Native 記憶體中。如果不手動呼叫 recycle,Bitmap Native 記憶體的回收完全依賴finalize 函式回撥,熟悉 Java 的同學應該知道,這個時機不太可控。
- Android 3.0~Android 7.0 將 Bitmap物件和畫素資料統一放到 Java 堆中,這樣就算我們不呼叫 recycle,Bitmap 記憶體也會隨著物件一起被回收。不過 Bitmap 是記憶體消耗的大戶,把它的記憶體放到 Java 堆中似乎不是那麼美妙。即使是最新的華為 Mate 20,最大的 Java 堆限制也才到 512MB,可能我的實體記憶體還有 5GB,但是應用還是會因為 Java 堆記憶體不足導致 OOM。Bitmap 放到 Java 堆的另外一個問題會引起大量的GC,對系統記憶體也沒有完全利用起來。
- 有沒有一種實現,可以將 Bitmap 記憶體放到 Native 中,也可以做到和物件一起快速釋放,同時 GC 的時候也能考慮這些記憶體防止被濫用?NativeAllocationRegistry 可以一次滿足你這三個要求,Android 8.0 正是使用這個輔助回收 Native記憶體的機制,來實現畫素資料放到 Native 記憶體中。Android 8.0 還新增了硬體點陣圖 Hardware Bitmap,它可以減少圖片記憶體並提升繪製效率。
2、Native 記憶體不用管。雖然 Android 8.0 重新將 Bitmap 記憶體放回到Native中,那麼我們是不是就可以隨心所欲地使用圖片呢?答案當然是否定的。正如前面所說當系統實體記憶體不足時,從後臺、桌面、服務、前臺,直到手機重啟。lmk 開始殺程序,系統構想的場景就像下面這張圖描述的一樣,大家有條不絮的按照優先順序排隊等著被 kill。low memory killer 的設計,是假定我們都遵守 Android 規範,但並沒有考慮到中國國情。國內很多應用就像是打不死的小強,殺死一個拉起五個。頻繁的殺死、拉起程序,又會導致 system server 卡死。當然在 Android 8.0 以後應用保活變得困難很多,但依然有一些方法可以突破。只是流程複雜些。
記憶體造成問題:
之前的崩潰優化中提到“記憶體優化”是崩潰優化工作中非常重要的一部分,類似 OOM,很多的“異常退出”其實都是由記憶體問題引起。記憶體主要會引發兩方面問題
1、異常。 異常包括 OOM、記憶體分配失敗這些崩潰,也包括因為整體記憶體不足導致應用被殺死、裝置重啟等問題。
2、卡頓。Java 記憶體不足會導致頻繁 GC,這個問題在 Dalvik虛擬機器會更加明顯。而 ART 虛擬機器在記憶體管理跟回收策略上都做大量優化,記憶體分配和 GC 效率相比提升了 5~10 倍。如果想具體測試 GC 的效能,例如暫停掛起時間、例如暫停掛起時間、總耗時、GC 吞吐量,我們可以通過傳送SIGQUIT 訊號獲得 ANR 日誌。
adb shell kill -S QUIT PID adb pull /data/anr/traces.txt複製程式碼
它包含一些 ANR 轉儲資訊以及 GC 的詳細效能資訊,另外我們還可以使用 systrace 來觀察 GC 的效能耗。
sticky concurrent mark sweep paused:Sum: 5.491ms 99% C.I. 1.464ms-2.133ms Avg: 1.830ms Max: 2.133ms// GC 暫停時間 Total time spent in GC: 502.251ms// GC 總耗時 Mean GC size throughput: 92MB/s// GC 吞吐量 Mean GC object throughput: 1.54702e+06 objects/s 複製程式碼
著手記憶體優化:
1、裝置分級。
- 相信你肯定遇到過,同一個應用在 4GB 記憶體的手機執行得非常流暢,但在 1GB 記憶體的手機就不一定可以做到,而且在系統空閒和繁忙的時候表現也不太一樣。所以記憶體優化首先需要根據裝置環境來綜合考慮,Facebook 有一個叫device-year-class 的開源庫,它會用年份來區分裝置的效能。使用類似 device-year-class 的策略對裝置分級,對於低端機使用者可以關閉複雜的動畫,或者是某些功能;使用 565 格式的圖片,使用更小的快取記憶體等。在現實環境下,不是每個使用者的裝置都跟我們的測試機一樣高階,在開發過程我們要學會思考功能要不要對低端機開啟、在系統資源吃緊的時候能不能做降級。
- 快取管理。我們需要有一套統一的快取管理機制,可以適當地使用記憶體;當“系統有難”時,也要義不容辭地歸還。我們可以使用 OnTrimMemory 回撥,根據不同的狀態決定釋放多少記憶體。對於大專案來說,可能存在幾十上百個模組,統一快取管理可以更好地監控每個模組的快取大小。
- 安裝包大小。安裝包中的程式碼、資源、圖片以及 so 庫的體積,跟它們佔用的記憶體有很大的關係。一個 80MB 的應用很難在 512MB 記憶體的手機上流暢執行,這種情況我們需要考慮針對低端機使用者推出 4MB 的輕量版本,例如 Facebook Lite、今日頭條極速版都是這個思路。
Bitmap 記憶體一般佔應用總記憶體很大一部分,所以做記憶體優化永遠無法避開圖片記憶體這個“永恆主題”。
- 統一圖片庫。圖片記憶體優化的前提是收攏圖片的呼叫,這樣我們可以做整體的控制策略。例如低端機使用 565 格式、更加嚴格的縮放演算法,可以使用 Glide、Fresco 或者採取自研都可以。而且需要進一步將所有 Bitmap.createBitmap、BitmapFactory 相關的介面也一併收攏。
- 統一監控。第一是大圖片監控:我們需要注意某張圖片記憶體佔用是否過大,例如長寬遠遠大於 View 甚至是螢幕的長寬。在開發過程中,如果檢測到不合規的圖片使用,應該立即彈出對話方塊提示圖片所在的 Activity 和堆疊,讓開發同學更快發現並解決問題。在灰度和線上環境下可以將異常資訊上報到後臺,我們可以計算有多少比例的圖片會超過螢幕的大小,也就是圖片的“超寬率”。第二是重複圖片監控:重複圖片指的是 Bitmap 的畫素資料完全一致,但是有多個不同的物件存在。這個監控不需要太多的樣本量,一般只在內部使用。第三是圖片總記憶體:通過收攏圖片使用,我們還可以統計應用所有圖片佔用的記憶體,這樣在線上就可以按不同的系統、螢幕解析度等維度去分析圖片記憶體的佔用情況。在 OOM 崩潰的時候,也可以把圖片佔用的總記憶體、Top N 圖片的記憶體都寫到崩潰日誌中,幫助我們排查問題。
- Java記憶體洩漏。建立類似LeakCanary 自動化檢測方案,至少做到 Activity 和 Fragment 的洩漏檢測。在開發過程,我們希望出現洩漏時可以彈出對話方塊,讓開發者更加容易去發現和解決問題。記憶體洩漏監控放到線上並不容易,我們可以對生成的 Hprof 記憶體快照檔案做一些優化裁剪大部分圖片對應的 byte 陣列減少檔案大小。比如一個 100MB 的檔案裁剪後一般只剩下 30MB 左右。使用 7zip 壓縮最後小於 10MB,增加了檔案上傳的成功率。
- OOM監控。美團有一個 Android 記憶體洩露自動化鏈路分析元件Probe,它在發生 OOM 的時候生成 Hprof 記憶體快照,然後通過單獨程序對這個檔案做進一步的分析。不過在線上使用這個工具風險還是比較大,在崩潰的時候生成記憶體快照有可能會導致二次崩潰,而且部分手機生成 Hprof 快照可能會耗時幾分鐘,對使用者造成的體驗影響會比較大。另外,部分 OOM 是因為虛擬記憶體不足導致,這塊需要具體問題具體分析。
- Native記憶體洩漏監控。Malloc 除錯(Malloc Debug)和 Malloc鉤子(Malloc Hook)似乎還不是那麼穩定。在 WeMobileDev 最近的一篇文章《微信 Android 終端記憶體優化實踐》 中,微信也做了一些其他方案上面的嘗試。
- 針對無法重編so的情況。使用 PLT Hook 攔截庫的記憶體分配函式,其中 PLT Hook 是 Native Hook 的一種方案,然後重定向到我們自己的實現後記錄分配的記憶體地址、大小、來源 so 庫路徑等資訊,定期掃描分配與釋放是否配對,對於不配對的分配輸出我們記錄的資訊。
- 針對可重編的so情況。通過 GCC 的“-finstrument-functions"引數給所有函式插樁,樁中模擬呼叫棧入棧出棧操作;通過 ld 的“–wrap”引數攔截記憶體分配和釋放函式,重定向到我們自己的實現後記錄分配的記憶體地址、大小、來源 so 以及插樁記錄的呼叫棧此刻的內容,定期掃描分配與釋放是否配對,對於不配對的分配輸出我們記錄的資訊。