版權宣告:
本賬號釋出文章均來自公眾號,承香墨影(cxmyDev),版權歸承香墨影所有。
每週會統一更新到這裡,如果喜歡,可關注公眾號獲取最新文章。
未經允許,不得轉載。
一、前言
在一個 App 中,無可避免的會有一些 Bitmap 的資源,會被打包在 apk 中,隨著 apk 釋出出去。而當你在使用這些 Bitmap 的資源的時候,它到底需要佔用多少記憶體空間?這是一個很實際的問題,把握不好就可能引發各種 OOM 的錯誤。
本文就來探討一下,本地的 Bitmap 到底佔用多少記憶體空間?
二、佔用多少記憶體?
2.1 如何獲取佔用的記憶體空間?
既然需要說道一個 Bitmap 資源,載入到記憶體中所要佔用的空間,那就需要有一個明確的獲取方法,來確定的知道它到底佔用了多少空間。而 Android 確實也為我們提供了類似的 API,那就是 Bitmap.getByteCount()
。
例如,現在專案內有一個 400 * 200 畫素的圖片,方在 drawable-xhdpi 目錄下,在 Nexus 6 裝置上,執行載入它。看它輸出的尺寸。
看一下輸出的結果:
I/cxmyDev: byteCound : 720000複製程式碼
可以看到,getByteCount()
是根據 getRowBytes() * getHeight()
計算出來的。getHeight()
方法它是 Bitmap 的高度,而 getRowBytes()
又是什麼?
2.2 getRowBytes() 的計算依據
getRowBytes()
方法,最終呼叫的是一個 nativeRowBytes()
的方法,它是一個 native 的方法。
既然要查就查到底,看看 native 的程式碼是如何實現的(文內 native 的原始碼,都是基於 Android 5.1.1,文末會有線上檢視地址,並且已經附帶行號,方便查閱)。
先看看 Bitmap.cpp 的程式碼中 rowBytes()
是如何實現的。
這裡閱讀的是 Android 5.1.1 的原始碼,實際上從 Android 6 開始,會使用 LocalScopedBitmap 去操作,它其實也只是對 SkBitmap 做了一個封裝而已。如下圖所示,rowBytes() 是使用的 LocalScoopedBitmap 來操作的,有興趣的可以繼續看看它是如何實現的。
可以看到,最終使用的是 SkBitmap 去實現的。
在 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 目錄。
上面是官方提供的一張比較經典的圖,可以看到,不同的目錄,代表不同的 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 即可。
看到 Nexus 5 輸出的結果:
I/cxmyDev: density : 3.0
I/cxmyDev: densityDpi : 480複製程式碼
瞭解了裝置的 density 和 densityDpi ,在繼續看看載入 Bitmap 的過程,使用的是 BitmapFactory.decodeResource()
方法。
從原始碼上可以看出,它實際上是分兩步完成的。
- 使用
openRawResource()
方法獲取圖片的原始流。 - 使用
decodeResourceStream()
方法,對資料流進行解碼和適配。
對於一個檔案流而言,在這裡我們是不需要關心的。主要影響圖片記憶體的是 decodeResourceStream()
方法中,對資料流進行解碼和適配的時候,都做了哪些處理。
在這個方法中,會傳遞一個 Options 的物件,用於配置當前圖片的解碼和適配。
從程式碼中可以瞭解到,影響圖片記憶體佔比的因素有 inDensity 和 inTargetDensity 兩個。
Options 中這兩個值,都是可以設定的,如果不對其進行額外的操作,它們預設情況下,分別表示的含義:
- inDensity :圖片存放的 Drawable 資料夾代表的 densityDpi 。
- inTargetDensity : 當前裝置固有的 densityDpi 。
而使用他們的程式碼,都是在 native 中,繼續追看 BitmapFactory.cpp 的原始碼(原始碼太多,只貼關鍵點)
可以看到,它實際上是會通過兩個 density 計算出一個比例值 scale ,它會去對圖片原始的畫素進行 scale 表示的比例的縮放。
也就是說同一張圖片,放在不同 drawable 資料夾下的圖片,在不同的裝置上,實際上載入出來的尺寸也是不同的。
那計算圖片記憶體的公式,就應該調整為:
scale = targetDensity / inDensity
bitmapInRam = (bitmapWidth*scale) * (bitmapHeight*scale) * 4 bytes複製程式碼
再來使用新的公式,計算一下上面圖片的尺寸:
400 * (480/320) * 200 *(480/320) * 4 = 720000複製程式碼
可以看到,最終得出的和我們程式中計算的值一致 了,所以這就是我們最終得到的計算圖片在記憶體中,佔比的公式了。
再改寫上面的 Demo ,把細節點都輸出出來。
看看我們關心的 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 中。
所以 getByteCount()
這個 Api 得到的尺寸,可能和我們前面使用公式計算的尺寸,略微有些偏差,這個值就是在小數點之間。
4、小結
好了,到這裡就講清楚了一個本地的 Bitmap ,載入到記憶體中,到底會佔用多少記憶體。
決定 Bitmap 佔用記憶體大小的因素,和圖片檔案在磁碟上佔用的空間一點關係都沒有,總結來說,有以下幾點:
- 色彩格式:比如 ARGB_8888 、RGB_5555 這種,單位畫素佔的記憶體空間不同。
- 圖片本身的畫素尺寸。
- 圖片檔案存放的 Drawable 目錄。xhdpi 和 xxhdpi 可是不一樣的。
- 目標裝置的 densityDpi 值。
最後附上Android 5.1.1 的相關原始碼,供大家參考
- Bitmap.cpp :
- SkBitmap.cpp:
- BitmapFactory.cpp:
掃碼關注吧~