Android 性能优化-Bitmap知识梳理 & 高效加载
一、Bitmap 占用内存计算
bitmap 的内存计算可由下面的计算公式得出来:
Bitmap 内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× ( 设备dpi / 资源目录dpi ) ^ 2 × 单个像素的字节大小
其中单个像素的字节大小由 Bitmap 的一个可配置的参数 Config 来决定。Bitmap 中存在一个枚举类 Config,定义了 Android 中支持的 Bitmap 配置:
Android 系统中,默认 Bitmap 加载图片使用24位真彩色(ARGB_8888)模式。
资源目录dpi 跟图片存放的资源文件的目录有关系:
根据我们上面的公式计算来验证一下:
res\drawable-xhdpi:1920 * 2304 * (272/ 160)^ 2 * 4 = 17280000
res\drawable-xxxhdpi:1920 * 2304 * (272/ 640)^ 2 * 4 = 1080000
res\drawable-xxhdpi:1920 * 2304 * (272/ 480)^ 2 * 4 = 1920000
二、BitMap 优化
Bitmap内存优化从下面四个方面进行优化:
- 编码
- 采样
- 复用
- 匿名共享区
2.1 编码
在第一节中已经列举出的枚举配置中存在几种不同的配置:
其中,A代表透明度;R代表红色;G代表绿色;B代表蓝色。
- ALPHA_8
表示8位Alpha位图,即A=8,一个像素点占用1个字节,它没有颜色,只有透明度 - ARGB_4444
表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占4+4+4+4=16位,2个字节 - ARGB_8888
表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素点占8+8+8+8=32位,4个字节 - RGB_565
表示16位RGB位图,即R=5,G=6,B=5,它没有透明度,一个像素点占5+6+5=16位,2个字节
也即是说我们可以通过改变图片格式,来改变每个像素占用字节数,来改变占用的内存,看下面代码
fun compressBitmap() {
// 不获取图片,不加载到内存,只返回图片的宽高
val mOptions = BitmapFactory.Options()
mOptions.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.drawable.yasuo, mOptions)
// 获取图片的宽高
val mOrigWidth = mOptions.outWidth
val mOrigHeight = mOptions.outHeight
Log.e(TAG, "原图:mOrigWidth = $mOrigWidth, mOrigHeight = $mOrigHeight")
// 图片压缩-更改图片格式
mOptions.inPreferredConfig = Bitmap.Config.RGB_565
mOptions.inJustDecodeBounds = false
val mTestBitmap = BitmapFactory.decodeResource(resources, R.drawable.yasuo, mOptions)
Log.e(TAG, "===========================压缩后=================================")
Log.e(TAG, "mTestBitmap width = ${mTestBitmap.width}, height = ${mTestBitmap.height}")
Log.e(TAG,"mTestBitmap byteCount = ${mTestBitmap.byteCount}")
Log.e(TAG,"mTestBitmap allocationByteCount = ${mTestBitmap.allocationByteCount}")
}
从日志中可以看出从改变bitmap的格式可以有效的降低其占用内存大小。从 ARGB_8888 切换到 RGB_565 减少约 2 倍的内存大小。
2.2 采样
我们了解到了计算bitmap的占用内存的方法 ,是以bitmap的宽高和每个像素占用的字节数决定的。图片的大小就是bitmap的宽高,按公式我们可以缩减bitmap的宽高来达到压缩图片占用内存的目的,看下面代码,以缩减宽高来达到压缩的目的
fun decodeSampleSizeBitmapWithRes(res: Resources, resId: Int, reqWidth: Int, reqHeight: Int) {
val mOption = BitmapFactory.Options()
mOption.inJustDecodeBounds = true
BitmapFactory.decodeResource(res, resId, mOption)
mOption.inSampleSize = calculateInSampleSize(mOption, reqWidth, reqHeight)
Log.e("mTestBitmap", "sampleSize:${calculateInSampleSize(mOption, reqWidth, reqHeight)}")
mOption.inJustDecodeBounds = false
val mCompressBitmap = BitmapFactory.decodeResource(res, resId, mOption)
Log.e(TAG, "===========================压缩后=================================")
Log.e(TAG, "mTestBitmap width = ${mCompressBitmap.width}, height = ${mCompressBitmap.height}")
Log.e(TAG, "mTestBitmap byteCount = ${mCompressBitmap.byteCount}")
Log.e(TAG, "mTestBitmap allocationByteCount = ${mCompressBitmap.allocationByteCount}")
}
这种我们根据BitmapFactory 的采样率进行压缩 设置采样率,不能小于1 假如是2 则宽为之前的1/2,高为之前的1/2,一共缩小1/4 一次类推,我们看到log ,确实起到了压缩的目的
2.2 复用
重复加载图片资源耗费太多资源(CPU、内存 & 流量),我们可以使用三级缓存机制,即内存缓存、本地缓存(硬盘、数据库、文件)、网络缓存。当加载 Bitmap 图片资源时,先从内存缓存中寻找;若内存缓存中没有,则从本地缓存中查找;若本地缓存没有,则从网络中加载寻找。
另外还可以使用软引用(内存空间不足时才回收这些对象的内存)的方式实现内存敏感的高速缓存。
同时还可以开启 inBitmap 这个属性
inBitmap 属性的作用:
不使用这个属性,你加载三张图片,系统会给你分配三份内存空间,用于分别储存这三张图片
如果用了inBitmap这个属性,加载三张图片,这三张图片会指向同一块内存,而不用开辟三块内存空间
inBitmap的限制
- 3.0-4.3
复用的图片大小必须相同
编码必须相同 - 4.4以上
复用的空间大于等于即可
编码不必相同
不支持WebP
图片复用,这个属性必须设置为true; options.inMutable = true;
2.3 压缩
一个是上面讲到的采样压缩,另外一种则是下面的 质量压缩
质量压缩
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//quality 为0~100,0表示最小体积,100表示最高质量,对应体积也是最大
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
在上述代码中,我们选择的压缩格式是 CompressFormat.JPEG,除此之外还有两个选择:CompressFormat.PNG, PNG 格式是无损的,它无法再进行质量压缩,quality 这个参数就没有作用了,会被忽略,所以最后图片保存成的文件大小不会有变化;CompressFormat.WEBP ,这个格式是 google 推出的图片格式,它会比 JPEG 更加省空间,经过实测大概可以优化 30% 左右。
参考文章:
Android BitMap 优化
Android Bitmap 的高效加载解析
性能优化:Android中Bitmap内存大小优化的几种常见方式