Bitmap記憶體佔用及華為機型長圖載入問題
部分內容轉載自:
0 本章內容
- 一個Bitmap物件佔多大記憶體空間
- 解決大圖載入OOM的幾種方式
- 華為mate10等機型長圖載入失敗問題
1 一個Bitmap物件佔多大記憶體空間
1.1 解碼格式
筆者曾經在面試時,也曾經遇到過這個問題:一個Bitmap物件, 究竟佔用多大記憶體空間?
一般來講,Bitmap的佔用的記憶體空間,有下面的計算方法:
bitmap的記憶體空間 = 圖片寬度 * 圖片高度 * 單位畫素所佔位元組數(Byte)
圖片寬度和高度比較好理解,單位畫素標識每個畫素所佔位元組數。在不同的編碼下有所不同。例如RGB_8888 ARGB四個通道,每個通道佔8位,即每個畫素點佔32位,4個位元組。RGB_565三個通道,分別佔5、6、5位,即每個畫素點佔16位,2個位元組。
1.2 不同目錄導致佔用記憶體不同
假設一張長400畫素,寬300畫素,jpg格式。我們把它放到 assets 目錄中。
圖片的佔用記憶體,我們可以用Bitmap的getByteCount方法得到,在執行程式前,我們先按照公式算出該圖記憶體佔用是多少。由於Android預設採用ARGB_8888格式解碼,即一個畫素點佔4個位元組,因此:
該圖佔用記憶體 = 400 * 300 * 4 = 480000 Byte
PS:不同資源目錄下,圖片記憶體佔用情況
圖片放在drawable-hdpi目錄時,記憶體佔用1920000位元組:
圖片放在drawable-xhdpi目錄時,記憶體佔用1080000位元組:
圖片放在drawable-xxhdpi
螢幕密度與Drawable系列目錄的對應關係如下:
目錄 | 螢幕密度 |
---|---|
drawable-ldpi | 120dpi |
drawable-mdpi | 160dpi |
drawable-hdpi | 240dpi |
drawable-xhdpi | 320dpi |
drawable-xxhdpi | 480dpi |
測試機型為小米6,手機螢幕DPI為480。其根本原因在於,預設載入圖片時會根據圖片所處的檔案目錄的螢幕密度,與當前手機的螢幕密度進行對比縮放。
因此,我們的例子中,圖片放入hdpi資料夾時所佔記憶體為:
scale = 480 / 240 = 2;
記憶體 = int(400 * 2 + 0.5) * int(300 * 2 + 0.5) * 4 = 1920000
圖片放入xhdpi資料夾時所佔記憶體:
scale = 480 / 320 = 1.5;
記憶體 = int(400 * 1.5 + 0.5) * int(300 * 1.5 + 0.5) * 4 = 1080000
原始碼在BitmapFactory.java中:
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
//實際上,我們這裡的opts是null的,所以在這裡初始化。
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density; //這裡density的值如果對應資源目錄為hdpi的話,就是240
}
}
if (opts.inTargetDensity == 0 && res != null) {
//請注意,inTargetDensity就是當前的顯示密度,比如三星s6時就是640
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
2 解決大圖載入OOM的幾種方式
大圖載入避免OOM,算是老話題了,網上的資料很多,在此筆者只介紹幾種方法。OOM名為記憶體溢位,在避免記憶體溢位前,我們先搞清楚一個問題:為什麼會發生記憶體溢位?
2.1 largeHeap模式
這要從Android的記憶體管理說起,Android系統的手機在系統底層指定了堆記憶體的上限值,我們的程式在申請記憶體空間時,為了確保能夠成功申請到記憶體空間,應該保證當前已分配的記憶體加上當前需要分配的記憶體值的總大小不能超過當前堆的最大記憶體值(手機廠商會根據手機的配置情況來對其進行調整)。
如果解析後圖片的大小加上目前已分配的堆記憶體大小超過堆記憶體最大值,系統先會執行一遍gc操作,若gc後仍然超過堆記憶體最大值,這時候將會丟擲異常,這是就是發生了我們常說的OOM。
檢視手機的堆記憶體大小,我們有兩種方式:
通過程式碼檢視:
ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
Log.d("AchillesL","size: " + activityManager.getMemoryClass());
OR,通過adb指令檢視:
adb shell getprop|grep heapgrowthlimit
小米,查到堆記憶體最大值為256M。
但是可以通過AndroidManifest.xml中的Application節點中宣告android:largeHeap="true"
標識,即可以分配到更大的堆記憶體空間。
2.2 採用合適的圖片解碼格式
很多情況下,網路下載的圖片都使用jpg格式。對於jpg圖片,使用RGB_565格式進行解碼比ARGB_8888解碼會節省更多記憶體。
設定圖片的解碼格式,可以通過BitmapFactory.Options類的inPreferredConfig屬性來指定。程式碼如下所示:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.pic,options);
總結:如果不需要alpha通道,特別是資源本身為jpg格式的情況下,用RGB_565格式解碼更加節省記憶體。
3 華為mate10在載入大圖的問題
3.1 問題定位
開發過程中發現一個bug,表現是:在華為mate10 pro等機型上載入超長圖片失敗,會有一次黑屏閃爍,然後圖片出不來。圖片規格約為480*10000+。最初是懷疑Glide下載後圖片太大導致記憶體載入不上。但是經過定位發現問題出在華為機子本身上,檢視log發現一個openGL錯誤;Bitmap too large to be uploaded into a texture。提示超過了openGL 8192*8192大小限制。
我們知道,繪圖的最終點在於使用openGL將圖片進行繪製。而openGL在是有硬體繪製大小限制。這個值在Canvas中,可以通過Canvas.getMaximumBitmapWidth()獲取。Andriod系統本身限定值為MAXMIMUM_BITMAP_SIZE = 32766。因為才疏學淺對硬體知識不熟,我理解這值代表的是openGL繪製時可以一次容納的圖片寬高值。
通過測試部分國產手機,發現大部分獲取的值為16384。個人理解是在硬體燒錄的時候,手機廠商改了這個值,相當於閹割了部分GPU的效能。但是這不是最坑爹的,最坑爹的是華為mate10啊。。。你一個8.0的機型,這個值居然是8192。。。還沒有小米魅族2年前的機型大啊。。。搞毛啊。。。
3.2 解決方法
1、關閉硬體加速,Android預設在api14以後,會開啟硬體加速。但是在不使用硬體加速的情況下,並不會限制一次載入的圖片大小。但是效能影響太大,作罷。
2、在獲取到圖片時,先與系統的openGL可繪製大小進行比對,一旦發現大於系統大小,則對圖片進行scale裁剪。