mPaaS 3.0 多媒體組件發佈 | 支付寶百億級圖片組件 XMedia 錘鍊之路(圖片緩存篇)

號外:html

對 mPaaS 服務端組件,有任何問題,也能夠移步提問 mPaaS 服務端負責人android

一. 背景介紹

圖片加載一直是 Android App 面臨的「老大難」問題,加載速度與內存消耗天生就是一個矛盾統一體。咱們依託支付寶超級 App 複雜的生態業務場景,借鑑業界領先的開源框架 Fresco、Picasso,取其精華,棄其糟粕,並首創性地使用 Ashmem、Native Mem Cache、Bitmap Reuse、分場景緩存、圖片分大小緩存等多維一體的圖片加載技術,實現了加載速度與內存消耗的完美平衡。算法

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

二. Android 內存基礎與挑戰

Android 系統應用單個進程堆內存分配有限,再加上不一樣 Android 手機硬件性能和系統版本良莠不齊,對於大型App 來講,尤爲是包含圖片加載組件的 App,如何高效合理使用 Android 內存已是一個必不可少的話題。 工欲善其事,必先利其器。想要 App 高效合理地利用內存,還須要先了解下 Android 系統內存相關的一些基礎知識。緩存

1. Android 內存分類

對於手機來講,存儲空間跟計算機設備同樣分爲 ROM 和 RAM。網絡

| ROM (Read Only Memory):架構

名字上解釋爲只讀內存,其實ROM種類也分不少種,有隻讀的,有可讀寫的,主要用於存儲一些數據信息,斷點後數據不會丟失。併發

| RAM (Rondom Access Memory):框架

手機的運行時的物理內存,負責程序的運行以及數據交換,斷電時存儲信息丟失。程序進程的內存空間只是虛擬內存,而程序運行實際須要的是 RAM 實際物理內存,操做系統會將程序申請的進程虛擬內存映射到物理內存 RAM 中。dom

在 Android 應用進程中通常內存可分爲 Heap 堆內存、Code 代碼區、Stack 棧內存、Graphics 顯存、私有非共享內存以及系統內存,其中 Heap 內存又分爲 Davilk Heap 以及 Native Heap。

Android 能夠經過 adb shell dumpsys meminfo+package name 或 pid 命令來查看當前進程內存佔用狀況,如圖 1 所示。

圖1:經過dumsys輸出的內存佔用狀況

內存分類說明以下:

類型 描述
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 的詳細介紹請參考

圖2:Android的進程優先級

圖3:Android的進程級內存回收流程

3. 業界圖片組件

經過上面對 Android 內存分類及回收機制的簡單介紹,對於使用大量圖片的 App 來講,解碼後的圖片,即 Bitmap,佔用大量的內存,勢必更加容易觸發頻繁的 GC。 目前業界幾款比較成熟的開源圖片加載組件有 Facebook 的 Fresco,Google 的 Glide,Square 的 Picasso 等,其圖片緩存均使用了三級緩存技術,即「內存緩存+磁盤緩存+網絡」。加載的優先級從高到低依次爲「內存緩存->磁盤緩存->網絡」。在內存緩存方面,採用的是直接緩存 Bitmap 對象,部分策略大同小異,如圖 4 所示。

圖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+Weakference 結合的方式來直接存儲 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 太高

  1. 大量的圖片內存緩存致使 App 佔用 Java Heap 內存太高,容易頻繁觸發 GC 致使頁面卡頓。
  2. 後臺進程內存太高容易被 kill 掉,保持 App 低內存而不影響體驗很重要。 圖片內存在整個 App
  3. 進程中不能佔用過多,不然容易致使其餘業務或功能內存吃緊而致使功能或體驗影響。

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

  1. 採用 LruCache+Bitmap,超大圖片解碼後佔用內存過大,例如一張 1280*1280 按 ARGB8888 模式解碼出來佔用的內存接近 6M,而低端機上單個進程分配總的 Heap 內存大小才 100M 左右,圖片內存緩存最多隻能幾十兆,存放大圖頂多也就 10 來張,很容易引起圖片內存緩存 LRU 淘汰,影響小圖加載的體驗。

  2. 普通業務的圖片內存緩存在到達緩存上限值時是但願能有效被回收,可是也有特定業務是不但願被頻繁回收,好比頭像內存佔用小但使用頻率較高的業務場景。

  3. 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 所示:

圖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 所示。

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

2. 高速緩存 Heap

此緩存是普通的基於 LRU 淘汰策略的堆內存緩存,總大小爲當前進程的 1/8,最大不超過 64M,存儲的內容爲圖片解碼後的 Bitmap 對象,主要用於解決頭像這種佔用內存不大但使用頻率較高的業務場景。

3.臨時緩存 SoftReference

此緩存主要用於兩種場景:存儲 Gif 相關的對象和超大圖對象,佔用的是 Java Heap 內存,實現原理,經過 SoftReference 保留對 Bitmap 或 Gif 對象的引用,在內存吃緊時,能夠及時 GC,騰出內存。主要爲了減小因單個大內存圖(5M 默認爲大圖)加載會淘汰不少小內存圖的場景,提高用戶圖片體驗。

上面三種內存緩存組合起來的整個圖片內存加載以及存放流程如圖 10 和圖 11 所示:

圖 10.Bitmap 獲取流程 圖 11.Bitmap 存放流程

四. 競品測試對比

測試條件:

基於 Android 4.4 和 6.0 系統上,在同一界面使用不一樣的圖片組件加載 20 張本地圖片。如下爲各圖片組件的內存佔用狀況,結果如圖 8 和圖 9 所示。

圖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 變化較大。

圖10:Fresco 加載圖片先後內存佔用狀況

圖11:xMedia 加載圖片先後內存佔用狀況

2. Android 6.0 系統上

| Java Heap 內存佔用:

由高到低依次爲 Fresco->Picasso->xMedia->Glide。四種圖片組件均佔有 Java Heap,其中 xMedia 並不直接緩存 Bitmap,而是界面UI控件引用了這些 Bitmap,因此致使使用 xMedia 時佔用 Java Heap,可是當退出測試界面並 GC 後總體 Java Heap 便釋放,下次再進入測試頁面則直接從 Native 將對應的圖片數據 copy 到新建立或複用的 Bitmap 中便可顯示;Glide 在退出測試界面後內部會主動釋放掉全部的圖片內存緩存,可是在從新進入測試頁面加載時須要所有從新解碼,緩存的複用率不高。

| Native Heap 內存佔用:

由高到低依次爲 xMedia->(Fresco、Picasso和Glide),其中只有 xMedia 的圖片緩存用到 Native Heap,而其它三個均使用的是 Java Heap。

總的來講,在 5.0 系統如下,xMedia 在J ava 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 個不一樣級別尺寸的圖片,即便超大原圖,咱們也會限制最大邊 12000 的尺寸,而後解碼的時候再採樣處理。
  2. 對於社交會話的縮略模糊圖,直接經過服務端裁剪縮放後由push消息將縮放後的模糊圖片推送到客戶端直接渲染顯示,避免了查看圖片消息時再次網絡請求會後渲染中間出現灰底狀況。
  3. 壓後臺分不一樣階段對圖片內存緩存進行主動清理,保證壓後臺後錢包總體內存處於低位運行,減小後臺進程被kill掉的機率。
  4. 定時清理不經常使用內存緩存,原理是每次使用時更新緩存的使用時間,而後定時去掃描超過必定時間的緩存並主動清理掉。
  5. 支持普通 Listview、ViewPager、RecyclerView 的滑動過程當中中止加載,滑動結束後再加載,減小一些沒必要要的任務開銷。
  6. Gif 圖片使用自研解碼器,經過複用一個 Bitmap 對象來達到對每幀的數據的解碼顯示,減小了內存佔用。

六. 總結與展望

本文介紹了 xMedia 在圖片內存緩存上多維一體的精細化內存管理方案,並重點講解使用 JNI 管理 Native C 層內存達到圖片內存緩存目的,突破了 Java Heap 大小限制。此方案也存在小瑕疵,即在顯示當前圖片的時候,除了Native 佔用了一份解碼後的內存,Java 堆內存在業務上也一樣佔用了一分內存,所以須要業務在使用的時候儘可能複用 ImageView,使用完後要及時釋放。隨着移動終端智能化和大數據化的發展,後續若是能對圖片內存作一些基於大數據的人工智能化管理,相信會帶來更好的技術體驗。

若是你對 mPaaS 多媒體組件感興趣,歡迎你登陸mPaaS 文檔頁瞭解更多。

往期閱讀

《開篇 | 螞蟻金服 mPaaS 服務端核心組件體系概述》

《螞蟻金服 mPaaS 服務端核心組件體系概述:移動 API 網關 MGS》

《螞蟻金服 mPaaS 服務端核心組件:億級併發下的移動端到端網絡接入架構解析》

《支付寶 App 構建優化解析:經過安裝包重排布優化 Android 端啓動性能》

《支付寶 App 構建優化解析:Android 包大小極致壓縮》

關注咱們公衆號,得到第一手 mPaaS 技術實踐乾貨

QRCode

釘釘羣:經過釘釘搜索羣號「23124039」

期待你的加入~

相關文章
相關標籤/搜索