无论是 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