Bitmap 比你想的更費內(nèi)存 | 吊打 OOM
一、前言
在一個 App 中,無可避免的會有一些 Bitmap 的資源,會被打包在 apk 中,隨著 apk 發(fā)布出去。而當(dāng)你在使用這些 Bitmap 的資源的時候,它到底需要占用多少內(nèi)存空間?這是一個很實際的問題,把握不好就可能引發(fā)各種 OOM 的錯誤。
本文就來探討一下,本地的 Bitmap 到底占用多少內(nèi)存空間?
二、占用多少內(nèi)存?
2.1 如何獲取占用的內(nèi)存空間?
既然需要說道一個 Bitmap 資源,加載到內(nèi)存中所要占用的空間,那就需要有一個明確的獲取方法,來確定的知道它到底占用了多少空間。而 Android 確實也為我們提供了類似的 API,那就是 Bitmap.getByteCount() 。
例如,現(xiàn)在項目內(nèi)有一個 400 * 200 像素的圖片,方在 drawable-xhdpi 目錄下,在Nexus 6 設(shè)備上,運行加載它??此敵龅某叽?。
看一下輸出的結(jié)果:
- I/cxmyDev: byteCound : 720000
可以看到,getByteCount() 是根據(jù) getRowBytes() * getHeight() 計算出來的。getHeight() 方法它是 Bitmap 的高度,而 getRowBytes() 又是什么?
2.2 getRowBytes() 的計算依據(jù)
getRowBytes() 方法,最終調(diào)用的是一個 nativeRowBytes() 的方法,它是一個native 的方法。
既然要查就查到底,看看 native 的代碼是如何實現(xiàn)的(文內(nèi) native 的源碼,都是基于Android 5.1.1,文末會有在線查看地址,并且已經(jīng)附帶行號,方便查閱)。
先看看 Bitmap.cpp 的代碼中 rowBytes() 是如何實現(xiàn)的。
這里閱讀的是 Android 5.1.1 的源碼,實際上從 Android 6 開始,會使用 LocalScopedBitmap 去操作,它其實也只是對 SkBitmap 做了一個封裝而已。如下圖所示,rowBytes() 是使用的 LocalScoopedBitmap 來操作的,有興趣的可以繼續(xù)看看它是如何實現(xiàn)的。
可以看到,最終使用的是 SkBitmap 去實現(xiàn)的。
在 SkBitmap.cpp 里就可以確認(rèn) ,色彩度為 ARGB_8888 圖片,每像素會占用 4 bytes 的大小。
看這個樣子,結(jié)合前面提到的 Bitmap.getByteCount() 的計算公式就是:
- bitmapInRam = bitmapWidth * 4 bytes * bitmapHeight
但是如果依據(jù)這樣的公式計算一個結(jié)果,你會發(fā)現(xiàn)獲得的值會比真實的值差了很多。
前面 Demo 中的圖片,加載到內(nèi)存中,占用的內(nèi)存是:720000 。但是用我們這里得到的計算方式,計算的結(jié)果是。
- 400 * 200 * 4 = 320000
那么,問題出在哪里?
2.3 density 影響 Bitmap 內(nèi)存
2.1 中的 Demo ,明確指出了需要圖片存放的 Drawable 目錄,以及使用的設(shè)備,其實它們都是有關(guān)系的,不是無關(guān)系的路人甲。
關(guān)于圖片而言,放在不同的 Drawable 目錄下,對應(yīng)的不同 density 的設(shè)備。density 是設(shè)備的固有參數(shù),伴隨著 density 的,還有 densityDpi,它也是與設(shè)備相關(guān)的,表示屏幕每英寸對應(yīng)多少個點(非像素點)。
它們之間的關(guān)系,可以直接查閱官方文檔,這里就不贅述了。
https://developer.android.com/guide/practices/screens_support.html
這里說到的 density ,其實就是代表不同的 drawable-xxx 目錄。
上面是官方提供的一張比較經(jīng)典的圖,可以看到,不同的目錄,代表不同的 density ,例如 xhdpi 代表的 density 就是 2。而這里的 density 對 densityDip 的基準(zhǔn)是 160 ,也就是說,mdpi 對應(yīng)的 densityDpi 是 160 ,xhdpi 對應(yīng)的 densityDpi 是 320。
它們的關(guān)系如下表:
density 和 densityDpi 在 Android 中,都有標(biāo)準(zhǔn)的 API 可以拿到,利用 DisplayMetrics即可。
看到 Nexus 5 輸出的結(jié)果:
- I/cxmyDev: density : 3.0
- I/cxmyDev: densityDpi : 480
了解了設(shè)備的 density 和 densityDpi ,在繼續(xù)看看加載 Bitmap 的過程,使用的是 BitmapFactory.decodeResource() 方法。
從源碼上可以看出,它實際上是分兩步完成的。
- 使用 openRawResource() 方法獲取圖片的原始流。
- 使用 decodeResourceStream() 方法,對數(shù)據(jù)流進行解碼和適配。
對于一個文件流而言,在這里我們是不需要關(guān)心的。主要影響圖片內(nèi)存的是 decodeResourceStream() 方法中,對數(shù)據(jù)流進行解碼和適配的時候,都做了哪些處理。
在這個方法中,會傳遞一個 Options 的對象,用于配置當(dāng)前圖片的解碼和適配。
從代碼中可以了解到,影響圖片內(nèi)存占比的因素有 inDensity 和 inTargetDensity 兩個。
Options 中這兩個值,都是可以設(shè)置的,如果不對其進行額外的操作,它們默認(rèn)情況下,分別表示的含義:
- inDensity :圖片存放的 Drawable 文件夾代表的 densityDpi 。
- inTargetDensity : 當(dāng)前設(shè)備固有的 densityDpi 。
而使用他們的代碼,都是在 native 中,繼續(xù)追看 BitmapFactory.cpp 的源碼(源碼太多,只貼關(guān)鍵點)
可以看到,它實際上是會通過兩個 density 計算出一個比例值 scale ,它會去對圖片原始的像素進行 scale 表示的比例的縮放。
也就是說同一張圖片,放在不同 drawable 文件夾下的圖片,在不同的設(shè)備上,實際上加載出來的尺寸也是不同的。
那計算圖片內(nèi)存的公式,就應(yīng)該調(diào)整為:
- scale = targetDensity / inDensity
- bitmapInRam = (bitmapWidth*scale) * (bitmapHeight*scale) * 4 bytes
再來使用新的公式,計算一下上面圖片的尺寸:
- 400 * (480/320) * 200 *(480/320) * 4 = 720000
可以看到,最終得出的和我們程序中計算的值一致 了,所以這就是我們最終得到的計算圖片在內(nèi)存中,占比的公式了。
再改寫上面的 Demo ,把細節(jié)點都輸出出來。
看看我們關(guān)心的 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 查缺補漏
前面舉的例子中,圖片尺寸和設(shè)備的 densityDpi 都是很規(guī)整的。但是不排除有一些比較不標(biāo)準(zhǔn)的設(shè)備,加載的圖片使用上面的計算公式,依然對不上。
這個問題,還是需要在源碼中找答案,對于不那么標(biāo)準(zhǔn)的 densityDpi 的設(shè)備而言,根據(jù)這個scale 計算出來的尺寸,可能是一個 float 值,也就是存在小數(shù)的情況,而圖片的尺寸,都是以 int 類型為單位。所以 Android 為了規(guī)避這樣的問題,做了個容差值(0.5),去轉(zhuǎn)換成 int 類型。
代碼依然在 BitmapFactory..cpp 中。
所以 getByteCount() 這個 Api 得到的尺寸,可能和我們前面使用公式計算的尺寸,略微有些偏差,這個值就是在小數(shù)點之間。
4、小結(jié)
好了,到這里就講清楚了一個本地的 Bitmap ,加載到內(nèi)存中,到底會占用多少內(nèi)存。
決定 Bitmap 占用內(nèi)存大小的因素,和圖片文件在磁盤上占用的空間一點關(guān)系都沒有,總結(jié)來說,有以下幾點:
- 色彩格式:比如 ARGB_8888 、RGB_5555 這種,單位像素占的內(nèi)存空間不同。
- 圖片本身的像素尺寸。
- 圖片文件存放的 Drawable 目錄。xhdpi 和 xxhdpi 可是不一樣的。
- 目標(biāo)設(shè)備的 densityDpi 值。
最后附上Android 5.1.1 的相關(guān)源碼,供大家參考
Bitmap.cpp :
http://androidxref.com/5.1.1_r6/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp
SkBitmap.cpp:
http://androidxref.com/5.1.1_r6/xref/external/skia/src/core/SkBitmap.cpp
BitmapFactory.cpp:
http://androidxref.com/5.1.1_r6/xref/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp
【本文為51CTO專欄作者“張旸”的原創(chuàng)稿件,轉(zhuǎn)載請通過微信公眾號聯(lián)系作者獲取授權(quán)】