无论是 Api 26 前还是之后的回收实现,释放 Native 层的 Bitmap 对象的思想都是去监听 Java 层的 Bitmap 是否被释放,一旦当 Java 层的 Bitmap 对象被释放则立即去释放 Native 层的 Bitmap 。只不过 Api 26 以前是基于 Java 的 GC 机制,而 Api 26 后是注册 native 的 Finalizer 方法,更详细的分析可查看: 图形图像处理 - 我们所不知道的 Bitmap。
BitmapFactory.Options 是 BitmapFactory 从不同的输入源中创建 Bitmap 对象的控制参数。
BitmapFactory.Options.inBitmap
字段java.lang.IllegalArgumentException
异常。对于被复用的 bitmap 要求其是可修改的(mutable),并且对于被复用的 bitmap 将会保持其可修改的属性,即使 decode 的资源将会导致 bitmap 变成不可修改的(immutable)。由于上述的限制存在,因此可能导致 decode 失败。因此不应该假定复用的 bitmap 是始终有效的,通过 decode 返回的 bitmap,检查其 inBitmap 字段可以确定 bitmap 是否被复用了。getAllocationByteCount()
大于等于要解码资源的getByteCount()
的bitmap。jpeg
或png
格式的图片inSampleSize
字段设置为1,也就是不支持采样。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
}
ARGB_8888
进行解码。对于不同的配置,其每个像素需要的字节数也不一样。通常在不需要 alpha 通道的场景下,选择RGB_565
进行解码,这样能比选择ARGB_8888
节省一半的内存。ALPHA_8 -> 1个字节
RGB_565 -> 2个字节(每个像素需要16个bit来表示)
ARGB_4444 -> 4个字节
RGBA_F16 -> 8个字节
ARGB_8888 -> 4个字节
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图不受这个标志位的影响,始终会被缩放。
计算公式:
(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 在应用中一般是导致 OOM 的几大原因之一,如何减少解码图片导致的 OOM 及 Bitmap 的创建回收导致的内存抖动就显得尤为重要。Bitmap 内存优化一般有以下几个手段:
Options.inSampleSize
对图片进行采样。一般图片的宽高都比我们显示图片的区域大很多,因此我们不必以原图尺寸解码图片,通过采样算法,计算一个合理的采样值,在解码时对图片进行下采样。Options.inBitmap
对图片进行复用。图片复用有两个好处,一个是加快图片解码速度,减少 Bitmap 创建耗时;另一个则是减少频繁申请和销毁 Bitmap 导致的内存抖动。在实际使用中,可建立 BitmapPool,每次需要使用 Bitmap 时,从 BitmapPool 申请符合要求的 Bitmap 内存,当 Bitmap 不需要使用的,放回 BitmapPool。详细实现可参考Glide BitmapPool。Options.inPreferredConfig
可选择Bitmap.Config.RGB_565
,相比较于默认的Bitmap.Config.ARGB_8888
,一个像素只需要两个字节,整体内存可节省一半。原文:https://www.cnblogs.com/ZhaoxiCheung/p/12041421.html