之前曾經(jīng)對Android中圖片中的壓縮方式進(jìn)行分析和總結(jié)。詳見圖片壓縮篇》基本涵蓋了基礎(chǔ)的壓縮方法和思路。
但是在實(shí)際應(yīng)用運(yùn)用仍有許多地方需要優(yōu)化地方才能夠被應(yīng)用影锈。本文將就以下角度進(jìn)行思考和優(yōu)化:
- 一般性應(yīng)用于朋友圈之類的圖片依照怎樣的參數(shù)進(jìn)行哪些方面的優(yōu)化處理芹务?
- 如何有效的減少壓縮時間?
- 如何避免壓縮過程中的oom鸭廷?
那我們就開始吧枣抱!
合理的壓縮參數(shù)
首先我們要考慮我們應(yīng)該用哪些參數(shù)來控制我們的壓縮過程。下面是我的建議
最大寬度辆床,最大高度
用于控制圖片的最終分辨率佳晶。我們根據(jù)最寬高來進(jìn)行等比例壓縮。這個值我一般設(shè)置的為最大寬為1080
最大高為1920佛吓。這樣的設(shè)置能滿足一般照片之類的圖片的要求宵晚,但是對于超長圖和超寬圖就會出現(xiàn)垂攘,壓縮過度的問題,所以我們需要對超長圖和超寬圖進(jìn)行單獨(dú)的計算和處理淤刃。
首先要判斷是否為長圖晒他,我這里的判斷標(biāo)準(zhǔn)為寬/高
或高/寬
大于3。若判斷為長圖則修改最大寬高為極限寬高逸贾。10000這個數(shù)值也是新浪微博對于壓縮長圖的最大處理值陨仅。
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();
}
}
壓縮率
即圖片 文件大小/(圖片寬*圖片高)
。這個比率其實(shí)不能夠最為一個絕對標(biāo)準(zhǔn)铝侵,尤其是在圖片特別小的情況下灼伤,所以我當(dāng)圖片小于50k的時候就不在壓縮了。
private final static float ignoreSize = 1024 * 50;
壓縮流程的優(yōu)化
之前曾說過圖片壓縮主要是分為兩個部分咪鲜,其一是壓縮圖片的分辨率 其二是壓縮圖片的質(zhì)量狐赡。這兩種方式結(jié)合才能讓我們得到體積小清晰度較高的圖片。一般分為以下幾個步驟
從Uri或者文件獲取圖片的bitmap
這里需要注意的是疟丙,我們要在這里進(jìn)行第一次關(guān)于圖片分辨率的壓縮颖侄。因?yàn)樵次募姆直媛适俏粗模蛔鋈魏蜗拗浦苯荧@取bitmap享郊,很可能直接oom览祖。處理方式為利用inJustDecodeBounds
屬性只獲取圖片寬高,然后計算一個inSampleSize
炊琉。注意展蒂,這個壓縮只能作為初步壓縮,因?yàn)?code>inSampleSize只能為2的倍數(shù)才有效苔咪,最終圖片很難得到精確的尺寸锰悼。
代碼如下
//根據(jù)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()
方法來進(jìn)行處理。這里可以對bitmap的寬高進(jìn)行精確的壓縮悼泌。
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進(jìn)行存儲,并且壓縮圖片文件大小馆里“溃基于compress()
函數(shù)的quality
參數(shù)。這里對quality參數(shù)進(jìn)行了動態(tài)化處理鸠踪。
//保存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();
}
基于線程池的動態(tài)任務(wù)分配
之前的文章曾講過壓縮圖片基于線程池的處理丙者。由于壓縮過程會消耗大量的內(nèi)存。所有中間提到一個矛盾:同時進(jìn)行的任務(wù)數(shù)越多营密,總體的壓縮速度越快械媒,但是oom的風(fēng)險也隨之增加。我通過兩種方式來嘗試解決這個問題:
單獨(dú)的壓縮進(jìn)程
將壓縮進(jìn)程單獨(dú)的放到某個進(jìn)程中,這樣就能夠獲取更多的可用內(nèi)存纷捞。但是這樣就需要我們進(jìn)行一些跨進(jìn)程的通信痢虹,來控制壓縮過程與接收壓縮回調(diào),這里可以基于Messenger
機(jī)制來實(shí)現(xiàn)主儡。一個PressImgService
類來負(fù)責(zé)壓縮業(yè)務(wù)奖唯。一個PressImgManager
來控制壓縮和接收回調(diào)。
動態(tài)控制線程池中的任務(wù)
盡管我們新開了進(jìn)程糜值,能夠獲得較大的內(nèi)存丰捷,但是仍然有oom的風(fēng)險。所以我打算動態(tài)的計算每一個壓縮任務(wù)能占用的內(nèi)存寂汇,然后根據(jù)內(nèi)存剩余往線程池中添加線程病往。
獲取內(nèi)存信息
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);
}
首先我取總內(nèi)存的2/3為可用內(nèi)存
FunUntil.MemoryMessage memoryMessage = FunUntil.getMemoryMsg(this);
availableMemory = (memoryMessage.maxMemory - memoryMessage.totalMemory + memoryMessage.freeMemory) * 2 / 3;
計算每個任務(wù)的占用內(nèi)存,這也是個不精確的值停巷,但是已經(jīng)很接近了。
//計算壓縮消耗內(nèi)存
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;
//計算內(nèi)存
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;
}
將壓縮任務(wù)保存成隊列累贤,每當(dāng)有任務(wù)結(jié)束叠穆,按占用內(nèi)存大小排序少漆,重新分發(fā)等待中的任務(wù)臼膏。
//分發(fā)任務(wù)
private void dispatchTask() {
if (pressMsgList.size() != 0) {
Collections.sort(pressMsgList);
if (pressMsgList.size() != 0)
Log.i("lzc", "availableMemory " + availableMemory);
dispatchCore(pressMsgList);
}
}
//核心分發(fā)
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);
}
}
}
//開始執(zhí)行壓縮
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的情況示损。
總結(jié)
利用上述的優(yōu)化方式渗磅,基本可以實(shí)現(xiàn)9張圖片在1-5秒的壓縮完成。一般9張相冊照片都在1秒左右即可壓縮完成检访。如30000多像素的超長圖始鱼,9張在5秒內(nèi)也可以壓縮完成。