Android客户端获取相册图片多张一起上传服务器时,如果不进行图片处理,可能会导致内存泄漏问题。这时图片压缩就至关重要了。但是又不能影响图片的清晰度。
一.设置图片格式
Android目前常用的图片格式有png,jpeg和webp。
1.png:无损压缩图片格式,支持Alpha通道,Android切图素材多采用此格式。
2.jpeg:有损压缩图片格式,不支持背景透明,适用于照片等色彩丰富的大图压缩,不适合logo。
3.webp:是一种同时提供了有损压缩和无损压缩的图片格式,派生自视频编码格式VP8,从谷歌官网来看,无损webp平均比png小26%,有损的webp平均比jpeg小25%~34%,无损webp支持Alpha通道,有损webp在一定的条件下同样支持,有损webp在Android4.0(API 14)之后支持,无损和透明在Android4.3(API18)之后支持。
采用webp能够在保持图片清晰度的情况下,可以有效减小图片所占有的磁盘空间大小。
二.质量压缩
1.描述
质量压缩并不会改变图片在内存中的大小,仅仅会减小图片所占用的磁盘空间的大小,因为质量压缩不会改变图片的分辨率,而图片在内存中的大小是根据width*height*一个像素的所占用的字节数计算的,宽高没变,在内存中占用的大小自然不会变。
质量压缩的原理是通过改变图片的位深和透明度来减小图片占用的磁盘空间大小,所以不适合作为缩略图,可以用于想保持图片质量的同时减小图片所占用的磁盘空间大小。
另外,由于png是无损压缩,所以设置quality无效。
2.代码
/**
* 质量压缩图片
*/
private Bitmap qualityBitmap(Bitmap bitmap) {
if (null == bitmap || bitmap.isRecycled()) return null;
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream);
byte[] bitmapByte = byteArrayOutputStream.toByteArray();
return BitmapFactory.decodeByteArray(bitmapByte, 0, bitmapByte.length);
} catch (Exception ignored) {
}
return null;
}
3.源码
/**
* Write a compressed version of the bitmap to the specified outputstream.
* If this returns true, the bitmap can be reconstructed by passing a
* corresponding inputstream to BitmapFactory.decodeStream(). Note: not
* all Formats support all bitmap configs directly, so it is possible that
* the returned bitmap from BitmapFactory could be in a different bitdepth,
* and/or may have lost per-pixel alpha (e.g. JPEG only supports opaque
* pixels).
*
* @param format The format of the compressed image
* @param quality Hint to the compressor, 0-100. 0 meaning compress for
* small size, 100 meaning compress for max quality. Some
* formats, like PNG which is lossless, will ignore the
* quality setting
* @param stream The outputstream to write the compressed data.
* @return true if successfully compressed to the specified stream.
*/
@WorkerThread
public boolean compress(CompressFormat format, int quality, OutputStream stream) {
checkRecycled("Can't compress a recycled bitmap");
// do explicit check before calling the native method
if (stream == null) {
throw new NullPointerException();
}
if (quality < 0 || quality > 100) {
throw new IllegalArgumentException("quality must be 0..100");
}
StrictMode.noteSlowCall("Compression of a bitmap is slow");
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress");
boolean result = nativeCompress(mNativePtr, format.nativeInt,
quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
return result;
}
三.二分法压缩
1.思路
1.如果 传入图片字节大小,比要压缩的字节大小还小 直接返回不需要压缩。
2.否则 执行压缩
首先读取100%的图片ByteArrayOutputStream对象 然后获取其字节的大小
//首先读取100%的图片ByteArrayOutputStream对象
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
//然后获取其字节的大小
byteArrayOutputStream.size()
如果100%的图片ByteArrayOutputStream对象的字节大小大于要压缩的值 需要压缩
获取0%的图片ByteArrayOutputStream对象
通过二分法不断找到[0,100]的某个位置 然后拿到这个最佳位置position 最终还是通过
bitmap.compress(Bitmap.CompressFormat.JPEG, position, byteArrayOutputStream);方法
获取ByteArrayOutputStream对象对象 然后获取ByteArrayOutputStream对象的字节流返回即可
result = byteArrayOutputStream.toByteArray();
2.代码
/**
* 图片质量压缩,采用二分法
*
* @param bytes 原图字节流
* @param maxByteSize 压缩图片最大字节 K为单位
* 大致原理:bitmap.compress(Bitmap.CompressFormat.JPEG, midPosition, byteArrayOutputStream);方法
* 不断变化第二个参数 来压缩获取ByteArrayOutputStream对象 然后比较ByteArrayOutputStream对象的大小和要压缩的字节大小
*/
private byte[] compressByQuality(final byte[] bytes, final long maxByteSize) {
if (bytes == null || bytes.length == 0) return null;
//压缩图片最大字节
long maxSize = maxByteSize * 1024;
//图片字节流的大小
int srcSize = bytes.length;
//图片字节流的大小 小于等于 压缩图片最大字节 不需要压缩
if (srcSize <= maxByteSize) return bytes;
//图片字节流的大小 大于 压缩图片最大字节 二分法压缩
//1.根据传入的图片字节流获取Bitmap
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, srcSize);
//2.读取100%质量的图片流ByteArrayOutputStream对象
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
//3.result最终的图片字节数
byte[] result;
//如果 100%质量的图片流ByteArrayOutputStream对象的大小 还比要压缩的大小 小于等于 直接返回100%质量的图片流ByteArrayOutputStream对象
if (byteArrayOutputStream.size() <= maxSize) {
result = byteArrayOutputStream.toByteArray();
} else {//需要压缩
//4.收回掉100%质量的图片流ByteArrayOutputStream对象
byteArrayOutputStream.reset();
//5.获取0%质量的图片流ByteArrayOutputStream对象
bitmap.compress(Bitmap.CompressFormat.JPEG, 0, byteArrayOutputStream);
//如果 0%质量的图片流ByteArrayOutputStream对象的大小 还比要压缩的大小 大于等于 直接返回0%质量的图片流ByteArrayOutputStream对象
if (byteArrayOutputStream.size() < maxSize) {// 二分法寻找最佳质量
int startPosition = 0;
int endPosition = 100;
int midPosition = 0;
while (startPosition < endPosition) {
midPosition = (startPosition + endPosition) / 2;
byteArrayOutputStream.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, midPosition, byteArrayOutputStream);
int len = byteArrayOutputStream.size();
if (len == maxSize) {//找到位置对应的字节流等于要压缩的字节流 退出while循环
break;
} else if (len > maxSize) {//中间位置的字节流大于要压缩的字节流 在前半部分找
endPosition = midPosition - 1;
} else {//中间位置的字节流小于等于要压缩的字节流 在后半部分找
startPosition = midPosition + 1;
}
}
if (endPosition == midPosition - 1) {
byteArrayOutputStream.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, startPosition, byteArrayOutputStream);
}
}
result = byteArrayOutputStream.toByteArray();
}
//最后回收掉Bitmap
bitmap.recycle();
//返回图片字节流
return result;
}
四.LuBan压缩
1.效果与对比
2.添加依赖
implementation project(':library')
3.压缩
private void lubanMethod(File file){
Luban.with(this)//上下文对象
.load(file)//原图路径
.ignoreBy(100)//不压缩的阈值 单位为K 即图片小于此阈值时不压缩
.setFocusAlpha(false)//设置是否保留透明通道
.setTargetDir(setLuBanPath())//设置缓存压缩图片路径
.filter(new CompressionPredicate() {
@Override
public boolean apply(String path) {
return !(TextUtils.isEmpty(path));//图片路径不为空 返回true压缩
}
})//设置开启压缩条件
.setCompressListener(new OnCompressListener() {
@Override
public void onStart() {
}
@Override
public void onSuccess(File file) {
addthumbArg(file);
Log.d("TAG","压缩成功后的路径:"+file.getAbsolutePath());
}
@Override
public void onError(Throwable e) {
}
})//压缩回调接口
.setRenameListener(new OnRenameListener() {
@Override
public String rename(String filePath) {
String result=savenum+".jpg";
savenum++;
return result;
}
})//压缩前重命名接口
.launch();//压缩
}
4.效果
压缩前
压缩后
5.代码详情:https://github.com/wujianning/LuBanDemo
6.算法
<1> 前言
Luban
是图片压缩工具,通过参考或者自创压缩规则推求极致的压缩效果 目前的版本压缩效果主要参考微信。因为微信用户量最大,如果压缩后的图片越接近微信则越被用户接受。
<2> 说明
目前的Luban
只是压缩结果接近微信,自身的算法只是为了达到这个效果而设计的。与微信并无任何联系,也不敢妄称是微信的算法。
<3> 算法步骤
(1) 判断图片比例值,是否处于以下区间内。
[1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内
[0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内
[0.5, 0) 即图片处于 [1:2 ~ 1:∞) 比例范围内
(2) 判断图片最长边是否过边界值。
[1, 0.5625) 边界值为:1664 * n(n=1), 4990 * n(n=2), 1280 * pow(2, n-1)(n≥3)
[0.5625, 0.5) 边界值为:1280 * pow(2, n-1)(n≥1)
[0.5, 0) 边界值为:1280 * pow(2, n-1)(n≥1)
3) 计算压缩图片实际边长值,以第2步计算结果为准,超过某个边界值则:width / pow(2, n-1),height/pow(2, n-1)
(4) 计算压缩图片的实际文件大小,以第2、3步结果为准,图片比例越大则文件越大。
size = (newW * newH) / (width * height) * m;
[1, 0.5625) 则 width & height 对应 1664,4990,1280 * n(n≥3),m 对应 150,300,300;
[0.5625, 0.5) 则 width = 1440,height = 2560, m = 200;
[0.5, 0) 则 width = 1280,height = 1280 / scale,m = 500;注:scale为比例值
(5) 判断第4步的size是否过小
[1, 0.5625) 则最小 size 对应 60,60,100
[0.5625, 0.5) 则最小 size 都为 100
[0.5, 0) 则最小 size 都为 100
(6) 将前面求到的值压缩图片 width, height, size 传入压缩流程,压缩图片直到满足以上数值