Android图片压缩终极版

时间:2022-11-16 19:06:40

做了几个移动项目了,每个项目都会涉及到图片压缩,之前也没来得及优化,经常出现OOM的情况,这几天重写了一下,现在简单分析一下,主要是从质量压缩和尺寸裁剪来着手(源码在文章末尾)。

1.基础知识

这里主要介绍Bitmap的基础,Bitmap在android中指的是一张图片,可以是png格式,jpg格式,也可以是其它常见的图片格式,那么如何加载一个图片?BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream和decodeByteArray从不同来源加载出一个Bitmap对象,最终的实现是在底层实现的。

BitMap在内存中占用的大小不仅与图片的像素和大小有关,还和设备的densityDpi和资源存放的位置(xhdpi,xxhdp…)有关,具体详细的知识请查阅Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?

那么如何高效加载Bitmap?
采用BitmapFactory.Options按照一定的采样率来加载所需尺寸的图片,因为imageview所需的图片大小往往小于图片的原始尺寸。BitmapFactory.Options的inSampleSize参数,即采样率。官方文档指出采样率的取值应该是2的指数,例如k,那么采样后的图片宽高均为原图片大小的 1/k。

BitmapFactory.Options中有个inSampleSize属性,可以理解为压缩比率。设定好压缩比率后,调用上面的decodexxxx()就能得到一个缩略图了。比如inSampleSize=4,载入的缩略图是原图大小的1/4。(inSampleSize一般是2的幂次方,系统会自动向下取证,当然这也不是绝对的,比如inSampleSize=7,系统会取4)

为了避免OOM异常,最好在解析每张图片的时候都先检查一下图片的大小,以下几个因素是我们需要考虑的:

a.预估一下加载整张图片所需占用的内存

b.为了加载这一张图片你所愿意提供多少内存

c.用于展示这张图片的控件的实际大小

d.当前设备的屏幕尺寸和分辨率

比如,你的ImageView只有128*96像素的大小,只是为了显示一张缩略图,这时候把一张1024*768像素的图片完全加载到内存中显然是不值得的。比如我们有一张2048*1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512*384像素。原本加载这张图片需要占用13M的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。

同时,我们也可以裁剪图片的大小来减少它在内存中的占用。

如何获取采样率?
下面是常用的获取采样率的代码片段:

public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
//只获取图片信息,不加载到内存中
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);

// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}

public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}

// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w= " + width + " h=" + height);
int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;

// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}

Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}

2.具体实现

/**
* 压缩相关配置参数
*/

private static final int COMPRESS_WIDTH = 480;//默认缩放宽度
private static final int COMPRESS_HEIGHT = 800;//默认缩放高度
private static final int COMPRESS_QUALITY = 100;//默认压缩质量

/**
* 压缩文件存放目录
*/

private final static String COMPRESS_PICTURE_DIRECTROY = "xzwc/compress/";
private static final String TAG = "ImageCompressUtil";

上面定义了一些压缩配置的基本参数,这几就不过多解释了。

    /**
* 压缩, TODO 每次压缩后保存始终都是相同文件
* @param origPath
* @return
*/

public static String compressSave(String origPath) {
// 计算图片
File origFile = new File(origPath);
Lg.i(TAG, "压缩 前文件大小:" + origFile.length() + ", 名称: " + origFile.getName());
Bitmap compressBmp = null;
// 压缩图片
compressBmp = getSmallBitmap(origPath);
// 保存压缩后的图片
String savePath = getSavePath();;
saveFile(compressBmp, savePath);
Lg.i(TAG, "压缩 后文件大小:" + new File(savePath).length() + ", 保存: " + savePath);
return savePath;
}
/**
* 根据路径获得图片并压缩返回bitmap用于显示
*
* @param imagesrc
* @return
*/

public static Bitmap getSmallBitmap(String filePath) {
// 1. 进行缩放压缩, 以480 * 800 读取图片,防止内存溢出
Bitmap compressBmp = compressBySize(decodeBitmap(filePath));

// 2. 进行质量压缩
ByteArrayOutputStream bos = new ByteArrayOutputStream();
compressBmp.compress(Bitmap.CompressFormat.JPEG, COMPRESS_QUALITY, bos);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());

//L.d("debug", "压缩后的流: " + bos.toByteArray().length);
return BitmapFactory.decodeStream(bis);
}
/**
* 读取图片
* @param filePath
* @return
*/

public static Bitmap decodeBitmap(String filePath) {
// 1. 采样率压缩
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);

// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, COMPRESS_WIDTH, COMPRESS_HEIGHT);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;

return BitmapFactory.decodeFile(filePath, options);
}
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w= " + width + " h=" + height);
int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
Lg.d(TAG, "inSampleSize " + inSampleSize);
return inSampleSize;
}
    /**
* 按尺图片压缩图片
* @param origBmp
* @param scale
* @return
*/

public static Bitmap compressBySize(Bitmap origBmp) {
float origW = origBmp.getWidth();
float origH = origBmp.getHeight();

float widthScale = 1, heightScale = 1;
if (origW > origH) {
widthScale = origW / (float)COMPRESS_HEIGHT;
heightScale = origH / (float)COMPRESS_WIDTH;
}
else {
widthScale = origW / COMPRESS_WIDTH;
heightScale = origH / COMPRESS_HEIGHT;
}
Lg.i(TAG, "widthScale,heightScale, mid "
+ widthScale + ", "
+ heightScale );
float sca = Math.max(heightScale, widthScale);
Lg.i(TAG, "缩放比例: " + sca);
float scaleW = origW, scaleH = origH;
if (sca > 1) {
scaleW = (origW / sca);
scaleH = (origH / sca);
}
return zoomImage(origBmp, (int)scaleW, (int)scaleH);
}
public static Bitmap zoomImage(Bitmap bgimage, int newWidth, int newHeight) {
// 获取这个图片的宽和高
int width = bgimage.getWidth();
int height = bgimage.getHeight();
// 创建操作图片用的matrix对象
Matrix matrix = new Matrix();
// 计算缩放率,新尺寸除原始尺寸
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// 缩放图片动作
matrix.postScale(scaleWidth, scaleHeight);
Bitmap bitmap = Bitmap.createBitmap(bgimage, 0, 0, width, height,
matrix, true);
return bitmap;
}

现在我们就完成了如下的三个过程。
a. 根据采样率inSampleSize来载入一个载入一个比imageview大一点的缩略图。
b. 按照尺寸来压缩图片(像素不能压缩到位)。
c. 质量压缩
d.保存图片

为啥要先从inSampleSize产生一个缩略图,而不是直接把原始的bitmap缩放图片呢?

因为如果要从原始的bitmap直接进行缩放的话,就需要将原始图片放入内存中,十分危险!!!现在通过计算得到一个缩略图,这个缩略图比原图可以小了很多,完全可以直接加载到内存中,这样再进行拉伸就比较安全了。

上述代码中主要逻辑都已经用注释标明,为了文章简洁明了,部分代码未粘贴出来,详细代码见底部源码

下面我们来看一下具体的实验效果:

Android图片压缩终极版

可以看到,在磁盘上(或者在流中)占用的空间远远小于Bitmap在内存中占用的空间。经过像素压缩和尺寸压缩的图片占用的内存会减少,但是质量压缩的图片占用的Bitmap内存空间不会减少。

源码下载