概述
一、引言
- Drawable:通用的图形对象,用于装载常用格式的图像,既可以是PNG,JPG这样的图像, 也是前面学的那13种Drawable类型的可视化对象!我们可以理解成一个用来放画的——画框!
- Bitmap(位图):我们可以把他看作一个画架,我们先把画放到上面,然后我们可以 进行一些处理,比如获取图像文件信息,做旋转切割,放大缩小等操作!
- Canvas(画布):如其名,画布,我们可以在上面作画(绘制),你既可以用Paint(画笔), 来画各种形状或者写字,又可以用Path(路径)来绘制多个点,然后连接成各种图形!
- Matrix(矩阵):用于图形特效处理的,颜色矩阵(ColorMatrix),还有使用Matrix进行图像的 平移,缩放,旋转,倾斜等!
在Android中我们使用图像经常涉及到Bitmap,而Bitmap使用不当容易引发Out Of Memory(内存溢出),接下来我们就来谈及如何优化处理Bitmap,防止引发内存溢出问题
先来了解下Bitmap占用内存大小的计算公式为:图片宽度×图片高度×一个像素点所占字节数 ,所以减小这三个参数的任一值都可减小bitmap所占的内存大小
也可以通过调用Bitmap.getAllocationByteCount() 方法来查看Bitmap所占内存大小
二、减小内存占用
1.BitmapFactory.Options.inSampleSize
inSampleSize 是BitmapFactory.Options的一个属性,改变该配置即改变图片的宽高,如果设置为大于1的值(小于1的值即为1),则请求解码器对原始图像进行二次采样,返回较小的图像以节省内存。
例如,inSampleSize = 4返回的图像为原始宽度/高度的1/4,像素数目的1/16。
该属性配合inJustDecodeBounds属性使用,如果设置为true,则解码器将返回null(无位图),但out...
仍将设置字段,从而允许调用者查询位图而不必为其像素分配内存。
private fun sampleCompress(requestWidth: Int, requestHeight: Int) {
val options = BitmapFactory.Options()
// 不分配内存空间,仅计算图片尺寸
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.mipmap.timg, options)
Log.d(TAG, "bitmap outWidth:${options.outWidth}")
Log.d(TAG, "bitmap outHeight:${options.outHeight}")
// 根据宽高要求,计算缩放倍数
var sampleSize = 1
if (requestWidth < options.outWidth || requestHeight < options.outHeight) {
sampleSize = max(options.outWidth * 1.0/requestWidth, options.outHeight * 1.0/requestHeight).toInt()
}
options.inJustDecodeBounds = false
options.inSampleSize = sampleSize
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.timg, options)
logBmInfo(bitmap)
}
若原本Bitmap宽高各为1000,要求宽高各为500 ,则得出inSampleSize为2,执行日志结果如下,根据计算可知,原Bitmap占用内存大小为4000000B,但是经过使用inSampleSize属性压缩宽高,从而减小为原先1/4的内存占用。
2.BitmapFactory.Options.inPreferredConfig
inPreferredConfig 是BitmapFactory.Options的一个属性,其默认值为 Bitmap.Config.ARGB_8888,改变该配置,可改变一个像素点占用字节数
该属性常用配置如下,A代表透明度,R代表红色,G代表绿色,B代表蓝色:
ALPHA_8 表示8位Alpha位图,A占8位,没有颜色,只有透明度 ,占用1个字节
ARGB_4444(已废弃) 表示16位ARGB位图,即A占4位,R占4位,G占4位,B占4位,共占用2个字节
ARGB_8888 表示32位ARGB位图,即A占8位,R占8位,G占8位,B占8位,共占用4个字节
RGB_565 表示16位RGB位图,即R占5位,G占6位,B占5位,没有透明度,共占用2个字节
private fun argbCompress() {
val bm1 = BitmapFactory.decodeResource(resources, R.mipmap.timg)
logBmInfo(bm1)
val options = BitmapFactory.Options()
// 设配置为RGB_565
options.inPreferredConfig = Bitmap.Config.RGB_565
val bm2 = BitmapFactory.decodeResource(resources, R.mipmap.timg, options)
logBmInfo(bm2)
}
执行日志结果如下,可以看到优化后的Bitmap内存占用为未优化Bitmap大小的一半 ,长度和宽度没发生变化。RGB_565对不要求透明度的图来说视觉影像不大。
3.误区:Bitmap.compress(...)
Bitmap的compress(Bitmap.CompressFormat format, int quality, OutputStream stream)方法是将位图的压缩版本写入指定的输出流,该方法可能需要几秒钟才能完成,因此最好从辅助线程中调用它。(注:并非所有格式都直接支持所有位图配置,因此从BitmapFactory返回的位图可能具有不同的位深,并且可能丢失了每个像素的alpha值(例如JPEG仅支持不透明的像素))。
参数:format是压缩图像格式,quality是压缩质量(根据format不同,quality压缩效果也不同),stream是写入压缩数据的输出流
- format为Bitmap.CompressFormat.JPEG,根据quality 0-100压缩
- format为Bitmap.CompressFormat.PNG,则quality参数就会失效,因为PNG图片是无损的,无法压缩
- format为Bitmap.CompressFormat.WEBP,google 推出的图片格式,它会比 JPEG 更加省空间,根据quality 0-100压缩
该方法压缩损失的是颜色精度,所需的存储空间变小了,但使用压缩后的流重新生成Bitmap,并不会改变bitmap占用内存的大小,因为bitmap的宽高未改变,而且Bitmap.Config未改变即一个像素所占用的字节数也未改变,所以最终bitmap所占的内存并没有改变。
private fun bitmapCompress(bitmap: Bitmap){
val out = ByteArrayOutputStream()
Log.d(TAG, "—————————— JPEG ——————————")
bitmap.compress(Bitmap.CompressFormat.JPEG, 30, out)
Log.d(TAG, "out byte count:${out.size()}")
val jpegArray = out.toByteArray()
val jpeg = BitmapFactory.decodeByteArray(jpegArray, 0, jpegArray.size)
logBmInfo(jpeg)
out.reset()
Log.d(TAG, "—————————— PNG ——————————")
bitmap.compress(Bitmap.CompressFormat.PNG, 30, out)
Log.d(TAG, "out byte count:${out.size()}")
val pngArray = out.toByteArray()
val png = BitmapFactory.decodeByteArray(pngArray, 0, pngArray.size)
logBmInfo(png)
out.reset()
Log.d(TAG, "—————————— WEBP ——————————")
bitmap.compress(Bitmap.CompressFormat.WEBP, 30, out)
Log.d(TAG, "out byte count:${out.size()}")
val webpArray = out.toByteArray()
val webp = BitmapFactory.decodeByteArray(webpArray, 0, webpArray.size)
logBmInfo(webp)
}
执行日志结果如下,quality为30进行压缩,JPEG格式和WEBP所占存储空间变小了(不一定WEBP格式所占存储空间小于JPEG格式),而PNG格式并未压缩。三者的流重新解码成bitmap,可见bitmap所占内存大小并未发生变化。
压缩后的流重新解码生成bitmap(因quality为30效果不明显改用1),展示出来可以看见PNG格式无影响,JPEG格式和WEBP格式图片质量明显变差了。
三、复用
除了减少bitmap对内存的占用,还有方案来优化,即重用已经占用内存的bitmap空间或使用现有的bitmap。
1.BitmapFactory.Options.inBitmap
inBitmap是BitmapFactory.Options的一个属性,可以通过设置该属性来重用已经占用内存的bitmap空间
Bitmap重用有一定限制:
-
在Android4.4之前,只能重用相同大小的Bitmap内存区域
-
在4.4之后可以重用任何Bitmap的区域,只要这块内存比将要分配内存的Bitmap大就可以
-
重用的bitmap是要可变的
以下代码仅适配Android4.4之后,先通过设置 options.inJustDecodeBounds为true,来查询需加载的bitmap宽高,判断reuseBitmap是否符合重用,若符合则将其赋值给options.inBitmap属性,最终得到想要的bitmap,即重用了reuseBitmap的内存空间。
private fun getBitmap(): Bitmap {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.mipmap.timg, options)
// 判断是否满足重用条件,这里就假设Bitmap.Config为ARGB_8888来计算内存大小
if (reuseBitmap.allocationByteCount >= options.outWidth * options.outHeight * 4) {
// reuseBitmap为可变的重用bitmap
options.inBitmap = reuseBitmap
}
options.inJustDecodeBounds = false
return BitmapFactory.decodeResource(resources, R.mipmap.timg, options)
}
2.LruCache
在使用RecyclerView时,itemView中含有图片,滑动导致bitmap不断重新创建,从而浪费内存空间。
此场景,可使用LruCache来缓存bitmap,再次需要时从缓存取出即可无需重新创建。
private val memoryCache = object : LruCache<String, Bitmap>(4*1024*1024) { // 缓存4M图片
override fun sizeOf(key: String, value: Bitmap): Int {
// 告知lruCache bitmap所占内存大小
return value.allocationByteCount
}
}
fun putBitmap(key: String, bitmap: Bitmap) {
memoryCache.put(key, bitmap)
}
fun getBitmap(key: String): Bitmap? {
return memoryCache.get(key)
}
四、查看超清大图
可使用BitmapRegionDecoder,详情查看Android 高清加载巨图方案 拒绝压缩图片
五、总结
本文介绍了决定bitmap的内存占用大小的因素,以及如何优化和复用bitmap,相信处理好bitmap,能大大减少相关OOM事件的发生。当然,现如今有许多三方库(如Glide)能帮我们做好这些事情,但我们也应该知道优化策略。
参考文献:
- BitmapFactory.Options官方文档
- Bitmap(位图)全解析
最后
以上就是专注可乐为你收集整理的Android Bitmap优化,防止内存溢出 一、引言三、复用四、查看超清大图五、总结的全部内容,希望文章能够帮你解决Android Bitmap优化,防止内存溢出 一、引言三、复用四、查看超清大图五、总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复