Android Bitmap的使用及優化

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 還存在其他限制:
    • 只支持jpegpng格式的圖片
    • 複用的 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*字段將會被設置,如outWidthoutHeightoutMimeType,這對於只想知道圖片寬高信息非常有用。

  • 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 -> 4個字節
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 將會繪製到的目標像素密度,也就是屏幕密度。這個值通常跟inDensityinScaled配合使用,來決定是否縮放以及如何縮放 bitmap 的大小。當這個值爲0時,decodeResource會根據Resources對象的DisplayMetrics來設置其值。

  • inScreenDensity

  • inScaled
      當被設置爲 true 時,如果inDensityinTargetDensity都不爲0,那麼加載的 bitmap 會被縮放到符合inTargetDensity的值。.9圖不受這個標誌位的影響,始終會被縮放。

Bitmap 內存佔用計算

計算公式:

(width / inSampleSize * inTargetDensity / inDensity) * (height / inSampleSize * inTargetDensity / inDensity) * bytesPerPixel

  其中bytesPerPixel的值根據解碼圖片傳入的 Bitmap.Config 決定,可參考inPreferredConfig,如果不是 drawable 文件夾下的資源的話,計算公式中 inTargetDensity / inDensity 當作1來處理,也就是不需要理會inTargetDensityinDensity導致的縮放影響。
  對於 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

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章