Bitmap 比你想的更費記憶體 | 吊打 OOM

承香墨影發表於2017-08-24

版權宣告:

本賬號釋出文章均來自公眾號,承香墨影(cxmyDev),版權歸承香墨影所有。

每週會統一更新到這裡,如果喜歡,可關注公眾號獲取最新文章。

未經允許,不得轉載。

一、前言

在一個 App 中,無可避免的會有一些 Bitmap 的資源,會被打包在 apk 中,隨著 apk 釋出出去。而當你在使用這些 Bitmap 的資源的時候,它到底需要佔用多少記憶體空間?這是一個很實際的問題,把握不好就可能引發各種 OOM 的錯誤。

本文就來探討一下,本地的 Bitmap 到底佔用多少記憶體空間?

二、佔用多少記憶體?

2.1 如何獲取佔用的記憶體空間?

既然需要說道一個 Bitmap 資源,載入到記憶體中所要佔用的空間,那就需要有一個明確的獲取方法,來確定的知道它到底佔用了多少空間。而 Android 確實也為我們提供了類似的 API,那就是 Bitmap.getByteCount()

/bitm-bytecount.png
/bitm-bytecount.png

例如,現在專案內有一個 400 * 200 畫素的圖片,方在 drawable-xhdpi 目錄下,在 Nexus 6 裝置上,執行載入它。看它輸出的尺寸。

/bitm-getbitmap.png
/bitm-getbitmap.png

看一下輸出的結果:

I/cxmyDev: byteCound : 720000複製程式碼

可以看到,getByteCount() 是根據 getRowBytes() * getHeight() 計算出來的。getHeight() 方法它是 Bitmap 的高度,而 getRowBytes() 又是什麼?

2.2 getRowBytes() 的計算依據

/bitm-getrowbyte.png
/bitm-getrowbyte.png

getRowBytes() 方法,最終呼叫的是一個 nativeRowBytes() 的方法,它是一個 native 的方法。

既然要查就查到底,看看 native 的程式碼是如何實現的(文內 native 的原始碼,都是基於 Android 5.1.1,文末會有線上檢視地址,並且已經附帶行號,方便查閱)。

先看看 Bitmap.cpp 的程式碼中 rowBytes() 是如何實現的。

/bitm-cbitroumap5.png
/bitm-cbitroumap5.png

這裡閱讀的是 Android 5.1.1 的原始碼,實際上從 Android 6 開始,會使用 LocalScopedBitmap 去操作,它其實也只是對 SkBitmap 做了一個封裝而已。如下圖所示,rowBytes() 是使用的 LocalScoopedBitmap 來操作的,有興趣的可以繼續看看它是如何實現的。

/bitm-cbitroumap.png
/bitm-cbitroumap.png

可以看到,最終使用的是 SkBitmap 去實現的。

/bitm-skbmethod.png
/bitm-skbmethod.png

SkBitmap.cpp 裡就可以確認 ,色彩度為 ARGB_8888 圖片,每畫素會佔用 4 bytes 的大小。

看這個樣子,結合前面提到的 Bitmap.getByteCount() 的計算公式就是:

bitmapInRam = bitmapWidth * 4 bytes * bitmapHeight複製程式碼

但是如果依據這樣的公式計算一個結果,你會發現獲得的值會比真實的值差了很多。

前面 Demo 中的圖片,載入到記憶體中,佔用的記憶體是:720000 。但是用我們這裡得到的計算方式,計算的結果是。

400 * 200 * 4 = 320000複製程式碼

那麼,問題出在哪裡?

2.3 density 影響 Bitmap 記憶體

2.1 中的 Demo ,明確指出了需要圖片存放的 Drawable 目錄,以及使用的裝置,其實它們都是有關係的,不是無關係的路人甲。

關於圖片而言,放在不同的 Drawable 目錄下,對應的不同 density 的裝置。density 是裝置的固有引數,伴隨著 density 的,還有 densityDpi,它也是與裝置相關的,表示螢幕每英寸對應多少個點(非畫素點)。

它們之間的關係,可以直接查閱官方文件,這裡就不贅述了。

developer.android.com/guide/pract…

這裡說到的 density ,其實就是代表不同的 drawable-xxx 目錄。

/bitm-screen.png
/bitm-screen.png

上面是官方提供的一張比較經典的圖,可以看到,不同的目錄,代表不同的 density ,例如 xhdpi 代表的 density 就是 2。而這裡的 density 對 densityDip 的基準是 160 ,也就是說,mdpi 對應的 densityDpi 是 160 ,xhdpi 對應的 densityDpi 是 320。

它們的關係如下表:

density 1 1.5 2 3 3.5 4
densityDpi 160 240 320 480 560 640

density 和 densityDpi 在 Android 中,都有標準的 API 可以拿到,利用 DisplayMetrics 即可。

/bitm-densityapi.png
/bitm-densityapi.png

看到 Nexus 5 輸出的結果:

I/cxmyDev: density : 3.0
I/cxmyDev: densityDpi : 480複製程式碼

瞭解了裝置的 densitydensityDpi ,在繼續看看載入 Bitmap 的過程,使用的是 BitmapFactory.decodeResource() 方法。

/bitm-getbitmapFra.png
/bitm-getbitmapFra.png

從原始碼上可以看出,它實際上是分兩步完成的。

  1. 使用 openRawResource() 方法獲取圖片的原始流。
  2. 使用 decodeResourceStream() 方法,對資料流進行解碼和適配。

對於一個檔案流而言,在這裡我們是不需要關心的。主要影響圖片記憶體的是 decodeResourceStream() 方法中,對資料流進行解碼和適配的時候,都做了哪些處理。

bitm-stream
bitm-stream

在這個方法中,會傳遞一個 Options 的物件,用於配置當前圖片的解碼和適配。

從程式碼中可以瞭解到,影響圖片記憶體佔比的因素有 inDensityinTargetDensity 兩個。

Options 中這兩個值,都是可以設定的,如果不對其進行額外的操作,它們預設情況下,分別表示的含義:

  • inDensity :圖片存放的 Drawable 資料夾代表的 densityDpi 。
  • inTargetDensity : 當前裝置固有的 densityDpi 。

而使用他們的程式碼,都是在 native 中,繼續追看 BitmapFactory.cpp 的原始碼(原始碼太多,只貼關鍵點)

/bitm-scale.png
/bitm-scale.png

可以看到,它實際上是會通過兩個 density 計算出一個比例值 scale ,它會去對圖片原始的畫素進行 scale 表示的比例的縮放。

也就是說同一張圖片,放在不同 drawable 資料夾下的圖片,在不同的裝置上,實際上載入出來的尺寸也是不同的。

那計算圖片記憶體的公式,就應該調整為:

scale = targetDensity / inDensity
bitmapInRam = (bitmapWidth*scale) * (bitmapHeight*scale) * 4 bytes複製程式碼

再來使用新的公式,計算一下上面圖片的尺寸:

400 * (480/320) * 200 *(480/320) * 4 = 720000複製程式碼

可以看到,最終得出的和我們程式中計算的值一致 了,所以這就是我們最終得到的計算圖片在記憶體中,佔比的公式了。

再改寫上面的 Demo ,把細節點都輸出出來。

/bitm-code1.png
/bitm-code1.png

看看我們關心的 Log 輸出:

I/cxmyDev: byteCound : 720000
I/cxmyDev: rowBytes : 2400
I/cxmyDev: height : 300
I/cxmyDev: width : 600
I/cxmyDev: density : 3.0
I/cxmyDev: densityDpi : 480複製程式碼

3.4 查缺補漏

前面舉的例子中,圖片尺寸和裝置的 densityDpi 都是很規整的。但是不排除有一些比較不標準的裝置,載入的圖片使用上面的計算公式,依然對不上。

這個問題,還是需要在原始碼中找答案,對於不那麼標準的 densityDpi 的裝置而言,根據這個 scale 計算出來的尺寸,可能是一個 float 值,也就是存在小數的情況,而圖片的尺寸,都是以 int 型別為單位。所以 Android 為了規避這樣的問題,做了個容差值(0.5),去轉換成 int 型別。

程式碼依然在 BitmapFactory..cpp 中。

/bitm-scalesize.png
/bitm-scalesize.png

所以 getByteCount() 這個 Api 得到的尺寸,可能和我們前面使用公式計算的尺寸,略微有些偏差,這個值就是在小數點之間。

4、小結

好了,到這裡就講清楚了一個本地的 Bitmap ,載入到記憶體中,到底會佔用多少記憶體。

決定 Bitmap 佔用記憶體大小的因素,和圖片檔案在磁碟上佔用的空間一點關係都沒有,總結來說,有以下幾點:

  • 色彩格式:比如 ARGB_8888 、RGB_5555 這種,單位畫素佔的記憶體空間不同。
  • 圖片本身的畫素尺寸。
  • 圖片檔案存放的 Drawable 目錄。xhdpi 和 xxhdpi 可是不一樣的。
  • 目標裝置的 densityDpi 值。

最後附上Android 5.1.1 的相關原始碼,供大家參考

  • Bitmap.cpp :

androidxref.com/5.1.1_r6/xr…

  • SkBitmap.cpp:

androidxref.com/5.1.1_r6/xr…

  • BitmapFactory.cpp:

androidxref.com/5.1.1_r6/xr…

公眾號二維碼.jpg
公眾號二維碼.jpg

掃碼關注吧~

相關文章