精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

抖音 Android 性能優化系列:Java OOM 優化之 NativeBitmap 方案

原創 精選
移動開發 移動應用 Android
對于 Java 內存泄漏治理,業界已經有比較成熟的方案,這里不做介紹,本文主要針對第二點嘗試進行分析和優化。

作者 | 抖音基礎技術團隊

一、背景和目標

背景

作為 Android 開發者,相信大家都碰到過 Java OOM 問題,導致 OOM 的原因可能是應用存在內存泄漏,也可能是因為手機的 heapsize 比較小不能滿足復雜應用對內存資源的大量需求。對于 Java 內存泄漏治理,業界已經有比較成熟的方案,這里不做介紹,本文主要針對第二點嘗試進行分析和優化。

舉個例子:我們在監控平臺查看穩定性數據,發現 heapsize=256M 的設備發生的 OOM 崩潰最多,而 heapsize=512M 的設備很少發生 OOM 崩潰。且除此之外,還有一個特點:OOM 崩潰絕大多數發生在 Android 8.0 之前的設備。

對于這種 heapsize 較小難以滿足業務復雜度的情況,可能有以下幾種方式來解決:

1. 增加 heapsize

如果我們已經設置了 largeHeap,也就沒有常規的提升 heapsize 的方式了;再想往前一步,可以嘗試從虛擬機中突破這個限制,因為 heapsize 是虛擬機的配置,是否拋出 OOM 異常也是在虛擬機中決定的;修改虛擬機運行邏輯是有一定可能的,但是其難度和可行性與想要修改的內容相關性較大,修改方案的穩定性也需要非常深厚的功力才能保證,而如果運氣不好,找不到好的切入點,甚至從理論上都無法保證其穩定性,那么達到上線的難度就更大了,本文不在這個方向深入。

2. 降低業務復雜度,裁剪應用功能

這個方案也不在我們的考慮范圍之內,實際上很多應用都有推出極速版,但是功能都會有所裁剪,對于使用常規版本的用戶,我們也不能推送極速版,因為使用體驗會有很大變化。

3. 分析 Java Heap 里的內容都是什么,嘗試發現主要矛盾進行優化,對癥下藥

實際上本文就是從這個方向經過調查后,找到了一個相對穩定的突破口。下面是結合 OOM 堆棧、android 版本、heapsize 維度對 OOM 整體概況的一個分析:

最常見 OOM 堆棧

出現最多的堆棧就是 Bitmap 創建時內存不足從而 OOM 崩潰,那么是不是已使用的內存大多都是 Bitmap 呢 ?不能 100%確定,因為直接觸發 OOM 崩潰的原因是最后一次內存分配失敗,而真正的原因是 OOM 之前的內存分配;但是仍然有一定可能性,因為總是出現同一個堆棧可能并不是巧合,可以在一定程度上說明這個堆棧執行的比較頻繁,而且 Bitmap 一般占用內存較大。

這里先做一個不 100%確認的初步推斷:OOM 時 Java heap 中占用內存較多的對象是 Bitmap。

OOM 在不同 android 版本、heapsize 上的表現

繼續對 OOM 數據做總結后發現了 OOM 的分布規律如下圖:

上圖紅色地雷代表 OOM,橫坐標是 android 版本,縱坐標是 heapsize,原點是:(android8.0, 384M);可以看到:

  • 第一、四象限,OOM 最少;對應 android 高版本,大 heapsize 和小 heapsize 都有
  • 第二象限有一定 OOM;對應 android 低版本,大 heapsize
  • 第三象限 OOM 最多;對應 android 低版本,小 heapsize

簡單總結就是:

  • heapsize 越大越不容易 OOM
  • Android8.0 及之后的版本更不容易 OOM

第四象限的數據說明,即便在 heapsize 較小的情況下,在 android 8.0 之后的版本上也不容易發生 OOM,結合上面的初步推斷信息“OOM 時 Java heap 中占用內存較多的對象是 Bitmap”,很容易想到,應該是 Bitmap 在 android 8.0 前后的實現變化導致了當前的 OOM 分布現象:

Bitmap 變化:

  • 在 Android 8.0 之前,Bitmap 像素占用的內存是在 Java heap 中分配的
  • Android 8.0 及之后,Bitmap 像素占用的內存分配到了 Native Heap

由于 Native heap 的內存分配上限很大,32 位應用的可用內存在 3~4G,64 位上更大,虛擬內存幾乎很難耗盡,所以在前面的推測 “OOM 時 Java heap 中占用內存較多的對象是 Bitmap” 成立的情況下,應用更不容易 OOM。

而第三象限數據,則進一步佐證了前面的推測,Android 8.0 之前,Bitmap 像素內存在 Java heap 中分配時,即便 heap size 大到 512M,OOM 發生也比較多。

至此,得到了確定的結論:

  • OOM 的分布主要在 Android 8.0 之前 heap size 較小的設備
  • OOM 時 Java heap 中占用內存較多的是 Bitmap(確切的說是 Bitmap 的像素數據),當 Bitmap 像素占用內存在 Native Heap 分配時,即便 heap size 很小,應用也不容易 OOM

目標

根據上述結論,目標也就比較清晰了:

  • 使 Android 8.0 之前 Bitmap 的像素內存也從 Native 層分配,從而減少 Java OOM 崩潰。

二、Bitmap 使用分析和方案調查

想要使得 Android 8.0 之前的設備 Bitmap 像素內存也分配在 Native heap,需要先把 Bitmap 的創建流程調查清楚。

Bitmap 創建流程

如下堆棧描述了 Bitmap 的創建:

Bitmap 的構造方法是不公開的,在使用 Bitmap 的時候,一般都是通過 Bitmap、BitmapFactory 提供的靜態方法來創建 Bitmap 實例。下圖中以 Bitmap.createBitmap 說明了 Bitmap 對象的主要創建過程:

從上圖可以看到 Java Bitmap 對象是在 Native 層通過 NewObject 創建的。圖中的兩個函數:

  • allocateJavaPixelRef,是 8.0 之前版本為 Bitmap 像素從 Java heap 申請內存
  • allocateHeapBitmap,是 8.0 版本為 Bitmap 像素從 Native heap 申請內存

allocateJavaPixelRef 函數的實現

allocateJavaPixelRef 通過 newNonMovableArray 從 Java 堆上為 Bitmap 像素分配內存,然后再構造 Native Bitmap 對象,對應的構造函數如下:

構造函數中發現 Native Bitmap 構造時對應的 mPixelStorageType 是 PixelStorageType::Java,表示 Bitmap 的像素是保存在 Java 堆上,所以嘗試看下 PixelStorageType 總共有幾種,是否可能有把 pixels 數據存儲到 Native 層。查找代碼發現 PixelStorageType 只有三類,如下:

這個信息可以作為一個切入點,在后面進行深入調查。

allocateHeapBitmap 實現

allocateHeapBitmap 主要是通過 calloc 為 Bitmap 的像素分配內存,這個分配就在 Native 堆上了。

通過初步的分析,初步有兩個思路可以先進行嘗試:

  • 在創建 Bitmap 時,把對 allocateJavaPixelRef 的調用替換為調用 allocateHeapBitmap 來達到從 Native 層分配內存的目的
  • 調查 PixelStorageType 共有哪些種類,是否可能從當前的保存到 Java 堆切換為保存到 Native 堆

思路 1:allocateJavaPixelRef 替換為 allocateHeapBitmap

這個思路看起來想要實現目標,做一下替換就可以了,但實際上沒有這么簡單,存在的問題如下:

  • allocateHeapBitmap 返回的是 skBitmap,allocateJavaPixelRef 返回的是 android::Bitmap,類型并不匹配
  • 并不是簡單的插拔就可以把 allocateJavaPixelRef 替換為 allocateHeapBitmap,8.0 之前的 Android 版本上沒有 allocateHeapBitmap 的實現。如果想要為 8.0 之前的系統寫一個全新的實現,只是參數的獲取就需要做很多適配,比如無法直接使用 skia 中的 SkBitmap、SkColorTab、SkImageInfo,就沒有辦法動態獲取到要分配的內存 size
  • Bitmap 內存的申請和釋放要有匹配的邏輯和合適的時機

所以這個思路基本可以斷定不可行。

思路 2:allocateJavaPixelRef 替換為 allocateAshmemPixelRef

前面的調查發現 PixelStorageType 只有三類,如下:

其中 External 方式存儲 Bitmap 像素,在源碼中沒有看到相關使用,無法參考;Java 類型就是默認的 Bitmap 創建方式,像素內存分配的 Java 堆上;Ashmem 方式存儲 Bitmap 像素的方式在源碼中有使用,主要是在跨進程 Bitmap 傳遞時使用,對應的場景主要是 Notification 和截圖場景:

查看其實現:

從代碼中看到 allocateAshmemPixelRef 這個函數是通過 mmap ashmem 內存來創建 native Bitmap 對象,且參數、返回值都與 allocateJavaPixelRef 相同,所以使用 Ashmem 方式存儲 Bitmap 像素看起來有一定可行性,只需把 allocateJavaPixelRef 的調用替換為 allocateAshmemPixelRef 即可達到從 Native 層為 Bitmap 像素分配內存的目的。

但經過詳細的源碼分析以及實際驗證,其可行性仍然很低,主要原因如下:

  • allocateAshmemPixelRef 實現只在 android 6.0 ~ android7.1 上存在,所以這個方案即便能夠實現,也只能覆蓋 android 6.0 ~ android 7.1

實際情況中,6.0 系統的 OOM 占了非常大一部分,如果這個方案可行,也可以解決一部分問題,所以不會因為這個原因阻礙對這種方案的嘗試,還可以繼續嘗試

  • ashmem 方式存儲 Bitmap 像素,每個 Bitmap 需要對應一個 fd,應用的 Bitmap 使用數量是能夠達到 1000+ 的,這樣可能會導致 fd 資源使用耗盡,從而發生崩潰

這個問題基本是無解的,但如果方案可行,可以嘗試只給一定數量的 Bitmap 使用 ashmem 方式申請像素內存,比如 500 個;所以方案還可以繼續嘗試

  • 最終嘗試后發現這種方式影響 Bitmap 正常功能(一些視頻動圖不能正常展示),經分析主要原因是使用 ashmem 申請的 Bitmap 無法進行 reconfigure :

上圖 Bitmap 的 reconfigure 代碼中可以看到沒有 mBuffer 的 Bitmap 不支持 reconfigure,Ashmem 方式創建的 Bitmap 沒有從 Java 堆申請 mBuffer,所以一定是不支持 reconfigure 的。當然到這里之后還沒有完全堵死這個方式,還可以繼續嘗試在 ashmem 方式申請 Bitmap 時給其一個假的 mBuffer 來繞過這個限制,但接下來要做的調查和改動勢必很大,因為 ashmem 方式申請 Bitmap 本身不支持 mBuffer 的管理,新創建的 buffer 就難以找到合適的時機進行釋放。

結合上述 3 個點綜合判斷,這個方案限制比較多,也有一定風險,所以暫時將當前的方案暫時掛起,作為備用方案。

上述的兩種思路不成功其實有一定的必然性,畢竟對應代碼的設計并不是為了給我們取巧做切換用的。既然沒有辦法這么容易實現,就深入調查清楚為 Bitmap 從 Java 堆申請內存的流程和這個內存的使用流程,再嘗試從這些流程中找到切入點進行修改。

思路 3:剖析 Java 堆分配 Bitmap 內存的過程,再嘗試找到方案

Bitmap 內存申請

調查思路:

實際就是查找 hook 點的思路,先分析內存是如何分配的,分配出來的內存是如何使用的(主要指分配出內存后,指針或者對象的傳遞路徑),嘗試把從 Java 堆分配內存的關鍵點替換為使用 malloc/calloc 函數從 Native 堆上進行分配,并把分配出來的內存指針構造成原流程中使用的數據結構,并保證其能夠正常運行。

Android 8.0 之前 Bitmap 內存申請和使用如下圖:

上圖為簡化后的核心內存分配流程,框起來的部分就是為 Bitmap 從 Java heap 申請像素內存的代碼。其中:

  • arrayObj 是通過 newNonMovableArray 從 java heap 分配出來的 byte array 對象
  • addr 是 arrayObj 對象存放 byte 元素的首地址

這里需要先說明一下 java byte array 的內存布局(對應代碼在 ART 虛擬機中):

前面的 8 個字節是 Object 成員,length_ 是這個數組的長度,first_element_ 數組用來實際存放 byte 數據,數組的長度由 length_/4 來決定。addressOf(arrayObj) 獲取到的就是 first_element_地址;arrayObj 和 addr 的傳遞在上圖已經用分別用綠色和紅色虛線箭頭標記出來了。

想要把 Bitmap 內存分配改為在 Native 層分配,就需要從分配這里入手, 所以必須要把 arrayObj 和 addr 使用梳理清晰,為后續替換和適配做好鋪墊。arrayObj 和 addr 使用如下:

arrayObj 的使用

1. 在 Native 層使用,即在 android::Bitmap 對象中使用

在創建 Bitmap 時,把 arrayObj 添加到 weak global ref tab 中,并通過 Bitmap 的 mPixelStorage.jweakRef 引用 arrayObj:

在 Bitmap 的 pinPixelsLocked 中,把 arrayObj 添加到 global ref tab 中,并保存在 Bitmap 的 mPixelStorage.java.jstrongRef 中:

在 Bitmap 的 unpinPixelsLocked 中,從 global ref tab 中刪除對 arrayObj 的引用:

在 Bitmap 的 doFreePixel 中(即釋放像素內存),刪除 arrayObj 對應的 weak ref:

通過 Bitmap 的成員函數 javaByteArray() 向外部提供引用,即 mPixelStorage.jstrongRef (只在創建 Java Bitmap 對象時傳遞為參數,賦值給 Bitmap 的 mBuffer 成員進行使用)

2. 在 Java Bitmap 對象中引用,對應 Bitmap 的 mBuffer 成員

在創建 Java Bitmap 時通過 nativeBitmap->javaByteArray()獲取對 arrayObj 的引用,并賦值給 Java Bitmap 的 成員:private byte[] mBuffer;

在 Bitmap.reconfigure 中,需要使用 arrayObj.length,在 Native 層會使用這個 length 判斷當前的 Bitmap 能否滿足 reconfigure 需求:

在 Bitmap.getAllocationByteCount()中通過 arrayObj.length 獲取這個 Bitmap 的像素內存大小:

小結:arrayObj 對象的引用只在 Bitmap native 對象和 Java 對象中,作用分別是用來管理 arrayObj 的生命周期以及使用它的 length 來獲取 Bitmap 像素占用的內存大小。

addr 的使用

在為 Bitmap 分配 nonMovableArray 之后,通過 addr = addressOf(arrayObj)獲取:

在創建 native bitmap 時,作為指針傳遞給其成員 mPixelRef:

上述參數 mStorage 就是 addr,其關鍵使用點是在 WrappedPixelRef 的 onNewLockPixels 被調用時,賦值給 LockRec 的 fPixels 成員:

mPixelRef 會被設置給 skBitmap。

每個 nativeBitmap 對應一個 skia 的 skBitmap 對象,在創建 Bitmap 時會把 native bitmap 的成員 mPixelRef 設置給 skBitmap:

在 skia 中 SkBitmap 繪制 Bitmap 需要使用內存來處理 Bitmap 像素數據時,就會通過 mPixelRef->onNewLockPixels() 來獲取存放 Bitmap 像素的內存地址,即 arrayObj 的元素地址 addr,其是作為指針類型數據來使用的。

小結:addr 指向的內存是在 java 堆上,其會在需要的時候被傳遞給 skia 用來處理 bitmap 像素數據。

Bitmap 內存使用總結:

  • 存儲 Bitmap 像素數據使用的內存是通過 NewNonMovableArray 從 Java heap 申請的 byte 數組 arrayObj,arrayObj 對象的引用只在 Bitmap native 對象和 Java 對象中,作用分別是用來管理 arrayObj 的生命周期以及使用它的 length 來獲取 Bitmap 像素占用的內存大小。
  • skia 中并不會為 Bitmap 的像素數據分配內存,它把 Java heap 上 byte 數組的元素首地址轉換為 void* 來使用;也就是說在當前實現中,Bitmap 像素內存不一定非得是在 Java heap 上分配,我們可以 malloc 一塊內存傳遞給 skia 使用,并不需要再給 skia 做任何適配。

有了上面這些信息,把 android 8.0 之前的 Bitmap 像素內存改到在 Native 層分配目標就看到了希望,因為不需要在 skia 層適配,可以降低一定難度。

嘗試從 native 層申請 Bitmap 內存

根據上面的分析,只需要找好 hook 的切入點,并完成 3 個關鍵點的替換即可,如下圖:

  • 目標是不再從 java heap 給 Bitmap 分配內存,這一步的 byte[] 申請必然是需要去掉的
  • 這里通過 malloc 分配內存,交給 PixelRef 引用,間接的就可以被 SkBitmap 使用了
  • 原有實現中 Java Bitmap 通過 mBuffer 成員引用 byte[],主要用來通過 mBuffer.length 獲取圖片大小

上述 3 個關鍵點中,前兩個點比較好實現,都是 native 層的代碼,hook 點也比較好找,這里不再贅述。而第 3 個點需要特殊處理,因為 Java 層 Bitmap 通過 mBuffer.length 獲取 Bitmap size,目前沒有穩定的 Java hook 方案,且我們又不能真的給它一個長度為 Bitmap size 大小的 byte[](那樣就又從 Java 堆上進行 Bitmap 的內存分配了),所以只能給個假的。

那么如何構造一個假的 byte array ?前面分析過 java byte array 的內存布局:

實際上 array.length 的就是 array 對象的 length_ 值,而虛擬機又提供了 addressOf 來獲取一個 array 的首元素地址,也即 first_element_ 地址,所以可以嘗試通過 first_element_ 來定位 length_ 的位置,進行修改即可。

這樣就可以在 java heap 上申請一個比較小的 byte array,并把它的長度偽造成與 Bitmap size 相等。申請的這個小 size 的 byte array 本身占用的內存就作為 Bitmap 內存轉移到 Native 層的代價。

這種方式看起來好像不太穩定,但是可以通過校驗來保證,比如我們在執行方案之前先嘗試偽造一個 byte array 來進行驗證,如下代碼就是申請了 1 字節長度的 byte array,把它的長度偽造成 36,然后進行校驗,校驗失敗則不再執行 NativeBitmap 方案。

至此,Bitmap 內存申請從 Java heap 轉移到 native heap 所需要解決的關鍵問題都解決了,離最終的目標還有 50% 的距離。接下來需要完成 malloc 出來的 Bitmap 內存的釋放邏輯。

Bitmap 內存釋放

原生釋放邏輯

原生 Bitmap 的像素內存存放在 byte array (mBuffer)中,Bitmap 的內存釋放流程就對應于 mBuffer 對象的釋放,這個釋放流程在 android 5.x ~7.x 大體相同,只有細微差別,下述以 android 6.0 代碼為例進行說明。Bitmap 像素內存釋放主要有兩種方式觸發:一種是 Java Bitmap 對象不再被引用后,GC 回收 Java Bitmap 對象時析構 Native Bitmap ,從而釋放 Bitmap 像素內存;一種是主動調用 Bitmap.recycle() 來觸發 Bitmap 像素內存的釋放:

這個 mBuffer 是在 Native 層申請的 Java 對象,主要在兩個地方引用:

  • Native 層通過 NewWeakGlobalRef(arrayObj) 把它添加到 Weak Global Reference table 中進行引用
  • Java 層 Bitmap 通過 mBuffer 來引用,實際是在 Native 層通過 NewGlobalRef(arrayObj) 把它添加到了 Global Ref table 中,即 mBuffer 是一個關聯到 Java byte array 的 Global ref

而這兩個引用的釋放順序是先通過 DeleteGlobalRef 刪除全局強引用(Skia 中不再使用這個 Bitmap 時會觸發強引用刪除),再通過 DeleteWeakGlobalRef 來刪除全局弱引用,最終這個 byte array 對象被 GC 回收。

但實際運行過程中不完全是這樣的順序,mBuffer 的回收必然是在 DeleteGlobalRef 之后,但卻不一定是在 DeleteWeakGlobalRef 之后,因為一旦 bytearray 只被 Weak glabal ref table 引用時,只要發生 GC,就會把它回收掉。

新的釋放邏輯

原生的 Bitmap 像素內存釋放是通過回收 mBuffer 引用的 byte array,而 NativeBitmap 方案將像素內存轉移到 Native 內存之后,存在兩份內存需要被釋放:

  • 給 Java Bitmap 使用的小 size 的 byte array 對象,這個對象仍然按照原生邏輯釋放,無需再做其他變動
  • malloc 出來的用以存放 bitmap 像素數據的內存,在 byte array 釋放時進行 free,相當于附著于原生的內存釋放邏輯,從而不會影響 Bitmap 的生命周期

實現釋放有兩個關鍵點:

1、malloc 出來的指針需要與 mBuffer 關聯,這樣才能在 mBuffer 釋放時找到對應的內存進行釋放

解決方式:由于此時的 mBuffer 是偽造的 byte array,可以把 malloc 出來的 bitmap 指針保存在 byte array 中,當 byte array 被釋放時,先從中取出 bitmap 指針進行 free,再進行 byte array 釋放即可

2、需要使 mBuffer 的釋放邏輯固定,這樣便于確認 hook 點,原生的 mBuffer 釋放邏輯是在 DeleteGlobalRef 之后的首次 GC 時,比較難以操作

解決方式:給 mBuffer 額外添加一個引用,放到 Global Reference Table 中,保證 mBuffer 不被提前釋放,從而保證 mBuffer 的釋放時機穩定保持在 Bitmap::doFreePixels() 中的 DeleteWeakGlobalRef(mBuffer) 位置,在這里從 mBuffer 中取出 malloc 出的 bitmap 指針執行 free,然后再依次刪除給 mBuffer 額外添加的 Global Reference 和 Weak global ref。

新的釋放邏輯與原生釋放邏輯變化不大,如下圖,主要是固定了 mBuffer 的釋放時機在 DeleteWeakGlobalRef(mBuffer) 時,以及在此時釋放 malloc 出來的 bitmap 內存:

至此,malloc 出來的內存也能夠找到合適的時機進行釋放,把 Bitmap 的像素內存從 Java heap 轉移到 Native heap 上的方案理論上完全可以實現,且需要的改動不大,只需要在原生 Bitmap 的創建流程和釋放流程中做好修改即可。

三、實現方案

根據上述思路 3 的方案,最終實現如下:

Bitmap 創建改造

改造前 Bitmap 的創建和內存申請流程:

改造后 Bitmap 的創建和內存申請流程:

改造后在 Bitmap 創建過程中做了兩個 hook,對應上圖中兩條紫色箭頭指向的代碼:

1. hook newNonMovableArray 函數

當為一個 Bitmap 在 java 堆上通過 newNonMovableArray 申請一個 bitmapSize 大小的 byte array 時,通過代理改造,實際只申請大小為 (sizeof(int) + sizeof(jobject) + sizeof(void*)) 的 byte array(32 位上大小為 12 字節,64 位上為 16 字節)。

修改這個 byte array 的 size 為 bitmapSize,以供 Java 層 Bitmap 使用它獲取 bitmap 的真實 size。

在 byte array 的 element 首地址開始的前 4 個字節保存 0x13572468 作為 magic number,用以判斷這是一個改造之后的 byte array。

通過 NewGlobalRef(fakeArrayObj) 把這個 byte array 對象添加到 Global Ref table 中,以保證 byte array 的釋放時機一定是在 DeleteWeakGlobalRef 之后,并保存到 byte array 中,以便后續釋放時使用;實際創建的 array 內存布局如下,這個 array 稱為 fakeArray。

這個 array 的實際 length 是 12 字節(32 位),此時 1~4 字節存放 magic:0x13572468,5~8 字節存放 globalRef,9~12 字節暫時沒有存放數據

2. hook addressOf 函數

在 addressOf 的代理函數中根據前 4 個字節數據是否是 magic number 來判斷傳入進來的 array 是否是被改造的 array,如果不是則調用原函數進行返回,如果是則繼續進行下述步驟;

  • 從 array 中獲取 bitmapSize,并通過 calloc(bitmapSize,1) 在 Native 堆上為 Bitmap 分配內存;
  • 把分配出來的 bitmap 指針保存到 fakeArray 的 9-12 字節中;
  • 把 bitmap 指針返回,由原生邏輯在后續傳遞給 skia 使用;

此時 fake array 中存放數據如下:

在后面釋放 Bitmap 相關內存時會使用到 byte array 中填充的這些數據。

在前面提到過申請的 fakeArray 本身占用的內存就作為 Bitmap 內存轉移到 Native 層的代價,到這里及可以計算一出 Bitmap 被轉移到 Native 層需要付出的內存代價是多少 ?

答案是:在 32 位上是 12 字節,在 64 位上是 16 字節,多使用的內存就是 fakeArray 中 0x13572468,globalRef,bitmap 這三個數據占用的內存。一個進程如果使用 1000 個 Bitmap,最多額外占用 16* 1000 = 15KB+,是能夠被接受的。

Bitmap 釋放改造

前述 Bitmap 創建過程的改造已經保證了 Bitmap 成員 mBuffer 的釋放一定是在 Bitmap::doFreePixels() 的 DeleteWeakGlobalRef 之后了,所以只需要按照之前思路 hook DeleteWeakGlobalRef 函數即可:

上圖中虛線上方為原生的釋放流程,虛線下方是在原生流程上新添加的釋放流程。其中右側的代碼就是新的邏輯下對 Bitmap 像素數據和輔助數據釋放的關鍵代碼。釋放邏輯已經在第二大節中的 [新的釋放邏輯] 中說明,這里不再復述。

上述對 Bitmap 創建和釋放流程的改造即可實現從 Native heap 給 Bitmap 申請像素內存,但這樣的改造必然會影響原有的 java heap GC 的發生,因為 Bitmap 使用的像素內存被轉移到了 Native 層,Java heap 內存的壓力會變小,但 Native heap 內存的壓力會變大,需要有對應的 GC 觸發邏輯來回收 Java Bitmap 對象,從而回收其對應的 Native 層像素內存。

這種情況可以通過在 native 內存申請和釋放時通知到虛擬機,由虛擬機來判斷是否達到 GC 條件,來進行 GC 的觸發。實際上 android 8.0 之后 Bitmap 內存申請和釋放就是使用的這個方式。

對應的代碼在 VMRuntime 中實現:

只需要在給 Bitmap 申請內存時調用 registerNativeAllocation(bitmapSize),在釋放 Bitmap 內存時調用 registerNativeFree(bitmapSize)即可。

兼容性:android 5.1.x ~ 7.x

目前該方案支持到 android 5.1.x ~ 7.x 的系統。4.x~5.0 的系統較早,實現差異較大,待后續完善。

四、線下驗證和線上效果

線下驗證

使用一臺 android 6.0 的手機機型驗證,java heapsize 是 128M。

測試代碼

在測試代碼中嘗試把一個 bitmap 緩存 5001 次:

private static ArrayList<Bitmap> sBitmapCache = new ArrayList<>();
void testNativeBitmap(Context context) {
NativeBitmap.enable(context);
for (int i = 0; i <= 5000; i++) {
Bitmap bt = BitmapFactory.decodeResource(context.getResources(),R.drawable.icon);
if (i%100 == 0) {
Log.e("hanli", "loadbitmaps: " + i);
}
sBitmapCache.add(bt);
}
}

原生流程,只能加載 1400+個 Bitmap

在不開啟 NativeBitmap 時,load 1400+ 張圖片后,應用的 Java 堆內存耗盡,發生 OOM 崩潰:

17979 18016 E hanli: loadbitmaps: 0
17979 18016 E hanli: loadbitmaps: 100
...
17979 18016 E hanli: loadbitmaps: 1300
17979 18016 E hanli: loadbitmaps: 1400
17979 18016 I art : Alloc concurrent mark sweep GC freed 7(208B) AllocSpace objects, 0(0B) LOS objects, 0% free, 127MB/128MB, paused 280us total 15.421ms
17979 18016 W art : Throwing OutOfMemoryError "Failed to allocate a 82956 byte allocation with 7560 free bytes and 7KB until OOM"

打開 NativeBtimap

完成加載 5001 個 Bitmap,并且應用仍能夠正常使用:

17516 17553 D hanli: NativeBitmap enabled.
17516 17553 E hanli: loadbitmaps: 0
17516 17553 E hanli: loadbitmaps: 100
...
17516 17553 E hanli: loadbitmaps: 4800
17516 17553 E hanli: loadbitmaps: 4900
17516 17553 E hanli: loadbitmaps: 5000

線上效果:發生 Java OOM 的用戶數量降低 50%+

產品 1

針對 heapsize 為 256M 及以下的設備啟用,當 Java heap 使用率達到 heapsize 的 70% 之后開始打開 NativeBitmap,Java OOM 崩潰影響用戶數-56.4785%,OOM 次數降低 72%。

產品 2

針對 heapsize 為 384M 及以下的設備啟用,當 Java heap 使用率達到 heapsize 的 80% 之后開始打開 NativeBitmap,Java OOM 崩潰影響用戶數降低 63.063%,OOM 次數降低 76%。

在使用中我們對 NativeBitmap 方案的使用做了限制,因為 Bitmap 內存轉移到 Native 層之后會占用虛擬內存,而 32 位設備的虛擬內存可用上限為 3G~4G,為了減少對虛擬內存的使用,只在 heap size 較小的機型才開啟 NativeBitmap。我們在持續的優化中發現 Android 5.1.x ~ 7.1.x 版本上,已經有很多設備是 64 位的,所以當用戶安裝了 64 位的產品時,就可以在 heap size 較大的機型上也開啟 NativeBitmap,因為此時的虛擬內存基本無法耗盡。在 64 位產品上把開啟 NativeBitmap 的 heap size 限制提升到 512M 之后,Java OOM 數據在優化的基礎上又降低了 72%。

五、兩點說明

有兩個問題做一下說明:

是否使用了 NativeBitmap 就一定不會發生 Java OOM 了?

答:并不是,NativeBitmap 只是把應用內存使用的大頭(即 Bitmap 的像素占用的內存)轉移到 Native 堆,如果其他的 Java 對象使用不合理占用較多內存,仍然會發生 Java OOM

方案可能產生的影響?

Bitmap 的像素占用的內存轉移到 Native 堆之后,會使得虛擬內存使用增多,當存在泄漏時,可能會導致 32 位應用的虛擬內存被耗盡(實際上這個表現和 Android8.0 之后系統的表現一致)。

所以,方案的目標實際是為了使老的 android 版本能夠支持更復雜的應用設計,而不是為了解決內存泄漏。

責任編輯:未麗燕 來源: 字節跳動技術團隊
相關推薦

2022-07-19 16:47:53

Android抖音

2022-03-29 13:27:22

Android優化APP

2024-06-13 17:10:16

2022-06-06 12:19:08

抖音功耗優化Android 應用

2019-07-25 13:22:43

AndroidAPK文件優化

2021-07-29 14:20:34

網絡優化移動互聯網數據存儲

2019-12-13 10:25:08

Android性能優化啟動優化

2025-07-30 09:36:47

2013-02-20 14:32:37

Android開發性能

2013-09-17 10:32:08

Android性能優化數據庫

2022-06-01 09:18:37

抖音ReDex算法優化

2013-12-17 16:21:17

iOSiOS性能優化

2017-01-15 15:13:37

Android性能優化優化點

2015-09-16 14:37:50

Android性能優化運算

2015-09-16 13:54:30

Android性能優化渲染

2015-09-16 15:48:55

Android性能優化電量

2023-11-03 17:02:18

抖音直播畫質優化

2021-11-29 11:13:45

服務器網絡性能

2022-02-16 14:10:51

服務器性能優化Linux

2018-01-09 16:56:32

數據庫OracleSQL優化
點贊
收藏

51CTO技術棧公眾號

国产欧美日韩精品一区二区免费| 成入视频在线观看| 裸体一区二区三区| 美女av一区二区| 精品伦一区二区三区| 国产色播av在线| 国产精品美女久久久久久2018| 成人av在线天堂| 日本一区二区网站| 欧美电影三区| 日韩精品在线观看一区二区| 欧美日韩中文不卡| 国产传媒av在线| 国产精品传媒在线| 国产精品麻豆免费版| 成人av网站在线播放| 欧美~级网站不卡| 亚洲热线99精品视频| 手机看片国产精品| 成人在线观看免费播放| 激情懂色av一区av二区av| 一级做a爰片久久| 五月婷婷六月色| 美女在线观看视频一区二区| 97碰在线观看| 国产又黄又爽又无遮挡| 精品国产精品国产偷麻豆| 日韩女优视频免费观看| 在线看免费毛片| 欧美大片1688| 午夜精品免费在线| 欧美日韩视频免费| 免费黄网在线观看| 国产欧美日韩麻豆91| 国产精品成人观看视频免费| 国产又粗又长视频| 理论片日本一区| 国产成人精品免费视频| 日韩欧美亚洲一区二区三区| 欧美激情性爽国产精品17p| 国产一区二区av| 一级做a爰片毛片| 精品五月天堂| 亚洲成人av中文字幕| 在线观看一区二区三区视频| 中文字幕综合| 欧美狂野另类xxxxoooo| 日本中文字幕精品—区二区| 小明成人免费视频一区| 在线观看亚洲精品| 国产精品乱码久久久久| 成人香蕉视频| 一本到高清视频免费精品| 波多野结衣乳巨码无在线| 97人澡人人添人人爽欧美| 亚洲第一狼人社区| 大j8黑人w巨大888a片| √天堂8资源中文在线| 亚洲成人自拍网| 男人天堂手机在线视频| 爱搞国产精品| 色哟哟一区二区在线观看| 久久久久久久久久福利| 成人精品三级| 欧美一区二区成人| 香蕉视频1024| 日韩电影不卡一区| 亚洲一区二区精品| 中国一级片在线观看| 欧美一区在线看| 久久久亚洲成人| 欧美bbbbbbbbbbbb精品| 肉肉av福利一精品导航| 国产日韩欧美在线看| 精品国产免费无码久久久| 国产99精品国产| 狠狠色综合色区| 国产精品99999| 亚洲日本一区二区| 和岳每晚弄的高潮嗷嗷叫视频| 中老年在线免费视频| 欧美性视频一区二区三区| 看看黄色一级片| 精品按摩偷拍| 一区二区欧美久久| 美女的奶胸大爽爽大片| 1024日韩| 国产精品色悠悠| 亚洲乱码在线观看| 久久精品一二三| 亚洲小说欧美另类激情| 欧美少妇精品| 欧美猛男男办公室激情| 午夜免费福利影院| 北条麻妃国产九九九精品小说| 久久久精品久久久| 国产精品一区二区三区四| 久久精品国内一区二区三区| 国产精品区免费视频| 国产高清在线| 亚洲国产日韩精品| 国产wwwxx| 91精品国产自产精品男人的天堂| 亚洲女人被黑人巨大进入| 波多野结衣喷潮| 国产日韩高清一区二区三区在线| 国产在线高清精品| 五月激情婷婷网| 最新日韩在线视频| 日韩 欧美 高清| 成人激情自拍| 久久精品久久久久电影| 国产一级淫片a视频免费观看| 狠狠v欧美v日韩v亚洲ⅴ| 久久一区二区三区欧美亚洲| 97caopron在线视频| 欧美私人免费视频| 久久久久国产精品区片区无码| 日韩成人免费| 欧洲亚洲在线视频| 人妻va精品va欧美va| 成人欧美一区二区三区| 北条麻妃av高潮尖叫在线观看| 91久久精品无嫩草影院| 色av中文字幕一区| 欧美一级黄视频| 久久综合九色综合欧美就去吻| av日韩在线看| 国产精品一区二区美女视频免费看| 在线观看视频亚洲| 亚洲AV无码成人精品区东京热| 成人深夜视频在线观看| 看全色黄大色大片| 亚洲一区二区小说| 久久精品视频在线观看| 在线免费观看高清视频| 久久久蜜桃精品| 日日橹狠狠爱欧美超碰| 国产精品久av福利在线观看| 免费97视频在线精品国自产拍| 91精品国自产| 亚洲欧美中日韩| jizz18女人| 99热在线成人| 成人久久久久久久| 二区在线播放| 欧美一级免费大片| 久草免费在线观看视频| 国产**成人网毛片九色 | 久久精品高清| 国产在线视频2019最新视频| 天堂а√在线官网| 欧美综合一区二区| 懂色av蜜臀av粉嫩av永久| 久久激情久久| 亚洲精品中文字幕乱码三区不卡| 国产综合色在线观看| 中文字幕亚洲欧美在线| 国产又粗又黄又爽视频| 亚洲视频资源在线| 国产精品99精品无码视亚| 亚洲性视频h| 精品综合在线| 岛国精品在线| 欧美精品中文字幕一区| 国产91绿帽单男绿奴| 欧美色xxxx| 成人在线观看免费高清| 激情都市一区二区| 免费在线黄网站| 牛牛视频精品一区二区不卡| 欧美主播福利视频| 色网站免费在线观看| 日韩一区二区在线观看视频播放| 国产精品19乱码一区二区三区| 99re热视频精品| 日本特黄a级片| 欧美一区成人| 牛人盗摄一区二区三区视频| 草民电影神马电影一区二区| 欧美成人久久久| 日韩av资源| 911国产精品| 亚洲精品www久久久久久| 国产欧美一区二区精品忘忧草| 涩涩网站在线看| 在线亚洲成人| 亚洲一区3d动漫同人无遮挡| aaa国产精品| 国产成人中文字幕| 色爱综合区网| 中文字幕av一区中文字幕天堂| 国产成人精品免费看视频| 欧美三级xxx| 青草影院在线观看| 久久久久久97三级| 三日本三级少妇三级99| 国产亚洲精品bv在线观看| 夜夜春亚洲嫩草影视日日摸夜夜添夜| 超碰精品在线| 国产免费观看久久黄| 国产精品yjizz视频网| 少妇av一区二区三区| 秋霞网一区二区| 日韩一区二区三区在线观看| 日韩国产成人在线| 午夜欧美视频在线观看| 性欧美疯狂猛交69hd| 久久久五月婷婷| 日本一级大毛片a一| 美女脱光内衣内裤视频久久网站| 色欲色香天天天综合网www| 97精品视频| 日韩高清国产一区在线观看| 成人av激情人伦小说| 亚洲xxx大片| 国模私拍国内精品国内av| 国产91精品高潮白浆喷水| 亚洲欧美成人影院| 日韩在线视频一区| 国产在线色视频| 国产视频亚洲视频| 欧美一区二区三区激情| 日韩女优电影在线观看| 国产精品久久久久久久久毛片 | av av片在线看| 欧美日韩综合在线| 日韩欧美国产另类| 日韩欧美精品网站| 六月丁香婷婷综合| 午夜av一区二区| 免费毛片一区二区三区| 亚洲综合在线视频| 深夜福利影院在线观看| 中文字幕一区av| 天堂а√在线中文在线鲁大师| 国产欧美日韩在线看| 无码一区二区三区在线| 久久久国产精品麻豆| mm131美女视频| 久久久久国产精品免费免费搜索| 日本xxxx裸体xxxx| 91麻豆高清视频| 毛茸茸多毛bbb毛多视频| 久久综合九色综合欧美98| www.久久国产| 99久久免费精品高清特色大片| 亚洲啪av永久无码精品放毛片| 国产不卡在线播放| 日韩成人av影院| 成人激情综合网站| 天堂www中文在线资源| av高清不卡在线| 黄色国产在线观看| 久久免费视频色| 日本精品在线观看视频| 中文字幕电影一区| 黄色录像一级片| 亚洲午夜电影在线| 国产成人免费观看视频| 欧美性猛交xxxx免费看久久久| 中文字幕免费在线观看视频| 精品欧美一区二区三区| 中文字幕在线播| 欧美日韩一区视频| 99久久精品国产色欲| 精品sm捆绑视频| 国产裸舞福利在线视频合集| 色婷婷综合久久久久中文字幕1| 成人看片免费| 国内精品久久久久久久久| 刘亦菲一区二区三区免费看| 国产日韩在线一区| 中文一区二区三区四区| 女人一区二区三区| 欧美hentaied在线观看| 成人一级生活片| 日韩高清中文字幕一区| 久久精品亚洲天堂| av色综合久久天堂av综合| 一级黄色片网址| 一区二区三区中文字幕电影| 午夜毛片在线观看| 欧美日韩久久不卡| 欧美一区二区黄片| 在线精品视频视频中文字幕| 牛牛精品在线| 国产精品成人播放| 亚洲国产高清在线观看| 奇米精品在线| 欧美99久久| 人人干人人视频| 成人免费观看视频| 一本色道久久88| 五月综合激情婷婷六月色窝| 一级二级三级视频| 亚洲国产小视频在线观看| 美女写真理伦片在线看| 91av视频在线免费观看| 伊人久久精品| 欧美久久综合性欧美| 亚洲一区二区三区| 人妻内射一区二区在线视频| 国产精品系列在线观看| 微拍福利一区二区| 五月天视频一区| a网站在线观看| 国产亚洲精品久久久久动| av不卡高清| 91嫩草在线| 日韩大片在线播放| 国产精品免费成人| 99在线精品视频| 劲爆欧美第一页| 欧美日韩精品一区二区在线播放| 欧美美乳在线| 午夜精品久久17c| 久久亚洲精精品中文字幕| 三区精品视频| 久久成人亚洲| 老熟妇精品一区二区三区| 亚洲精品一二三| 国产精品国产三级国产aⅴ | 国产免费看av| 午夜久久久久久久久久一区二区| 国产女同91疯狂高潮互磨| 在线观看不卡av| 欧美不卡高清一区二区三区| 久久亚裔精品欧美| 亚洲精选国产| 图片区偷拍区小说区| 亚洲女与黑人做爰| 国产一区二区三区四区视频| 最近2019好看的中文字幕免费| 欲香欲色天天天综合和网| 九色91视频| 中文国产一区| 亚洲av成人无码一二三在线观看| 一区二区国产盗摄色噜噜| 精品国精品国产自在久不卡| 久久久精品999| 久久久久久久久久久久电影| 成人午夜免费剧场| 国产精品一卡二| 中文字幕av免费在线观看| 欧美一级欧美三级在线观看| 怡红院在线观看| 国产成人精品日本亚洲11| 狠狠入ady亚洲精品经典电影| 欧美丰满熟妇bbb久久久| 亚洲第一狼人社区| 日韩电影免费| 国产精品男女猛烈高潮激情| 日韩.com| 五月天视频在线观看| 亚洲免费在线看| 亚洲精品久久久久久久久久久久久久 | 欧美做受高潮中文字幕| 亚洲国产精品久久艾草纯爱| 日本xxxx人| 日本人成精品视频在线| 欧美亚洲激情| 色婷婷激情视频| 亚洲成人免费影院| 男人天堂网在线观看| 国产精品高精视频免费| 四季av一区二区三区免费观看| 在线视频观看91| 亚洲午夜视频在线观看| 天天操天天干天天干| 国产成人精品999| 天天天综合网| 午夜不卡久久精品无码免费| 欧美天堂在线观看| 在线激情网站| 成人动漫视频在线观看免费| 久久国产精品亚洲77777| 污污视频网站在线免费观看| 日韩午夜激情电影| www.成人爱| 99热一区二区三区| 99久久精品一区二区| 亚洲毛片一区二区三区| 精品国内产的精品视频在线观看| 国产成人高清精品免费5388| 欧洲熟妇精品视频| 一区二区三区在线播| 免费看男男www网站入口在线| 成人免费福利视频| 亚洲伦理一区| 黄色一级大片在线免费观看| 日韩精品在线观| 国产午夜精品一区在线观看| 国产免费黄视频| 亚洲日本丝袜连裤袜办公室| 欧美女同网站| 成人性色av| 毛片av中文字幕一区二区| 国产成人无码精品久在线观看| 色婷婷综合成人|