无论是 API 26 前还是之后的回收实现,释放 Native 层的 Bitmap 对象的思想都是去监听 Java 层的 Bitmap 是否被释放,一旦当 Java 层的 Bitmap 对象被释放则立即去释放 Native 层的 Bitmap 。只不过 API 26 以前是基于 Java 的 GC 机制,而 API 26 后是注册 native 的 Finalizer 方法,更详细的分析可查看: 图形图像处理 - 我们所不知道的 Bitmap。
BitmapFactory.OptionsBitmapFactory.Options 是 BitmapFactory 从不同的输入源中创建 Bitmap 对象的控制参数。
inBitmapAndroID 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
。@Nullablepublic static Bitmap decodefile(@NonNull String path@R_502_6889@) { Bitmap bitmap; BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodefile(path@R_502_6889@, 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(path@R_502_6889@, options); // 检查是否复用成功 if (bitmap == options.inBitmap) { Log.i(TAG, "decodefile: inBitmap reuse successfully"); } } catch (Exception e) { Log.e(TAG, "decodefile", e); bitmap = BitmapFactory.decodefile(path@R_502_6889@); } 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 = trueBitmapFactory.decodefile(IMAGE_PATH, options)Log.i(TAG, "wIDth = ${options.outWIDth}, height = ${options.outHeight}, mimeType = ${options.outMimeType}")val imageWIDth = options.outWIDthval imageHeight = options.outHeightoptions.inJustDecodeBounds = falsefor (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/jpegbitmap wIDth = 4000, height = 3000, wIDth for inSampleSize = 1, height for inSampleSize = 1bitmap wIDth = 2000, height = 1500, wIDth for inSampleSize = 2, height for inSampleSize = 2bitmap wIDth = 1333, height = 1000, wIDth for inSampleSize = 3, height for inSampleSize = 3bitmap wIDth = 1000, height = 750, wIDth for inSampleSize = 4, height for inSampleSize = 4bitmap 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
节省一半的内存。inDensityAlpha_8 -> 1个字节
RGB_565 -> 2个字节(每个像素需要16个bit来表示)
ARGB_4444 -> 4个字节
RGBA_F16 -> 8个字节
ARGB_8888 -> 4个字节
图片所在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图不受这个标志位的影响,始终会被缩放。
计算公式:
(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
对图片进行采样。一般图片的宽高都比我们显示图片的区域大很多,因此我们不必以原图尺寸解码图片,通过采样算法,计算一个合理的采样值,在解码时对图片进行下采样。可参考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 EfficientlyCaching BitmapsManaging Bitmap MemoryAndroid DisplayingBitmaps SampleGlide bitmap_recycle 总结 以上是内存溢出为你收集整理的Android Bitmap的使用及优化全部内容,希望文章能够帮你解决Android Bitmap的使用及优化所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)