天天看點

mPaaS 3.0 多媒體元件釋出 | 支付寶百億級圖檔元件 xMedia 錘煉之路 (圖檔緩存篇)

mPaaS 3.0 多媒體元件釋出 | 支付寶百億級圖檔元件 xMedia 錘煉之路 (圖檔緩存篇)

一. 背景介紹

圖檔加載一直是 Android App 面臨的“老大難”問題,加載速度與記憶體消耗天生就是一個沖突統一體。我們依托支付寶超級 App 複雜的生态業務場景,借鑒業界領先的開源架構 Fresco、Picasso,取其精華,棄其糟粕,并獨創性地使用 Ashmem、Native Mem Cache、Bitmap Reuse、分場景緩存、圖檔分大小緩存等多元一體的圖檔加載技術,實作了加載速度與記憶體消耗的完美平衡。

曆經三年的風雨洗禮沉澱,xMedia 多媒體圖檔加載元件已經成為支付寶重要的驅動力,承載了絕大部分業務,與此同時,我們也通過移動開發平台 mPaaS 對外輸出,向外界企業提供穩定的圖檔加載技術。

二. Android 記憶體基礎與挑戰

Android 系統應用單個程序堆記憶體配置設定有限,再加上不同 Android 手機硬體性能和系統版本參差不齊,對于大型App 來說,尤其是包含圖檔加載元件的 App,如何高效合理使用 Android 記憶體已經是一個必不可少的話題。

工欲善其事,必先利其器。想要 App 高效合理地利用記憶體,還需要先了解下 Android 系統記憶體相關的一些基礎知識。

1.Android 記憶體分類

對于手機來說,存儲空間跟計算機裝置一樣分為 ROM 和 RAM。

| ROM (Read Only Memory):

名字上解釋為隻讀記憶體,其實ROM種類也分很多種,有隻讀的,有可讀寫的,主要用于存儲一些資料資訊,斷點後資料不會丢失。

| RAM (Rondom Access Memory):

手機的運作時的實體記憶體,負責程式的運作以及資料交換,斷電時存儲資訊丢失。程式程序的記憶體空間隻是虛拟記憶體,而程式運作實際需要的是 RAM 實際實體記憶體,作業系統會将程式申請的程序虛拟記憶體映射到實體記憶體 RAM 中。

在 Android 應用程序中一般記憶體可分為 Heap 堆記憶體、Code 代碼區、Stack 棧記憶體、Graphics 顯存、私有非共享記憶體以及系統記憶體,其中 Heap 記憶體又分為 Davilk Heap 以及 Native Heap。

Android 可以通過 adb shell dumpsys meminfo+package name 或 pid 指令來檢視目前程序記憶體占用情況,如圖 1 所示。

mPaaS 3.0 多媒體元件釋出 | 支付寶百億級圖檔元件 xMedia 錘煉之路 (圖檔緩存篇)

圖1:通過 dumpsys 輸出的記憶體占用情況

| 記憶體分類說明如下:

類型 描述
Native Heap 從C 或C++ 代碼配置設定的對象記憶體。 Native Heap就是在Native Code中使用malloc等配置設定出來的記憶體,這部分記憶體是不受Java Object Heap的大小限制的,也就是它可以自由使用,當然它是會受到系統的限制,其上限值一般為系統RAM的2/3大小。
Dalvik Heap 從Java 或 Kotlin 代碼配置設定的對象記憶體,Android系統對每個程序的Dalvik Heap大小做了限制,具體可以通過反射調用SystemProperties的方法來擷取到程序的最大Heap記憶體值。
Code 代碼和資源(如 dex 位元組碼、已優化或已編譯的 dex 碼、.so 庫和字型)占用的記憶體。
Stack 系統棧,由作業系統配置設定,主要存儲函數位址、參數、局部變量、遞歸資訊等,stack 空間不大,一般為幾MB。
Cursor 位于 /dev/ashmem/Cursor,Cursor 占用的記憶體。
.* mmap 各種用于存放.so、.dex、.apk、.jar、.ttf 等檔案檔案存儲映射所占用的記憶體。
AshMem 匿名共享記憶體,基于 mmap 系統實作,跟mmap的差別在于 AshMem 通過注冊 Cache Shrinker 來控制記憶體的回收。
Other dev 内部 Driver 占用。
EGL mtrack 占用的是 Graphics 記憶體,用于圖形緩沖隊列項螢幕顯示圖形像素所使用的記憶體。

通過圖 1 可以簡單直覺的了解 Android 程序的記憶體分類和使用基本情況。對于應用開發者來說,直接接觸到的記憶體操作主要集中在 Dalvik Heap 和 Native Heap,尤其是 Dalvik Heap 記憶體,經常程式使用不當就遇到 OOM 的情況。

為何應用程式容易出現 OOM,并不是系統 RAM 實體記憶體不夠,而是系統對虛拟機程序的 Dalvik Heap 大小做了強制限制,一旦應用程式配置設定所使用的 Dalvik Heap 記憶體總和大小超過了程序限制門檻值時,底層就會往應用層抛出 OOM 的異常。

2.Android 記憶體回收機制

既然應用程式容易出現 OOM,而 Android 上層應用大部分基于 Java 語言的程式開發,開發者不用像 C/C++ 開發那樣需要顯示的配置設定和釋放記憶體,絕大部分都是統一交由系統的垃圾回收機制進行記憶體的回收管理,記憶體好像變得一切都不在自己掌控中似的。

開發中也經常因為一些記憶體洩露和記憶體不合理造成系統頻繁觸發 GC 和 OOM,在系統 GC 時會暫停線程工作,導緻應用運作卡頓。是以作為應用開發者了解其中的記憶體回收機制還是有必要的。

Android 記憶體 GC 回收有兩個層面,分别為程序内的記憶體回收和程序級的記憶體回收。

| 程序内的記憶體回收:

主要是虛拟機自身的垃圾回收和系統記憶體狀态發生變化時,通知應用程式讓開發者自己進行記憶體回收。其中虛拟機的垃圾回收機制是通過虛拟機監測應用程式裡面的對象建立和使用情況,并在一定條件下銷毀回收無用對象占用的記憶體,這裡無用對象的識别通常有引用計數、對象标記追蹤以及分代等算法,相關算法具體原理可以

參考

。即使有了虛拟機自動回收那些不再被引用的對象,但開發者也不能無節制的使用記憶體進而導緻 OOM,開發者一般需要在适當的場合确認某些對象不再被使用時,主動将其引用釋放,避免出現無用對象被長期持有造成記憶體洩露,而虛拟機在記憶體回收的時候無法對洩露對象釋放記憶體。

| 程序級記憶體回收:

原則是按照程序的優先級進行記憶體回收,程序的優先級越低越容易被回收,如圖 2 所示,Android 程序優先級預設分為 5 種,其優先級從低到高依次為“空程序->背景程序->服務程序->可見程序->前台程序”。

在 Android 中以程序的 oom_adj 值代表程序的優先級,可通過 adb shell cat /proc/ 程序 pid/oom_adj 來檢視程序的 oom_adj 值大小,程序的 oom_adj 值越大其優先級越低。Android 的記憶體回收是通過 Frame Work 層和 Linux 核心層協調完成的,整體流程如圖 3 所示。

在 Framework 層,AMS(Activity Manager Service) 負責集中管理程序的記憶體配置設定以及調整程序的 oom_adj 值,然後将 oom_adj 值通知到核心層,同時根據系統記憶體以及程序狀态通知應用程式記憶體不足,便于開發者自己主動回收記憶體。

核心層裡面又分為 OOM Killer 和 LMK(Low Memory Killer),OOM Killer 是 Linux 下的記憶體回收機制,在系統記憶體耗盡無法配置設定新的記憶體情況下,啟用它選擇性的殺掉一些程序,到了 OOM 的時候,整個系統已經出現不穩定;而LMK 是 Andorid 基于 OOM Killer 原理所擴充的一個多層次 OOM Killer,在未到達 OOM 之前根據記憶體門檻值級别提前觸發記憶體回收,在使用者内置空間中指定了一組記憶體臨界值,當其中的某個值與程序描述中的 oom_adj 值在同一範圍時,将該程序 kill 掉。關于 LMK 的詳細介紹請

mPaaS 3.0 多媒體元件釋出 | 支付寶百億級圖檔元件 xMedia 錘煉之路 (圖檔緩存篇)

​​ 圖2:Android的程序優先級

mPaaS 3.0 多媒體元件釋出 | 支付寶百億級圖檔元件 xMedia 錘煉之路 (圖檔緩存篇)

圖3:Android的程序級記憶體回收流程

3. 業界圖檔元件

通過上面對 Android 記憶體分類及回收機制的簡單介紹,對于使用大量圖檔的 App 來說,解碼後的圖檔,即 Bitmap,占用大量的記憶體,勢必更加容易觸發頻繁的 GC。

目前業界幾款比較成熟的開源圖檔加載元件有 Facebook 的 Fresco,Google 的 Glide,Square 的 Picasso 等,其圖檔緩存均使用了三級緩存技術,即“記憶體緩存+磁盤緩存+網絡”。加載的優先級從高到低依次為“記憶體緩存->磁盤緩存->網絡”。在記憶體緩存方面,采用的是直接緩存 Bitmap 對象,部分政策大同小異,如圖 4 所示。

mPaaS 3.0 多媒體元件釋出 | 支付寶百億級圖檔元件 xMedia 錘煉之路 (圖檔緩存篇)

圖4:業界圖檔元件的記憶體緩存政策示意圖

| Fresco:

記憶體緩存使用的是 CountingMemoryCahce,裡面有包含了正在使用的緩存 mCachedEntries 以及将要回收的緩存 mExclusiveEntries,都是基于 CountingLruMap 存放的。記憶體緩存的内容包含 Bitmap 以及未解碼的圖檔資料 EncodedImage,優先檢查 Bitmap 的緩存,若沒有再去未解碼的圖檔記憶體緩存中擷取并解碼。

對于 Bitmap 記憶體緩存:

在 5.0 以下系統,其 KitKatPurgreableDecoder 解碼器利用系統特性将解碼 Bitmap 的pixel(像素資料)放到 AshMem 中(在實際測試中 Native Heap 也占用了一份資料),在圖檔不占用的時候主動釋放,從圖 1 中可以看到,AshMem 是不占用 Java Heap 記憶體的,是以Bitmap 的緩存不會占用大量的 Java Heap ,可以減少因圖檔占用 Java 堆記憶體而引發 GC 和 OOM 的頻率。

在 5.0 以上系統,其 ArtDecoder 裡面直接調用 BitmapFactory 進行圖檔解碼生成 Bitmap,生成的 Bitmap 占用的記憶體為 Java Heap 記憶體,隻不過在解碼過程中将 BitmapOptions 的 inBitmap 和 inTempStorage 屬性分别與 BitpmapPool 和 SyncronizedPool 實作複用,進而最大合理的利用和優化記憶體。詳細的解碼流程可

| Glide:

記憶體緩存設計采樣的是 LruCache+WeakReference 結合的方式來直接存儲 Bitmap 對象,而 Bitmap 對象是從 BitmapPool 中重複複用的,這樣減少了頻繁建立和回收 Bitmap 減少記憶體抖動。

| Picasso:

基于 LinkedHashMap 基礎上實作的 LruCache 來存儲 Bitmap 對象,Bitmap 對象占用的完全是 Java Heap 記憶體,是以其最大緩存容量僅為單程序最大記憶體值的 15%。

通過對比知道,除了 Fresco 外,另外兩種圖檔元件基本都是直接采用 LruCache+Bitmap 的方式,且 Bitmap 占用的都是 Java Heap 記憶體,而 Fresco 在部分系統版本上使用了所謂的黑科技将 Bitmap 占用的記憶體轉移到 AshMem,進而減少 Java Heap 記憶體的占用。

xMedia 圖檔元件的記憶體緩存則采用了多元一體的緩存設計,後面會詳細介紹。

4.技術挑戰

對于支付寶這種 App 複雜的生态業務場景,xMedia 一開始使用基于 LRU 淘汰機制的普通堆記憶體緩存技術已經不能滿足體驗與性能之間的平衡,在整個開發過程中遇到了以下坑:

| 主程序圖檔記憶體緩存占用 Java Heap 過高

大量的圖檔記憶體緩存導緻 App 占用 Java Heap 記憶體過高,容易頻繁觸發 GC 導緻頁面卡頓。

背景程序記憶體過高容易被 kill 掉,保持 App 低記憶體而不影響體驗很重要。

圖檔記憶體在整個 App 程序中不能占用過多,否則容易導緻其他業務或功能記憶體吃緊而導緻功能或體驗影響。

| 大圖緩存會加速小圖緩存淘汰

采用 LruCache+Bitmap,超大圖檔解碼後占用記憶體過大,例如一張 1280*1280 按 ARGB8888 模式解碼出來占用的記憶體接近 6M,而低端機上單個程序配置設定總的 Heap 記憶體大小才 100M 左右,圖檔記憶體緩存最多隻能幾十兆,存放大圖頂多也就 10 來張,很容易引發圖檔記憶體緩存 LRU 淘汰,影響小圖加載的體驗。

普通業務的圖檔記憶體緩存在到達緩存上限值時是希望能有效被回收,但是也有特定業務是不希望被頻繁回收,比如頭像記憶體占用小但使用頻率較高的業務場景。

Gif 包含多幀圖檔,每幀如果單獨解碼生成 Bitmap,則一個動畫需要緩存很多 Bitmap,更容易導緻普通圖檔被回收。

三. 精細化記憶體緩存

為了解決以上踩過的坑,思路是比較明确的,就是盡量減小圖檔緩存在 Java Heap 中所占比例,如圖檔緩存單獨程序、修改程序 Java Heap 限制、轉移圖檔記憶體至非 Java Heap 存儲區。最終 xMedia 選擇了如圖4中的方案,采用了三類記憶體緩存設計:普通緩存 NativeHeap,高速緩存 Heap,臨時緩存 SoftReference。

1. 普通緩存 NativeHeap

顧名思義使用 Native 記憶體作為圖檔的記憶體緩存,主要是 Native 記憶體不受虛拟機記憶體回收控制,能有效減少Java堆記憶體占用進而降低 GC 的機率。

- 在 5.0 系統版本以下,使用 LruCache 直接管了解碼使用 AshMem 記憶體的 Bitmap。

AshMem 記憶體不同于普通的堆記憶體,這部分記憶體與 Native 記憶體區類似,受 Android 系統底層管理的,在 Android 圖檔調用系統解碼的時候 BitmapFactory.Options 中有這 2 個屬性 inPurgeable 和 inInputShareable,通過這個屬性設定就能保證解碼出來的 Bitmap 使用 AshMem,這種記憶體在 Android 系統裡面是不被計算到普通堆記憶體的占用,是以不容易觸發 GC 和 OOM。

- 在 5.0 及以上版本使用 NativeCache。

NativeCache 方案占用的是 Native Heap 記憶體,對于使用頻率一般的圖檔,建議使用,實作原理:上層使用LruCache 管理緩存資訊,key 是唯一索引圖檔的 key,value 是儲存了 Bitmap Native 記憶體拷貝的指針的BitmapInfo。有當緩存發生淘汰時,就把對應的 Native 的記憶體進行釋放。兩種方案都是占程序記憶體的 3/8,最大不超過 96M。

在最開始的記憶體緩存優化中,進行了多套方案嘗試對比,在 Android 4.0 及以上系統支援 Bitmap 的複用情況下最終選擇了使用 JNI 接口自己管理 C 記憶體的 Native 方案。

以下為記憶體讀取耗時資料測試對比,結果如圖 5 和圖 6 所示:

mPaaS 3.0 多媒體元件釋出 | 支付寶百億級圖檔元件 xMedia 錘煉之路 (圖檔緩存篇)

​​

圖5.Native(Bitmap複用)與Heap記憶體圖檔加載耗時      圖6.Native記憶體Bitmap複用與未複用加載耗時

- 測試條件:

紅米 Note1,系統版本 4.4.2,單個程序系統預設配置設定 128M 最大堆記憶體。

- 測試結果:

1)從圖 5 看,基于 Native 的圖檔記憶體緩存在讀取速度上基本控制在 3ms 以内,比純粹的基于 Heap 的記憶體速度耗時平均多1ms左右,基本可認為基于 Native 的記憶體讀取速度跟跟普通 Heap 記憶體讀取速度一樣。

2)從圖 6 看,Native 記憶體在 Bitmap 未複用(每次加載都從系統建立新的 Bitmap)的情況下,會周期性出現某次加載耗時到 100ms 以上的情況,原因主每次加載都頻繁建立新的 Bitmap 會增加系統堆記憶體開銷,引起記憶體抖動,進而增大了系統 GC 的頻率,尤其在低端機型上較明顯,如圖 7 所示。

mPaaS 3.0 多媒體元件釋出 | 支付寶百億級圖檔元件 xMedia 錘煉之路 (圖檔緩存篇)

圖7.未複用情況頻繁觸發了GC

2. 高速緩存 Heap

此緩存是普通的基于 LRU 淘汰政策的堆記憶體緩存,總大小為目前程序的 1/8,最大不超過 64M,存儲的内容為圖檔解碼後的 Bitmap 對象,主要用于解決頭像這種占用記憶體不大但使用頻率較高的業務場景。

3.臨時緩存 SoftReference

此緩存主要用于兩種場景:存儲 Gif 相關的對象和超大圖對象,占用的是 Java Heap 記憶體,實作原理,通過SoftReference 保留對 Bitmap 或 Gif 對象的引用,在記憶體吃緊時,可以及時 GC,騰出記憶體。主要為了減少因單個大記憶體圖(5M 預設為大圖)加載會淘汰很多小記憶體圖的場景,提升使用者圖檔體驗。

最終通過上面三種記憶體緩存組合起來使用達到圖檔記憶體的精細化配置設定和管理。

四. 競品測試對比

測試條件:基于 Android 4.4 和 6.0 系統上,在同一界面使用不同的圖檔元件加載 20 張本地圖檔。以下為各圖檔元件的記憶體占用情況,結果如圖 8 和圖 9 所示。

mPaaS 3.0 多媒體元件釋出 | 支付寶百億級圖檔元件 xMedia 錘煉之路 (圖檔緩存篇)

圖8:Android 4.4系統上記憶體占用對比          圖9:Android 6.0系統上記憶體占用對比

測試結果說明:

1.Android 4.4 系統上

| Java Heap 記憶體占用:

由高到低依次為 Picasso->Glide->(Fresco 和 xMedia)。

其中 Fresco 和 xMedia 圖檔緩存是沒有占用 Java Heap 記憶體。在退出測試界面 GC 後,Picasso 沒有釋放 Java Heap 記憶體,而 Glide 内部則進行了主動釋放。

| Native Heap 記憶體占用:

由高到低依次為 Fresco->xMedia->(Picasso和Glide)。

其中 Fresco 使用所謂黑科技到将圖檔記憶體緩存放到AshMem,但實際上 AshMem 跟 Native Heap 是兩塊不同的記憶體區域,Fresco 在 AshMem 和 Native Heap 各占用一份;而 xMedia 并沒有占用 Native Heap,而是隻占用 AshMem;Picasso 和 Glide 則均不占用 Native 和 AshMem 記憶體。至于為何說 Fresco在AshMem 和 Native Heap 各占用一份,而 xMedia 隻占用了 AshMem,通過 dump 目前程序記憶體占用就一目了然,圖 10 中 Fresco 加載圖檔前後 Native Heap 以及 AshMem 占用均發生較大變化;而圖 11 中 xMedia 圖檔加載前後隻有 AshMem 變化較大。

mPaaS 3.0 多媒體元件釋出 | 支付寶百億級圖檔元件 xMedia 錘煉之路 (圖檔緩存篇)

圖10:Fresco 加載圖檔前後記憶體占用情況           圖11:xMedia 加載圖檔前後記憶體占用情

2. Android 6.0 系統上

由高到低依次為 Fresco->Picasso->xMedia->Glide。

四種圖檔元件均占有 Java Heap,其中 xMedia 并不直接緩存 Bitmap,而是界面UI控件引用了這些 Bitmap,是以導緻使用 xMedia 時占用 Java Heap,但是當退出測試界面并 GC 後整體 Java Heap 便釋放,下次再進入測試頁面則直接從 Native 将對應的圖檔資料 copy 到新建立或複用的 Bitmap 中即可顯示;Glide 在退出測試界面後内部會主動釋放掉所有的圖檔記憶體緩存,但是在重新進入測試頁面加載時需要全部重新解碼,緩存的複用率不高。

由高到低依次為 xMedia->(Fresco、Picasso和Glide)。

其中隻有 xMedia 的圖檔緩存用到 Native Heap,而其它三個均使用的是 Java Heap。

總的來說,在 5.0 系統以下,xMedia 在Java Heap 和 Native Heap 上均占有優勢;5.0 以上系統,xMedia 突破了圖檔記憶體緩存使用 Native Heap 的技術,雖說從 Java Heap 還是 Native Heap 占用來看,Glide 的 Java Heap 和 Native Heap 最小,但 Glide 隻要 Bitmap 不再使用後就會主動回收,下次加載需要重新解碼,緩存複用率不高;另外 xMedia 對于正在顯示的圖檔會占用雙份記憶體,對于不再顯示的圖檔隻占用 Native Heap,但是相對 Glide 好處在于退出界面後 Native 的記憶體緩存仍然存在,下次再使用時不需要重新解碼圖檔,效率上更有優勢。 Fresco 和 Picasso 的整體表現相對 xMedia 和 Glide 要偏弱。

五. 其它優化點

  1. 針對普通大圖,通過限制最大邊為 1280 降低圖檔大小以及記憶體大小,針對會話圖檔,我們提供了縮略圖(120x120)、大圖(1280x1280)、原圖 3 個不同級别尺寸的圖檔,即使超大原圖,我們也會限制最大邊尺寸,然後解碼的時候再采樣處理。
  2. 對于會話的縮略模糊圖,直接通過服務端裁剪縮放後由push消息将縮放後的模糊圖檔推送到用戶端直接渲染顯示,避免了檢視圖檔消息時再次網絡請求會後渲染中間出現灰底情況。
  3. 壓背景分不同階段對圖檔記憶體緩存進行主動清理,保證壓背景後錢包整體記憶體處于低位運作,減少背景程序被kill掉的機率。
  4. 定時清理不常用記憶體緩存,原理是每次使用時更新緩存的使用時間,然後定時去掃描超過一定時間的緩存并主動清理掉。
  5. 支援普通 Listview、ViewPager、RecyclerView 的滑動過程中停止加載,滑動結束後再加載,減少一些不必要的任務開銷。
  6. Gif 圖檔使用自研解碼器,通過複用一個 Bitmap 對象來達到對每幀的資料的解碼顯示,減少了記憶體占用。

六. 總結與展望

本文介紹了 xMedia 在圖檔記憶體緩存方面多元一體的精細化記憶體管理方案,并重點講解使用 JNI 管理 Native C 層記憶體達到圖檔記憶體緩存目的,突破了 Java Heap 大小限制。

此方案也存在小瑕疵,即在顯示目前圖檔的時候,除了Native 占用了一份解碼後的記憶體,Java 堆記憶體在業務上也同樣占用了一份記憶體,是以需要業務在使用的時候盡量複用 ImageView,使用完後要及時釋放。

随着移動終端智能化和大資料化的發展,後續我們也會針對圖檔記憶體持續進行一些基于大資料的人工智能化管理,相信會帶來更好的技術體驗。

如果你對 mPaaS 多媒體元件感興趣,歡迎你登入

mPaaS 文檔頁

了解更多。

| 移動開發平台 mPaaS 三款元件重磅上線螞蟻金服開放平台:

往期閱讀 《支付寶用戶端架構解析:iOS 容器化架構初探》 《支付寶用戶端架構解析:Android 容器化架構初探》 《支付寶用戶端架構解析:Android 用戶端啟動速度優化之「垃圾回收」》 《支付寶用戶端架構解析:iOS 用戶端啟動性能優化初探》

關注我們微信公衆号「mPaaS」,獲得第一手 mPaaS 技術實踐幹貨

繼續閱讀