图片压缩之优化篇
之前曾经对Android中图片中的压缩方式进行分析和总结。详见图片压缩篇。基本涵盖了基础的压缩方法和思路。
但是在实际应用运用仍有许多地方需要优化地方才能够被应用。本文将就以下角度进行思考和优化:
- 一般性应用于朋友圈之类的图片依照怎样的参数进行哪些方面的优化处理?
- 如何有效的减少压缩时间?
- 如何避免压缩过程中的oom?
那我们就开始吧!
合理的压缩参数
首先我们要考虑我们应该用哪些参数来控制我们的压缩过程。下面是我的建议
最大宽度,最大高度
用于控制图片的最终分辨率。我们根据最宽高来进行等比例压缩。这个值我一般设置的为最大宽为1080
最大高为1920。这样的设置能满足一般照片之类的图片的要求,但是对于超长图和超宽图就会出现,压缩过度的问题,所以我们需要对超长图和超宽图进行单独的计算和处理。
首先要判断是否为长图,我这里的判断标准为宽/高
或高/宽
大于3。若判断为长图则修改最大宽高为极限宽高。10000这个数值也是新浪微博对于压缩长图的最大处理值。
public static int imgMemoryMaxWidth = 10000;
public static int imgMemoryMaxHeight = 10000;
private static final float longImgRatio = 3f;
public static void preDoOption(Context context, Uri uri, PressImgService.Option option) {
if(!option.specialDealLongImg)
return;
try {
FileUntil.ImgMsg imgMsg = FileUntil.getImgWidthAndHeight(context, uri);
if (imgMsg == null) {
return;
}
float ratio = (float) imgMsg.width / imgMsg.height;
//超宽图
if (ratio > longImgRatio) {
option.maxWidth = imgMemoryMaxWidth;
}
//超长图
else if (1 / ratio > longImgRatio) {
option.maxHeight = imgMemoryMaxHeight;
}
} catch (IOException e) {
e.printStackTrace();
}
}
压缩率
即图片 文件大小/(图片宽*图片高)
。这个比率其实不能够最为一个绝对标准,尤其是在图片特别小的情况下,所以我当图片小于50k的时候就不在压缩了。
private final static float ignoreSize = 1024 * 50;
压缩流程的优化
之前曾说过图片压缩主要是分为两个部分,其一是压缩图片的分辨率 其二是压缩图片的质量。这两种方式结合才能让我们得到体积小清晰度较高的图片。一般分为以下几个步骤
从Uri或者文件获取图片的bitmap
这里需要注意的是,我们要在这里进行第一次关于图片分辨率的压缩。因为源文件的分辨率是未知的,不做任何限制直接获取bitmap,很可能直接oom。处理方式为利用inJustDecodeBounds
属性只获取图片宽高,然后计算一个inSampleSize
。注意,这个压缩只能作为初步压缩,因为inSampleSize
只能为2的倍数才有效,最终图片很难得到精确的尺寸。
代码如下
//根据uri 获取bitmap
public static Bitmap getBitmapFromUri(Context context, Uri uri, float targetWidth, float targetHeight) throws Exception, OutOfMemoryError {
Bitmap bitmap = null;
int ratio = 1;
InputStream input = null;
if (targetWidth != -1 && targetHeight != -1) {
input = context.getContentResolver().openInputStream(uri);
BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
onlyBoundsOptions.inJustDecodeBounds = true;
onlyBoundsOptions.inDither = true;
onlyBoundsOptions.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
if (input != null) {
input.close();
}
int originalWidth = onlyBoundsOptions.outWidth;
int originalHeight = onlyBoundsOptions.outHeight;
if ((originalWidth == -1) || (originalHeight == -1))
return null;
float widthRatio = originalWidth / targetWidth;
float heightRatio = originalHeight / targetHeight;
ratio = (int) (widthRatio > heightRatio ? widthRatio : heightRatio);
if (ratio < 1)
ratio = 1;
}
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inSampleSize = (int) ratio;
bitmapOptions.inDither = true;
bitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
input = context.getContentResolver().openInputStream(uri);
bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
if (input != null) {
input.close();
}
return bitmap;
}
处理bitmap至合适的分辨率
处理bitmap的分辨率我们可以利用Android自带的ThumbnailUtils.extractThumbnail()
方法来进行处理。这里可以对bitmap的宽高进行精确的压缩。
Bitmap originBitmap = FileUntil.getBitmapFromUri(context, Uri.fromFile(file), maxWidth, maxHeight);
if (originBitmap == null)
return false;
float widthRadio = (float) originBitmap.getWidth() / (float) maxWidth;
float heightRadio = (float) originBitmap.getHeight() / (float) maxHeight;
float radio = widthRadio > heightRadio ? widthRadio : heightRadio;
if (radio > 1) {
bitmap = ThumbnailUtils.extractThumbnail(originBitmap, (int) (originBitmap.getWidth() / radio), (int) (originBitmap.getHeight() / radio));
originBitmap.recycle();
} else
bitmap = originBitmap;
压缩bitmap的生成流的大小,并存储为文件
之后我们需要对bitmap进行存储,并且压缩图片文件大小。基于compress()
函数的quality
参数。这里对quality参数进行了动态化处理。
//保存bitmap 为文件
public static File saveImageAndGetFile(Bitmap bitmap, File file, float limitSize) {
if (bitmap == null || file == null) {
return null;
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
if (limitSize != -1) {
PressImgUntil.compressImageFileSize(bitmap, fos, limitSize);
} else {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
}
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return file;
}
---------------------------------------------------------
/** 压缩文件大小
* @param image
* @param outputStream
* @param limitSize 单位byte
* @throws IOException
*/
public static void compressImageFileSize(Bitmap image, OutputStream outputStream, float limitSize) throws Exception {
ByteArrayOutputStream imgBytes = new ByteArrayOutputStream();
int options = 100;
image.compress(Bitmap.CompressFormat.JPEG, options, imgBytes);
while (imgBytes.size() > limitSize && imgBytes.size() > ignoreSize && options > 20) {
imgBytes.reset();
int dx = 0;
float dz = (float) imgBytes.size() / limitSize;
if (dz > 2)
dx = 30;
else if (dz > 1)
dx = 25;
else
dx = 20;
options -= dx;
image.compress(Bitmap.CompressFormat.JPEG, options, imgBytes);
// Log.i("lzc", "compressImageFileSize " + options + "---" + imgBytes.size() +"---"+image.getWidth()+"---"+image.getHeight());
}
outputStream.write(imgBytes.toByteArray());
imgBytes.close();
outputStream.close();
}
基于线程池的动态任务分配
之前的文章曾讲过压缩图片基于线程池的处理。由于压缩过程会消耗大量的内存。所有中间提到一个矛盾:同时进行的任务数越多,总体的压缩速度越快,但是oom的风险也随之增加。我通过两种方式来尝试解决这个问题:
单独的压缩进程
将压缩进程单独的放到某个进程中,这样就能够获取更多的可用内存。但是这样就需要我们进行一些跨进程的通信,来控制压缩过程与接收压缩回调,这里可以基于Messenger
机制来实现。一个PressImgService
类来负责压缩业务。一个PressImgManager
来控制压缩和接收回调。
动态控制线程池中的任务
尽管我们新开了进程,能够获得较大的内存,但是仍然有oom的风险。所以我打算动态的计算每一个压缩任务能占用的内存,然后根据内存剩余往线程池中添加线程。
获取内存信息
public static MemoryMessage getMemoryMsg(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
int memClass = activityManager.getMemoryClass();//64,以m为单位
int largeClass = activityManager.getLargeMemoryClass();//64,以m为单位
long freeMemory = Runtime.getRuntime().freeMemory();
long totalMemory = Runtime.getRuntime().totalMemory();
long maxMemory = Runtime.getRuntime().maxMemory();
return new MemoryMessage(memClass, freeMemory, totalMemory, maxMemory,largeClass);
}
首先我取总内存的2/3为可用内存
FunUntil.MemoryMessage memoryMessage = FunUntil.getMemoryMsg(this);
availableMemory = (memoryMessage.maxMemory - memoryMessage.totalMemory + memoryMessage.freeMemory) * 2 / 3;
计算每个任务的占用内存,这也是个不精确的值,但是已经很接近了。
//计算压缩消耗内存
public static int calcPressTaskMemoryUse(Context context, PressImgService.PressMsg pressMsg) {
int memory = 0;
final int imgMemoryRatio = 2;
int targetWidth = pressMsg.option.maxWidth;
int targetHeight = pressMsg.option.maxHeight;
Uri uri = Uri.fromFile(pressMsg.originFile);
try {
//获取图片宽高
FileUntil.ImgMsg imgMsg = FileUntil.getImgWidthAndHeight(context, uri);
if (imgMsg == null)
return 0;
//长宽比例
float widthRatio = (float) imgMsg.width / targetWidth;
float heightRatio = (float) imgMsg.height / targetHeight;
int ratio = (int) (widthRatio > heightRatio ? widthRatio : heightRatio);
if (ratio < 1)
ratio = 1;
//第一次处理后宽高
int originWidth = 0;
int originHeight = 0;
ratio = Integer.highestOneBit(ratio);
originWidth = imgMsg.width / ratio;
originHeight = imgMsg.height / ratio;
//计算内存
memory += originWidth * originHeight * imgMemoryRatio;
//计算第二次处理
float secondWidthRadio = (float) originWidth / (float) targetWidth;
float secondHeightRadio = (float) originHeight / (float) targetHeight;
float secondRadio = secondWidthRadio > secondHeightRadio ? secondWidthRadio : secondHeightRadio;
if (secondRadio > 1) {
memory += (originWidth / secondRadio) * (originHeight / ratio) * imgMemoryRatio;
}
memory += targetWidth * targetHeight * PressImgService.pressRadio;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
return memory;
}
将压缩任务保存成队列,每当有任务结束,按占用内存大小排序,重新分发等待中的任务。
//分发任务
private void dispatchTask() {
if (pressMsgList.size() != 0) {
Collections.sort(pressMsgList);
if (pressMsgList.size() != 0)
Log.i("lzc", "availableMemory " + availableMemory);
dispatchCore(pressMsgList);
}
}
//核心分发
public void dispatchCore(List<PressMsg> prepareMsgList) {
int current = 0;
while (current <= prepareMsgList.size() - 1) {
if (prepareMsgList.get(current).userMemory > availableMemory)
current++;
else {
PressMsg addTask = prepareMsgList.remove(current);
startThread(addTask.uuid, addTask);
}
}
}
//开始执行压缩
private void startThread(String uuid, PressMsg pressMsg) {
if (shutDownSet.contains(uuid))
return;
try {
pressMsg.currentStatus = 0;
executorService.execute(new PressRunnable(pressMsg));
availableMemory -= pressMsg.userMemory;
} catch (Exception e) {
e.printStackTrace();
errorUUID(uuid, pressMsg, "线程启动异常!");
}
}
通过这样的方式基本可以完全避免oom的情况。
总结
利用上述的优化方式,基本可以实现9张图片在1-5秒的压缩完成。一般9张相册照片都在1秒左右即可压缩完成。如30000多像素的超长图,9张在5秒内也可以压缩完成。