為何要壓縮
1、體積的原因
如果你的圖片是要準備上傳的,那動輒幾M的大小肯定不行的,況且圖片分辨率大于設備分辨率的話毫無意義。
2、內存原因
如果圖片要顯示下Android設備上,ImageView最終是要加載Bitmap對象的,就要考慮單個Bitmap對象的內存占用了,如何計算一張圖片的加載到內存的占用呢?其實就是所有像素的內存占用總和:
bitmap內存大小 = 圖片長度 x 圖片寬度 x 單位像素占用的字節數
起決定因素就是最后那個參數了,Bitmap'常見有2種編碼方式:ARGB_8888和RGB_565,ARGB_8888每個像素點4個byte,RGB_565是2個byte,一般都采用ARGB_8888這種。那么常見的10801920的圖片內存占用就是:
1920 x 1080 x 4 = 7.9M
壓縮原理
從上面可以總結出,圖片壓縮應該從兩個方面入手同時進行:先是降低分辨率,然后降低每個像素的質量也就是內存占用。
分辨率壓縮
假設有張原圖是3840x2400,我想壓縮成1920x1080,實際是不可能100%能壓縮這個值的。因為圖片壓縮要保證寬高比,試想一下800x100的橫向圖可能壓成20x200豎向圖嗎? 不可能的.。這里常見的算法就是在1920x1080的范圍內保證較短邊,然后按照比例壓縮整個圖,
質量壓縮
質量壓縮并不會改變圖片在內存中的大小,僅僅會減小圖片所占用的磁盤空間的大小,因為質量壓縮不會改變圖片的分辨率,而圖片在內存中的大小是根據widthheight一個像素的所占用的字節數計算的,寬高沒變,在內存中占用的大小自然不會變,質量壓縮的原理是通過改變圖片的位深和透明度來減小圖片占用的磁盤空間大小,所以不適合作為縮略圖,可以用于想保持圖片質量的同時減小圖片所占用的磁盤空間大小。另外,由于png是無損壓縮,所以設置quality無效,以下是實現方式:
/*
- 質量壓縮
- @param format 圖片格式 jpeg,png,webp
- @param quality 圖片的質量,0-100,數值越小質量越差
*/
public static void compress(Bitmap.CompressFormat format, int quality) {
File sdFile = Environment.getExternalStorageDirectory();
File originFile = new File(sdFile, "originImg.jpg");
Bitmap originBitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
originBitmap.compress(format, quality, bos);
try {
FileOutputStream fos = new FileOutputStream(new File(sdFile, "resultImg.jpg"));
fos.write(bos.toByteArray());
fos.flush();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
3、采樣率壓縮
采樣率壓縮是通過設置BitmapFactory.Options.inSampleSize,來減小圖片的分辨率,進而減小圖片所占用的磁盤空間和內存大小。
設置的inSampleSize會導致壓縮的圖片的寬高都為1/inSampleSize,整體大小變為原始圖片的inSampleSize平方分之一,當然,這些有些注意點:
1、inSampleSize小于等于1會按照1處理
2、inSampleSize只能設置為2的平方,不是2的平方則最終會減小到最近的2的平方數,如設置7會按4進行壓縮,設置15會按8進行壓縮。
具體的代碼實現方式如下:
/**
-
@param inSampleSize 可以根據需求計算出合理的inSampleSize
*/
public static void compress(int inSampleSize) {
File sdFile = Environment.getExternalStorageDirectory();
File originFile = new File(sdFile, "originImg.jpg");
BitmapFactory.Options options = new BitmapFactory.Options();
//設置此參數是僅僅讀取圖片的寬高到options中,不會將整張圖片讀到內存中,防止oom
options.inJustDecodeBounds = true;
Bitmap emptyBitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath(), options);options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
Bitmap resultBitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath(), options);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
resultBitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
try {
FileOutputStream fos = new FileOutputStream(new File(sdFile, "resultImg.jpg"));
fos.write(bos.toByteArray());
fos.flush();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
4、縮放壓縮
通過減少圖片的像素來降低圖片的磁盤空間大小和內存大小,可以用于緩存縮略圖
實現方式如下:
public void compress(View v) {
File sdFile = Environment.getExternalStorageDirectory();
File originFile = new File(sdFile, "originImg.jpg");
Bitmap bitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath());
//設置縮放比
int radio = 8;
Bitmap result = Bitmap.createBitmap(bitmap.getWidth() / radio, bitmap.getHeight() / radio, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
RectF rectF = new RectF(0, 0, bitmap.getWidth() / radio, bitmap.getHeight() / radio);
//將原圖畫在縮放之后的矩形上
canvas.drawBitmap(bitmap, null, rectF, null);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
result.compress(Bitmap.CompressFormat.JPEG, 100, bos);
try {
FileOutputStream fos = new FileOutputStream(new File(sdFile, "sizeCompress.jpg"));
fos.write(bos.toByteArray());
fos.flush();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
5、JNI調用JPEG庫
Android的圖片引擎使用的是閹割版的skia引擎,去掉了圖片壓縮中的哈夫曼算法——一種耗CPU但是能在保持圖片清晰度的情況下很大程度降低圖片的磁盤空間大小的算法,這就是為什么ios拍出的1M的照片比Android5M的還要清晰的原因。
由于筆者能力有限,暫時沒有對NDK這塊做深入研究,后期將會將此方法補全
6、其他
還有一些技巧,比如在縮放壓縮的時候,Bitmap.createBitmap(bitmap.getWidth() / radio, bitmap.getHeight() / radio, Bitmap.Config.ARGB_8888),如果不需要圖片的透明度,可以將ARGB_8888改成RGB_565,這樣之前每個像素占用4個字節,現在只需要2個字節,節省了一半的大小。
7、總結
1、使用webp格式的圖片可以在保持清晰度的情況下減小圖片的磁盤大小,是一種比較優秀的,google推薦的圖片格式
2、質量壓縮可以減小圖片占用的磁盤空間,不會減小在內存中的大小
3、采樣率壓縮可以通過改變分辨率來減小圖片所占用的磁盤空間和內存空間大小,但是采樣率只能設置2的n次方,可能圖片的最優比例在中間
4、尺寸壓縮同樣也是通過改變分辨率來減小圖片所占用的磁盤空間和內存空間大小,縮放的尺寸沒有什么限制
5、jni調用jpeg庫來彌補安卓系統skia框架的不足,也是比較優秀的解決方式(方法會在后期補充)
既然尺寸壓縮和采樣率壓縮都是通過改變圖片的分辨率來降低大小,有什么區別嗎?
其中一個原因已經說明了,采樣率的壓縮比例會受到限制,尺寸壓縮不會,但是采樣率壓縮的清晰度會比尺寸壓縮的清晰度要好一些,
另外一個 主函數中直接懟兩個方法進去,復制粘貼即可
//圖片壓縮功能獲取長寬比
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// 源圖片的高度和寬度
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// 計算出實際寬高和目標寬高的比率
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高
// 一定都會大于等于目標的寬和高。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
//圖片壓縮功能
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 第一次解析將inJustDecodeBounds設置為true,來獲取圖片大小
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 調用上面定義的方法計算inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 使用獲取到的inSampleSize值再次解析圖片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
方法調用,哪里要圖片就塞哪里,直接把圖片給你整成100*100
imageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), imgs[position], 100, 100));
Android應用開發中三種常見的圖片壓縮方法,分別是:質量壓縮法、比例壓縮法(根據路徑獲取圖片并壓縮)和比例壓縮法(根據Bitmap圖片壓縮)。
一、質量壓縮法
private Bitmap compressImage(Bitmap image) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//質量壓縮方法,這里100表示不壓縮,把壓縮后的數據存放到baos中
int options = 100;
while ( baos.toByteArray().length / 1024>100) { //循環判斷如果壓縮后圖片是否大于100kb,大于繼續壓縮
baos.reset();//重置baos即清空baos
image.compress(Bitmap.CompressFormat.JPEG, options, baos);//這里壓縮options%,把壓縮后的數據存放到baos中
options -= 10;//每次都減少10
}
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把壓縮后的數據baos存放到ByteArrayInputStream中
Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream數據生成圖片
return bitmap;
}
二、圖片按比例大小壓縮方法(根據路徑獲取圖片并壓縮)
private Bitmap getimage(String srcPath) {
BitmapFactory.Options newOpts = new BitmapFactory.Options();
//開始讀入圖片,此時把options.inJustDecodeBounds 設回true了
newOpts.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeFile(srcPath,newOpts);//此時返回bm為空
newOpts.inJustDecodeBounds = false;
int w = newOpts.outWidth;
int h = newOpts.outHeight;
//現在主流手機比較多是800*480分辨率,所以高和寬我們設置為
float hh = 800f;//這里設置高度為800f
float ww = 480f;//這里設置寬度為480f
//縮放比。由于是固定比例縮放,只用高或者寬其中一個數據進行計算即可
int be = 1;//be=1表示不縮放
if (w > h && w > ww) {//如果寬度大的話根據寬度固定大小縮放
be = (int) (newOpts.outWidth / ww);
} else if (w < h && h > hh) {//如果高度高的話根據寬度固定大小縮放
be = (int) (newOpts.outHeight / hh);
}
if (be <= 0)
be = 1;
newOpts.inSampleSize = be;//設置縮放比例
//重新讀入圖片,注意此時已經把options.inJustDecodeBounds 設回false了
bitmap = BitmapFactory.decodeFile(srcPath, newOpts);
return compressImage(bitmap);//壓縮好比例大小后再進行質量壓縮
}
三、圖片按比例大小壓縮方法(根據Bitmap圖片壓縮)
private Bitmap comp(Bitmap image) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
if( baos.toByteArray().length / 1024>1024) {//判斷如果圖片大于1M,進行壓縮避免在生成圖片(BitmapFactory.decodeStream)時溢出
baos.reset();//重置baos即清空baos
image.compress(Bitmap.CompressFormat.JPEG, 50, baos);//這里壓縮50%,把壓縮后的數據存放到baos中
}
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
BitmapFactory.Options newOpts = new BitmapFactory.Options();
//開始讀入圖片,此時把options.inJustDecodeBounds 設回true了
newOpts.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, newOpts);
newOpts.inJustDecodeBounds = false;
int w = newOpts.outWidth;
int h = newOpts.outHeight;
//現在主流手機比較多是800*480分辨率,所以高和寬我們設置為
float hh = 800f;//這里設置高度為800f
float ww = 480f;//這里設置寬度為480f
//縮放比。由于是固定比例縮放,只用高或者寬其中一個數據進行計算即可
int be = 1;//be=1表示不縮放
if (w > h && w > ww) {//如果寬度大的話根據寬度固定大小縮放
be = (int) (newOpts.outWidth / ww);
} else if (w < h && h > hh) {//如果高度高的話根據寬度固定大小縮放
be = (int) (newOpts.outHeight / hh);
}
if (be <= 0)
be = 1;
newOpts.inSampleSize = be;//設置縮放比例
//重新讀入圖片,注意此時已經把options.inJustDecodeBounds 設回false了
isBm = new ByteArrayInputStream(baos.toByteArray());
bitmap = BitmapFactory.decodeStream(isBm, null, newOpts);
return compressImage(bitmap);//壓縮好比例大小后再進行質量壓縮
}