Bitmap記憶體模型
- 在 Android 2.2(API 8)及更低版本上,當發生垃圾回收時,應用的執行緒會停止(stop the world),這會導致延遲,從而降低性能,Android 2.3 添加了并發GC功能,這意味著系統不再參考位圖后,很快就會回收記憶體,
- 在 Android 2.3.3(API 10)及更低版本上,bitmap 的像素資料存盤在 native 記憶體(native memeory)中,它與存盤在 Dalvik 堆中的 bitmap 物件本身是分開的,native 記憶體中的像素資料并不以可預測的方式釋放,可能會導致應用短暫超出其記憶體限制并崩潰,
- 從 Android 3.0(API 11)到 Android 7.1(API 級別 25),像素資料會與關聯的 bitmap 物件一起存盤在 Dalvik 堆上,因此其 bitmap 的使用的記憶體會隨著 bitmap 物件一起回收,
- 在 Android 8.0(API 26)及更高版本中,位影像素資料存盤在native堆(native heap)中,當然,盡管位影像素資料又放回了 native 堆中,但其會跟隨 Java 物件的釋放而被釋放,
無論是 Api 26 前還是之后的回收實作,釋放 Native 層的 Bitmap 物件的思想都是去監聽 Java 層的 Bitmap 是否被釋放,一旦當 Java 層的 Bitmap 物件被釋放則立即去釋放 Native 層的 Bitmap ,只不過 Api 26 以前是基于 Java 的 GC 機制,而 Api 26 后是注冊 native 的 Finalizer 方法,更詳細的分析可查看: 圖形影像處理 - 我們所不知道的 Bitmap,
BitmapFactory.Options
BitmapFactory.Options 是 BitmapFactory 從不同的輸入源中創建 Bitmap 物件的控制引數,
- inBitmap
Android 3.0 (API level 11) 引入了BitmapFactory.Options.inBitmap欄位
如果設定了這個值,則使用了這個 Options 物件的 decode 方法在 decode 時將會嘗試去復用 bitmap,如果失敗了,將會拋出java.lang.IllegalArgumentException例外,對于被復用的 bitmap 要求其是可修改的(mutable),并且對于被復用的 bitmap 將會保持其可修改的屬性,即使 decode 的資源將會導致 bitmap 變成不可修改的(immutable),由于上述的限制存在,因此可能導致 decode 失敗,因此不應該假定復用的 bitmap 是始終有效的,通過 decode 回傳的 bitmap,檢查其 inBitmap 欄位可以確定 bitmap 是否被復用了,
從 KITKAT 版本開始,BitmapFactory 可以復用任何支持修改并且其getAllocationByteCount()大于等于要解碼資源的getByteCount()的bitmap,
在 KITKAT 版本之前,對于要復用的 bitmap 還存在其他限制:- 只支持
jpeg或png格式的圖片 - 復用的 bitmap 其大小要與 decode 得到的 bitmap 大小一致,并且其
inSampleSize欄位設定為1,也就是不支持采樣, - 復用的 bitmap 的
android.graphics.Bitmap.Config將會覆寫設定的inPreferredConfig,
- 只支持
@Nullable
public static Bitmap decodeFile(@NonNull String pathName) {
Bitmap bitmap;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(pathName, options);
options.inJustDecodeBounds = false;
options.inSampleSize = 1;
Bitmap inBitmap = AndroidBitmapPool.getInstance().get(options.outWidth, options.outHeight, options.inPreferredConfig);
try {
// 判斷是否可以使用 inBitmap,因為 inBitmap 在不同 Android 版本存在一些不同的限制
if (inBitmap != null && Util.canUseInBitmap(inBitmap, options)) {
// 復用需要把可修改的開關打開
options.inMutable = true;
options.inBitmap = inBitmap;
} else {
AndroidBitmapPool.getInstance().putBitmap(inBitmap);
}
bitmap = BitmapFactory.decodeFile(pathName, options);
// 檢查是否復用成功
if (bitmap == options.inBitmap) {
Log.i(TAG, "decodeFile: inBitmap reuse successfully");
}
} catch (Exception e) {
Log.e(TAG, "decodeFile", e);
bitmap = BitmapFactory.decodeFile(pathName);
}
return bitmap;
}
public static boolean canUseInBitmap(@NonNull Bitmap inBitmap, @NonNull BitmapFactory.Options options) {
//{@link android.graphics.BitmapFactory.Options.inBitmap} prior to KITKAT has some constraints
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int width = options.outWidth / options.inSampleSize;
int height = options.outHeight / options.inSampleSize;
int byteCount = width * height * getBytesPerPixel(inBitmap.getConfig());
int inBitmapByteCount = getBitmapByteSize(inBitmap);
return inBitmapByteCount >= byteCount;
}
return options.inSampleSize == 1 && options.outWidth == inBitmap.getWidth() && options.outHeight == inBitmap.getHeight();
}
-
inMutable
如果設定了這個值,那么 decode 方法將會回傳一個可修改的 bitmap 物件,這個屬性不能與inPreferredConfig設為android.graphics.Bitmap.Config#HARDWARE時候一同設定,因為硬體位圖是不可變的, -
inJustDecodeBounds
如果設定了這個值,那么 decode 將會回傳null,即 bitmap 不會被加載進記憶體,但是對于 Options 的out*欄位將會被設定,如outWidth、outHeight和outMimeType,這對于只想知道圖片寬高資訊非常有用, -
inSampleSize
圖片采樣的控制選項,當其值大于1時便會進行下采樣,通過這個標志位,在加載圖片時可有效節省記憶體,需要注意的是,這個值必須是2的冪次方,如果不是,將向下舍入為最接近的2的冪次方的值(根據實際測驗,inSampleSize并非是2的冪次方,測驗環境為 Android 10,MIUI 12 Xiaomi 9Pro, 在原始碼 BitmapFactory.cpp 中也沒有找到相關的代碼),設定 inSampleSize 之后,解碼得到的 bitmap 的寬、高都會縮小 inSampleSize 倍,如inSampleSize = 4,那么寬和高都會變為原來的1/4,整個大小會變為原來的1/16,對于 inSampleSize 的確定,在Loading Large Bitmaps Efficiently給出了示例,
inSampleSize測驗實體
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(IMAGE_PATH, options)
Log.i(TAG, "width = ${options.outWidth}, height = ${options.outHeight}, mimeType = ${options.outMimeType}")
val imageWidth = options.outWidth
val imageHeight = options.outHeight
options.inJustDecodeBounds = false
for (i in 1 until 6) {
options.inSampleSize = i
val bitmap = BitmapFactory.decodeFile(IMAGE_PATH, options)
Log.i(TAG, "bitmap width = ${bitmap.width}, height = ${bitmap.height}, width for inSampleSize = ${imageWidth / bitmap.width}, height for inSampleSize = ${imageHeight / bitmap.height}")
}
/*
width = 4000, height = 3000, mimeType = image/jpeg
bitmap width = 4000, height = 3000, width for inSampleSize = 1, height for inSampleSize = 1
bitmap width = 2000, height = 1500, width for inSampleSize = 2, height for inSampleSize = 2
bitmap width = 1333, height = 1000, width for inSampleSize = 3, height for inSampleSize = 3
bitmap width = 1000, height = 750, width for inSampleSize = 4, height for inSampleSize = 4
bitmap width = 800, height = 600, width for inSampleSize = 5, height for inSampleSize = 5
*/
確定 inSampleSize 的大小
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// Raw height and width of image
val (height: Int, width: Int) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
- inPreferredConfig
設定解碼圖片的像素格式,默認使用ARGB_8888進行解碼,對于不同的配置,其每個像素需要的位元組數也不一樣,通常在不需要 alpha 通道的場景下,選擇RGB_565進行解碼,這樣能比選擇ARGB_8888節省一半的記憶體,
ALPHA_8 -> 1個位元組
RGB_565 -> 2個位元組(每個像素需要16個bit來表示)
ARGB_4444 -> 2個位元組
RGBA_F16 -> 8個位元組
ARGB_8888 -> 4個位元組
- inDensity
圖片所在drawable檔案夾對應的密度,當這個值為0時,decodeResource會根據資源所在drawable檔案夾填充這個值,各檔案夾對應的 density 關系如下:
| 檔案夾 | density |
|---|---|
| drawable | 0 |
| ldpi | 120 |
| mdpi | 160 |
| hdpi | 240 |
| xhdpi | 320 |
| xxhdpi | 480 |
| xxxhdpi | 640 |
將圖片放入默認 drawable 檔案夾(不指定解析度),則最侄訓使用默認的 Density(DisplayMetrics.DENSITY_DEFAULT=160)
-
inTargetDensity
bitmap 將會繪制到的目標像素密度,也就是螢屏密度,這個值通常跟inDensity和inScaled配合使用,來決定是否縮放以及如何縮放 bitmap 的大小,當這個值為0時,decodeResource會根據Resources物件的DisplayMetrics來設定其值, -
inScreenDensity
-
inScaled
當被設定為 true 時,如果inDensity和inTargetDensity都不為0,那么加載的 bitmap 會被縮放到符合inTargetDensity的值,.9圖不受這個標志位的影響,始侄訓被縮放,
Bitmap 記憶體占用計算
計算公式:
(width / inSampleSize * inTargetDensity / inDensity) * (height / inSampleSize * inTargetDensity / inDensity) * bytesPerPixel
其中bytesPerPixel的值根據解碼圖片傳入的 Bitmap.Config 決定,可參考inPreferredConfig,如果不是 drawable 檔案夾下的資源的話,計算公式中 inTargetDensity / inDensity 當作1來處理,也就是不需要理會inTargetDensity和inDensity導致的縮放影響,
對于 bitmap 的記憶體占用大小,可以通過getByteCount方法獲取,在 Api 19 (Build.VERSION_CODES#KITKAT)及以后,新增了一個方法getAllocationByteCount,其表示分配給 bitmap 的記憶體大小,這個值大于等于getByteCount的數值,一般情況下,二者的回傳值相當,當 bitmap 復用的時候,則可能大于getByteCount的值,
支持解碼的圖片格式
注:對于 BitmapRegionDecoder 只支持 JPEG 和 PNG 格式的圖片
| Format | Encoder | Decoder | Details | File Types Container Formats |
|---|---|---|---|---|
| BMP | YES | BMP (.bmp) | ||
| GIF | YES | GIF (.gif) | ||
| JPEG | YES | YES | Base+progressive | JPEG (.jpg) |
| PNG | YES | YES | PNG (.png) | |
| WebP | Android 4.0+ Lossless: Android 10+ Transparency: Android 4.2.1+ | Android 4.0+ Lossless: Android 4.2.1+ Transparency: Android 4.2.1+ | Lossless encoding can be achieved on Android 10 using a quality of 100. | WebP (.webp) |
| HEIF | Android 8.0+ | HEIF (.heic; .heif) |
Bitmap 記憶體優化
Bitmap 在應用中一般是導致 OOM 的幾大原因之一,如何減少解碼圖片導致的 OOM 及 Bitmap 的創建回收導致的記憶體抖動就顯得尤為重要,Bitmap 記憶體優化一般有以下幾個手段:
- 使用
Options.inSampleSize對圖片進行采樣,一般圖片的寬高都比我們顯示圖片的區域大很多,因此我們不必以原圖尺寸解碼圖片,通過采樣演算法,計算一個合理的采樣值,在解碼時對圖片進行下采樣,
可參考Glide Downsampler - 使用
Options.inBitmap對圖片進行復用,圖片復用有兩個好處,一個是加快圖片解碼速度,減少 Bitmap 創建耗時;另一個則是減少頻繁申請和銷毀 Bitmap 導致的記憶體抖動,在實際使用中,可建立 BitmapPool,每次需要使用 Bitmap 時,從 BitmapPool 申請符合要求的 Bitmap 記憶體,當 Bitmap 不需要使用的,放回 BitmapPool,詳細實作可參考Glide BitmapPool, - 對于不需要 alpha 通道的圖片,
Options.inPreferredConfig可選擇Bitmap.Config.RGB_565,相比較于默認的Bitmap.Config.ARGB_8888,一個像素只需要兩個位元組,整體記憶體可節省一半, - 建立 Bitmap 記憶體快取,對于已經解碼的圖片,當下次需要再次使用時,可從記憶體快取中,直接取出,減少二次解碼的耗時,詳細實作可參考Glide MemoryCache
參考鏈接
- Loading Large Bitmaps Efficiently
- Caching Bitmaps
- Managing Bitmap Memory
- Android DisplayingBitmaps Sample
- Glide bitmap_recycle
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/286123.html
標籤:Android
上一篇:flutter 實作 有洗掉影片的 listview
下一篇:實體 -自定義繪制滑動解鎖
