版權聲明:html
本帳號發佈文章均來自公衆號,承香墨影(cxmyDev),版權歸承香墨影全部。android
每週會統一更新到這裏,若是喜歡,可關注公衆號獲取最新文章。api
未經容許,不得轉載。bash
在一個 App 中,無可避免的會有一些 Bitmap 的資源,會被打包在 apk 中,隨着 apk 發佈出去。而當你在使用這些 Bitmap 的資源的時候,它到底須要佔用多少內存空間?這是一個很實際的問題,把握很差就可能引起各類 OOM 的錯誤。ide
本文就來探討一下,本地的 Bitmap 到底佔用多少內存空間?oop
既然須要說道一個 Bitmap 資源,加載到內存中所要佔用的空間,那就須要有一個明確的獲取方法,來肯定的知道它到底佔用了多少空間。而 Android 確實也爲咱們提供了相似的 API,那就是 Bitmap.getByteCount()
。ui
例如,如今項目內有一個 400 * 200 像素的圖片,方在 drawable-xhdpi 目錄下,在 Nexus 6 設備上,運行加載它。看它輸出的尺寸。spa
看一下輸出的結果:3d
I/cxmyDev: byteCound : 720000複製代碼
能夠看到,getByteCount()
是根據 getRowBytes() * getHeight()
計算出來的。getHeight()
方法它是 Bitmap 的高度,而 getRowBytes()
又是什麼?code
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.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 中這兩個值,都是能夠設置的,若是不對其進行額外的操做,它們默認狀況下,分別表示的含義:
而使用他們的代碼,都是在 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複製代碼
前面舉的例子中,圖片尺寸和設備的 densityDpi 都是很規整的。可是不排除有一些比較不標準的設備,加載的圖片使用上面的計算公式,依然對不上。
這個問題,仍是須要在源碼中找答案,對於不那麼標準的 densityDpi 的設備而言,根據這個 scale 計算出來的尺寸,多是一個 float 值,也就是存在小數的狀況,而圖片的尺寸,都是以 int 類型爲單位。因此 Android 爲了規避這樣的問題,作了個容差值(0.5),去轉換成 int 類型。
代碼依然在 BitmapFactory..cpp 中。
因此 getByteCount()
這個 Api 獲得的尺寸,可能和咱們前面使用公式計算的尺寸,略微有些誤差,這個值就是在小數點之間。
好了,到這裏就講清楚了一個本地的 Bitmap ,加載到內存中,到底會佔用多少內存。
決定 Bitmap 佔用內存大小的因素,和圖片文件在磁盤上佔用的空間一點關係都沒有,總結來講,有如下幾點:
最後附上Android 5.1.1 的相關源碼,供你們參考