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 对象的配置参数,合理的设置配置项可以达到高效使用 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 是否被复用了。
  对于 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给出了示例。

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
*/

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节省一半的内存。需要注意的是,设置了这个值并不表示最后得到的 bitmap 的格式一定与设置的inPreferredConfig相等。

格式 占用字节
ALPHA_8 1个字节
RGB_565 2个字节(每个像素需要16个bit来表示)
ARGB_4444 2个字节
ARGB_8888 4个字节
RGBA_F16 8个字节

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

//TODO

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

Bitmap相关问题

Bitmap.Config 值问题

  问题表象在解码图片时,通过inPreferredConfig设置了希望使用的色彩格式,最后得到的 bitmap 的Bitmap.Config值却不等于inPreferredConfig设置的格式。这一问题常见于色彩深度是16位的图片上。当我们指定使用Bitmap.Config.ARGB_8888解码,得到的 bitmap 的 Bitmap.Config 值却是Bitmap.Config.F16
  在inPreferredConfig的源码注释中是这样描述的:将会尽力使用设置的值去解码。因此也可以看出代码层面并没有保证一定按照设置的值,最终色彩格式的确定在 native 代码上面。

// Set the decode colorType
SkColorType decodeColorType = codec->computeOutputColorType(prefColorType);

...

SkColorType SkAndroidCodec::computeOutputColorType(SkColorType requestedColorType) {
    bool highPrecision = fCodec->getEncodedInfo().bitsPerComponent() > 8;
    switch (requestedColorType) {
        case kARGB_4444_SkColorType:
            return kN32_SkColorType;
        case kN32_SkColorType:
            break;
        case kAlpha_8_SkColorType:
            // Fall through to kGray_8.  Before kGray_8_SkColorType existed,
            // we allowed clients to request kAlpha_8 when they wanted a
            // grayscale decode.
        case kGray_8_SkColorType:
            if (kGray_8_SkColorType == this->getInfo().colorType()) {
                return kGray_8_SkColorType;
            }
            break;
        case kRGB_565_SkColorType:
            if (kOpaque_SkAlphaType == this->getInfo().alphaType()) {
                return kRGB_565_SkColorType;
            }
            break;
        case kRGBA_F16_SkColorType:
            return kRGBA_F16_SkColorType;
        default:
            break;
    }

    // F16 is the Android default for high precision images.
    return highPrecision ? kRGBA_F16_SkColorType : kN32_SkColorType;
}

  假如我们一定要得到格式为Bitmap.Config.ARGB_8888的 bitmap,那该如何呢?最简单的方式就是降低色彩深度的精度,一个像素使用四个字节表示。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap aBitmap = BitmapFactory.decodeFile("imagePath", options);
Log.i(TAG, "aBitmap config = " + aBitmap.getConfig());
final int[] pixels = new int[aBitmap.getWidth() * aBitmap.getHeight()];
aBitmap.getPixels(pixels, 0, aBitmap.getWidth(), 0, 0, aBitmap.getWidth(), aBitmap.getHeight());

Bitmap bBitmap = Bitmap.createBitmap(aBitmap.getWidth(), aBitmap.getHeight(), Bitmap.Config.ARGB_8888);
bBitmap.copyPixelsFromBuffer(IntBuffer.wrap(pixels));
Log.i(TAG, "bBitmap config = " + bBitmap.getConfig());

参考链接

posted @ 2021-05-15 01:43  zxzhang  阅读(1727)  评论(1编辑  收藏  举报