文章目录


两年前写过一篇文章,​​Android Bitmap 研究与思考(上篇)​​ 介绍了 bitmap 的基本概念,今天这篇文章就来看看 bitmap 的压缩问题

质量压缩

private fun compressQuality() {
//把 drawable 转成 bitmap
val bm = BitmapFactory.decodeResource(resources, R.drawable.a123)
val bos = ByteArrayOutputStream()
//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
bm.compress(Bitmap.CompressFormat.JPEG, 70, bos)
val bytes = bos.toByteArray()
//把数据流解码为 bitmap
val mSrcBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}

在解码的代码中,还可以这只解码参数 ​​Options​

public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) {

设置采样率

val options = BitmapFactory.Options()
//设置采样率,为2的阶乘
options.inSampleSize = 16

我们去解析一个图片,如果太大,就会OOM,我们可以设置压缩比例inSampleSize,但是这个压缩比例设置多少就是个问题,所以我们解析图片可以分为俩个步骤,


  • 第一步就是获取图片的宽高,这里要设置​​Options.inJustDecodeBounds=true​​,这时候decode的bitmap为null,只是把图片的宽高放在Options里.
  • 第二步就是设置合适的压缩比例 inSampleSize ,这时候获得合适的Bitmap

测量解码完成后的bitmap宽高,解码返回值为null,bitmap 不会加载至内存。

val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
//decodeByteArray 返回值为 null
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)

val realWidth = options.outWidth //bitmap的宽度
val realHeight = options.outHeight //bitmap的高度

总结:


  • bitmap.compress 质量压缩方案,不会降低bitmap 内存占用。如果保存到磁盘会降低占用空间。
  • BitmapFactory.decodeByteArray 解码方案,使用采样率 inSampleSize 采样参数大于 1 会降低内存占用,保存到磁盘会降低磁盘占用空间。

采样率压缩

private fun compressSampling() {
//定义解码参数
val options = BitmapFactory.Options()
//设置采样率,为2的阶乘
options.inSampleSize = 4
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.a123, options)
binding.image2.setImageBitmap(bitmap)
}

采样率压缩其原理其实也是缩放bitamp的尺寸,通过调节其inSampleSize参数,比如调节为2,宽高会为原来的1/2,内存变回原来的1/4.

小例子:当我们把 inSampleSize 设置为 256 的时候,看看什么效果。

Android Bitmap 研究与思考(中篇)_android

可以看到当采样率很大的时候,图片会变模糊。

矩阵缩放

private fun compressMatrix() {
val options = BitmapFactory.Options()
options.inSampleSize = 2
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.a123, options)

val matrix = Matrix()
matrix.setScale(0.25f, 0.25f) //缩放
matrix.setRotate(30f) //旋转
val srcBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}

放缩法压缩使用的是通过矩阵对图片进行裁剪,也是通过缩放图片尺寸,来达到压缩图片的效果,和采样率的原理一样。

RGB_565压缩

private void compressRGB565() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test, options);
}

这是通过压缩像素占用的内存来达到压缩的效果,一般不建议使用ARGB_4444,因为画质实在是辣鸡,如果对透明度没有要求,建议可以改成RGB_565,相比ARGB_8888将节省一半的内存开销。

createScaledBitmap 压缩

private void compressScaleBitmap() {
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.test);
mSrcBitmap = Bitmap.createScaledBitmap(bm, 600, 900, true);
bm = null;
}

将图片的大小压缩成用户的期望大小,来减少占用内存。

BitmapFactory.Options 属性介绍

Android Bitmap 研究与思考(中篇)_java_02

bitmap 保存为文件

/**
* 保存bitmap到本地
*/
private fun saveBitmap(bitmap: Bitmap, file: File) {
FileOutputStream(file).use {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
it.flush()
}
}
/**
* 保存bitmap到本地
*/
private fun saveBitmap(bitmap: Bitmap, file: File) {
val bos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos)
FileOutputStream(file).use {
it.write(bos.toByteArray())
}
}

文件转为 bitmap

/**
* 保存bitmap到本地
*/
private fun fileToBitmap(file: File): Bitmap {
FileInputStream(file).use {
return BitmapFactory.decodeStream(it)
}
}
/**
* 保存bitmap到本地
*/
private fun fileToBitmap(file: File): Bitmap {
FileInputStream(file).use {
val data = it.readBytes()
val option = BitmapFactory.Options()
option.inSampleSize = 2
return BitmapFactory.decodeByteArray(data, 0, data.size, option)
}
}
/**
* 保存bitmap到本地
*/
private fun fileToBitmap(file: File): Bitmap {
val option = BitmapFactory.Options()
option.inSampleSize = 2
return BitmapFactory.decodeFile(file.absolutePath, option)
}

高效加载大位图

大位图加载时的OOM问题,解决方式是通过 inSample 属性创建一个原位图的子采样版本以减低内存。那么这里的采样率 inSample 值如何选取最好呢?

这里我们利用官方推荐的采样率最佳计算方式:基本步骤就是:

①获取位图原尺寸

②获取ImageView即最终图片显示的尺寸

③依据两种尺寸计算采样率(或缩放比例)。

public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 位图的原宽高通过options对象获取
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {

final int halfHeight = height / 2;
final int halfWidth = width / 2;
//当要显示的目标大小和图像的实际大小比较接近时,会产生没必要的采样,先除以2再判断以防止过度采样
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}

return inSampleSize;
}

依据上面的最佳采样率计算方法,进一步可以封装出利用最佳采样率创建子采样版本再创建位图对象的方法,这里以从项目图片资源文件加载Bitmap对象为例:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {

final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
//因为inJustDecodeBounds为true,所以不会创建Bitmap对象只会扫描轮廓从而给options对象的宽高属性赋值
BitmapFactory.decodeResource(res, resId, options);

// 计算最佳采样率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

// 记得将inJustDecodeBounds属性设置回false值
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}

bitmap 转为 drawable

val bitmap = BitmapFactory.decodeResource(resources, R.drawable.a123)
val drawable = BitmapDrawable(resources, bitmap)

drawable 转为 bitmap

/**
* drawable转为bitmap
*/
private fun drawableToBitmap(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
var bitmapWidth = drawable.intrinsicWidth
var bitmapHeight = drawable.intrinsicHeight

bitmapWidth = max(bitmapWidth, 1) //最小为1
bitmapHeight = max(bitmapHeight, 1) //最小为1

val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas()
canvas.setBitmap(bitmap)
drawable.setBounds(0, 0, bitmap.width, bitmap.height)
drawable.draw(canvas)
return bitmap
}

调用:

val drawable = ResourcesCompat.getDrawable(resources, R.drawable.a123, null)
val bitmap = drawableToBitmap(drawable)

getResources().getDrawable() 过时的解决方法

Android Bitmap 研究与思考(中篇)_android_03

​public Drawable getDrawable(@DrawableRes int id)​​ 已经过时,推荐使用 ​​public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)​​ .

解决办法:

1、当你这个Drawable不受主题影响时
ResourcesCompat.getDrawable(getResources(), R.drawable.name, null);

2、当你这个Drawable受当前Activity主题的影响时
ContextCompat.getDrawable(getActivity(), R.drawable.name);

3、当你这个Drawable想使用另外一个主题样式时
ResourcesCompat.getDrawable(getResources(), R.drawable.name, anotherTheme);

view 获取bitmap 对象

private fun getBitmap(view: View): Bitmap {
val width = view.width
val height = view.height
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas()
canvas.setBitmap(bitmap)
view.draw(canvas)
return bitmap
}

ScrollView 获取 bitmap

private fun getBitmap(scrollVew: ScrollView): Bitmap {
val width = scrollVew.width
var height = 0
scrollVew.forEach {
height += it.height
}

val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas()
canvas.setBitmap(bitmap)
scrollVew.draw(canvas)
return bitmap
}

总结

以上5种就是我们常用的压缩方法了,这里的压缩也只是针对在运行加载的bitmap占用内存的大小。我们在做App内存优化的时候,一般可以从这两个方面入手,一个内存泄漏,另外一个是Bitmap压缩了,在要求像素不高的情况下,可以对Bitmap进行压缩,并且针对一些只使用一次的bitmap,要做好recycle的处理。