一. Bitmap的内存管理的演变过程
Android 2.3.3(API 10)及更低版本,Bitmap的像素数据存在是本地内存(Native)中,这些像素数据与存储在Dalvik堆中的Bitmap本身是分开的;本地内存中的像素数据何时会释放无法监测,这就很容易导致应用超出内存限制(OOM)从而崩溃。建议使用recycler()方法,使应用尽快释放内存。Android 3.0(API 11)~Android 7.1(API 25)版本,Bitmap本身与其像素数据一起存储在Dalvik堆上。Andorid 8.0(API 26)及更高版本,Bitmap的像素数据的存储在原生堆(Native)中。
二、Bitmap在内存中的大小,如何计算
2.1 像素的存储格式 — Bitmap.Config(色深)
Bitmap.Config.ALPHA_8: 颜色信息组成:Alpha(透明度),占8bit = 1byteBitmap.Config.RGB_565: 颜色信息组成:R(Red)、G(Green)、B(Blue),占16bit = 2byteBitmap.Config.ARGB_4444: 颜色信息组成:A(Alpha)、R(Red)、G(Green)、B(Blue),占16bit = 2byteBitmap.Config.ARGB_8888: 颜色信息组成:A(Alpha)、R(Red)、G(Green)、B(Blue),占32bit = 4byte
2.2 Btimap在内存中的大小,如何计算
Btimap在内存中的计算公式:
长 * 宽 * 单个像素的大小比如
1000 * 1000像素,存储格式Bitmap.Config.ARGB_8888的bitmap占用的内存1000 * 1000 * 4 = 4000000 byte,约等于3.8M
上面的公式适用于非drawable/mipmap下的图片的计算,比如assets目录下以及手机本地图片。
如果是drawable/mipmap资源目录下的图片,计算方式就会有些许不同了,长宽方向上的像素值会乘一个scale(缩放系数)
drawable/mipmap的Btimap在内存中的计算公式:(长 * scale) * (宽 * scale) * 单个像素的大小
scale(缩放系数) = 当前设备屏幕密度 / 图片所在drawable(or mipmap)目录对应屏幕密度
万字不如一图,下面用代码来验证下
把上面这张图分别放在assets、drawable、mipmap-mdpi、mipmap-hdpi、mipmap-xhdpi、mipmap-xxhdpi、mipmap-xxxhdpi文件夹下,运行的测试机屏幕密度是480
测试代码:
运行结果:
结果分析:
BitmapFactory默认decode的存储格式为Bitmap.Config.ARGB_8888
那么Bitmap原始大小:1406 * 580 * 4 = 3261920
查看运行结果发现只有assets 和 xxhdip 目录下的值没有经过缩放,更严谨的说assets目录下没有缩放,xxhdip目录下的缩放系数是1。
三、Bitmap的复用
Android 3.0(Api 11)开始,Bitmap引入了BitmapFactory.Options.inBitmap字段,如果设置了inBitmap,那么采用Options对象的解码方法会在加载内容的同时尝试复用现有的Bitmap.
Bitmap的复用是有限制的,Android 4.4(Api 19)前后也有很大的差别,看官方说明:
Android 4.4开始
Android 4.4之前
先看怎么用,看代码:
- 不复用
- 复用
结果分析:
分析之前先介绍下BitmapFactory.Options.inMutable、getByteCount()、getAllocationByteCount(),还是看官方介绍:
BitmapFactory.Options.inMutable
getByteCount()
getAllocationByteCount()
结果分析:
- 不复用的情况,每创建一个bitmap都会分配一个对应大小的内存,且
getByteCount()=getAllocationByteCount() - 复用的情况,bitmap2的
getAllocationByteCount()是大于getByteCount()的,说明它是复用的bitmap1的。
单看打印的结果说明不了内存使用的情况,可以使用Android Studio的Profiler功能自行查看。
问题来了:如果使用了bitmap的复用,且解码的bitmap大小比被复用的bitmap还要大,会怎么样?
答:会crash,抛
throw new IllegalArgumentException("Problem decoding into existing bitmap")异常,因为不够。
四、Bitmap的缓存
最常见的场景就是列表中的图片显示,随着不断的上下滑动,Bitmap可能会在短时间内反复创建、销毁;这种情况下使用缓存可以有效的减少GC频率保证图片加载效率,提高页面的响应速度和流畅性。
google提供的缓存策略是LruCache,具体使用自行google。
最最最常用的缓存方式是Glide,一行代码闯天下,你只管码,缓存交给它。Glide的源码是非常值得且有必要认真仔细阅读研究的。
五、Bitmap的压缩
凡是涉及bitmap的地方,关于bitmap的压缩必然是一个不可忽略的问题,对bitmap合理的压缩,才能保证app的流畅性。
5.1 色深与位深
-
色深:色彩的深度,指每一个像素点用多少bit存储
ARGB值,属于图片自身的一种属性。色深用来衡量一张图片的色彩处理能力(即色彩丰富程度)。上述介绍Bitmap.Config参数的值就是色深,它们的色深分别是8-bit、16-bit、24-bit、32-bit。色深是数字图像的参数。 -
位深:在记录数字图像时,计算机实际上使用每个像素需要的二进制数值位数来表示的。当这些数据按照一定的编排方式被记录在计算机中,就构成了一个数字图像的计算机文件。
每一个像素在计算机中所使用的位数就是‘位深度’,位深是物理硬件参数,主要用来存储。
比如一张100 * 100,色深是32bit(ARGB_8888),保存时位深度是24位的图片
- 该图片在内存中的大小:100 * 100 * (32/8) byte
- 该图片文件的大小:100 * 100 * (24/8) * 压缩率 byte
拓展小知识:
24位颜色可称之为真彩色,色深度是24,它能组合成2的24次幂种颜色,即:16777216种颜色,超过了人眼能够分辨的颜色数量。
5.2 bitmap的压缩
关于bitmap的压缩,大方向上说就2种:质量压缩、尺寸压缩。
5.2.1 质量压缩
质量压缩是通过算法改变图片的位深及透明度等来达到改变图片大小的目的,并不会减少图片的像素数,经过质量压缩的图片文件大小会变小,但解码成bitmap后所占用的内存大小不变;
bitmap中提供了质量压缩的方法 —— compress()方法,该方法耗时,在子线程调用。
基本使用:
val outputStream = ByteArrayOutputStream()
// quality 为0~100,0表示最小体积,100表示最高质量,对应体积也是最大
bitmap.compress(Bitmap.CompressFormat.JPEG, quality , outputStream)
代码验证:
结果分析:
图片文件大小从原来的151255 byte减小到8143 byte,解码成bitmap所占用的内存大小依然都是3261920 byte。
5.2.1 尺寸压缩
尺寸压缩则是通过算法减少图片的像素数,和裁切图片有着本质的区别,虽然2者都会减少图片像素,尺寸压缩尽可能保留了原图所表现的全部信息,裁切则是只保留原图上的一部分信息。
针对图片尺寸的修改其实就是一个图像重新采样的过程,放大图像称为上采样(upsamping),缩小图像称为下采样(downsampling),这里重点讨论下采样。
在 Android 中图片重采样提供了2种方法,一种叫做邻近采样(Nearest Neighbour Resampling),另一种叫做双线性采样(Bilinear Resampling)。
邻近采样(Nearest Neighbour Resampling)
Android 中常用的压缩方法之一,使用代码:
val options = BitmapFactory.Options()
// 或者 inDensity 搭配 inTargetDensity 使用,算法和 inSampleSize 一样
options.inSampleSize = 2
val bitmap = BitmapFactory.decodeFile("/sdcard/test.png")
val compress = BitmapFactory.decodeFile("/sdcard/test.png", options)
邻近采样的压缩效果:
结果分析:
图是每个像素红绿相间的图片,可以看到处理之后的图片已经完全变成了绿色,接着来看看 inSampleSzie的官方描述:
从官方的解释中我们可以看到 x(x 为 2 的倍数)个像素最后对应一个像素,由于采样率设置为 1/2,所以是两个像素生成一个像素。邻近采样的方式比较粗暴,直接选择其中的一个像素作为生成像素,另一个像素直接抛弃,这样就造成了图片变成了纯绿色,也就是红色像素被抛弃。
邻近采样采用的算法也叫做邻近点插值算法。
双线性采样(Bilinear Resampling)
双线性采样Android中的使用方式有2种:
val bitmap = BitmapFactory.decodeFile("/sdcard/test.png")
val compress = Bitmap.createScaledBitmap(bitmap, bitmap.width /2, bitmap.height /2, true)
// 或者直接使用 matrix 进行缩放
val bitmap = BitmapFactory.decodeFile("/sdcard/test.png")
val matrix = Matrix()
matrix.setScale(0.5f, 0.5f)
val compress = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
双线性采样的压缩效果:
可以看到处理之后的图片不是像邻近采样一样纯粹的一种颜色,而是两种颜色的混合。双线性采样使用的是双线性內插值算法,这个算法不像邻近点插值算法一样,直接粗暴的选择一个像素,而是参考了源像素相应位置周围 2x2 个点的值,根据相对位置取对应的权重,经过计算之后得到目标图像。
双线性内插值算法在图像的缩放处理中具有抗锯齿功能, 是最简单和常见的图像缩放算法,当对相邻 2x2 个像素点采用双线性內插值算法时,所得表面在邻域处是吻合的,但斜率不吻合,并且双线性内插值算法的平滑作用可能使得图像的细节产生退化,这种现象在上采样时尤其明显。
占用内存上的验证:
结果分析:
宽高上像素分别是原来的1/2,所占用的内存是原来的1/4.
有关更多
尺寸压缩方式请移步QQ音乐大佬的文章——Android中图片压缩分析(下)
六、总结
本文分别从
1. Bitmap的内存管理的演变过程、2. Bitmap在内存中的大小,如何计算、3. Bitmap的复用、4. Bitmap的缓存、5. Bitmap的压缩, 5个方面介绍了bitmap,只有更加全面的了解了bitmap,才能在开发中考虑到更深的一个层次,编写出更加优秀的代码。
最后留一个关于bitmap的高频率面试题,也是文章中缺少的一部分:
问:如何加载一张超大大大图却不撑爆内存(如何实现图片的分片加载)?
提示:
BitmapRegionDecoder类,如果你现在对这个问题还模棱两可,就现在,打开Android Studio,google一篇博客,亲自动手尝试一下吧。
【参考文章/资料】
如果文章对你有帮助,点个赞再走呗
如果文章中存在错误,还望评论区指出
一起成长,共同进步