一. 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 = 1byte
Bitmap.Config.RGB_565
: 颜色信息组成:R(Red)、G(Green)、B(Blue)
,占16bit = 2byte
Bitmap.Config.ARGB_4444
: 颜色信息组成:A(Alpha)、R(Red)、G(Green)、B(Blue)
,占16bit = 2byte
Bitmap.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一篇博客,亲自动手尝试一下吧。
【参考文章/资料】
如果文章对你有帮助,点个赞再走呗
如果文章中存在错误,还望评论区指出
一起成长,共同进步