最近一段時間的開發中和Bitmap接觸較多,就Bitmap的使用有了一些新的認識,如何對Bitmap進行壓縮,減小內存佔用有了一些總結。javascript
社交類(或者說是包含用戶系統)的APP基本上都會包含用戶自定義頭像的功能,可讓用戶從相冊選擇或拍攝一張圖片做爲本身的頭像,這樣才能顯現出每一個人的個性嘛!每一個用戶的手機裏各類各樣不可描述的照片,從尺寸到大小各不相同,所以如何把用戶選擇的圖片正確的加載到ImageView裏就成了一件值得探討的事情。好了,廢話不說,下面就讓咱們一步步揭開Bitmap的神祕面紗。java
咱們先從簡單的入手,看看從手機相冊加載一張圖片到ImageView的正確方式。android
咱們就以上圖爲列,這張圖片在我手機裏的信息以下:git
能夠看到,圖片大小不足1M。那麼把他加載到手機內存中時又會發生什麼呢?github
/** * 打開手機相冊 */
private void selectFromGalley() {
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, REQUEST_CODE_PICK_FROM_GALLEY);
}複製代碼
在Android 中打開相冊是一件很是方便的事情,選擇好圖片以後就能夠在onActivityResult中接收這張圖片測試
if (resultCode == Activity.RESULT_OK) {
Uri uri = data.getData();
if (uri != null) {
ProcessResult(uri);
}
}複製代碼
根據Uri獲得Bitmapui
@TargetApi(Build.VERSION_CODES.KITKAT)
private void ProcessResult(Uri destUrl) {
String pathName = FileHelper.stripFileProtocol(destUrl.toString());
showBitmapInfos(pathName);
Bitmap bitmap = BitmapFactory.decodeFile(pathName);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
float count = bitmap.getByteCount() / M_RATE;
float all = bitmap.getAllocationByteCount() / M_RATE;
String result = "這張圖片佔用內存大小:\n" +
"bitmap.getByteCount()== " + count + "M\n" +
"bitmap.getAllocationByteCount()= " + all + "M";
info.setText(result);
Log.e(TAG, result);
bitmap = null;
} else {
T.showLToast(mContext, "fail");
}
}
/** * 獲取Bitmap的信息 * @param pathName */
private void showBitmapInfos(String pathName) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(pathName, options);
int width = options.outWidth;
int height = options.outHeight;
Log.e(TAG, "showBitmapInfos: \n" +
"width=: " + width + "\n" +
"height=: " + height);
options.inJustDecodeBounds = false;
}複製代碼
這裏的處理很簡單,須要注意的一點是onActivityResult 方法中返回的Intent返回的圖片地址是一個Uri類型,包含具體協議,爲了方便使用BitmapFactory的decode方法,須要將這個個Uri類型的地址轉換爲普通的地址,stripFileProtocol具體實現可參考源碼。spa
showBitmapInfos 這個方法就是很簡單,就是獲取一下所要加載圖片的信息。這裏主要仍是靠inJustDecodeBounds 這個參數,當此參數爲true時,BitmapFactory 只會解析圖片的原始寬/高信息,並不會去真正的加載圖片。3d
咱們看一下輸出日誌及內存變化:日誌
關於getByteCount和getAllocationByteCount的區別,這裏暫時不討論,只要知道他們均可以獲取Bitmap佔用內存大小
能夠看到,因爲這張圖片是放在手機內部SD卡上,因此showBitmapInfos 解析後獲取的圖片寬高信息和以前是一致的,寬x高爲 2160x1920。看到所佔用的內存 15M,是否是有點意外,一張658KB 的加載後竟然要佔這麼大的內存。在看一下monitor檢測的內存變化,在20s後選擇圖片後,佔用內存有了一個明顯的上升。佔用這麼大的內存,顯然是很差的。可能不少人和我同樣,在這個時候想到的第一個詞是壓縮圖片,把圖片變小他佔的內存不就會變小了嗎?好,那就壓縮圖片
由於咱們要處理的是Bitmap,首先從他自帶的方法出發,果真找到了一個compress方法。
private Bitmap getCompressedBitmap(Bitmap bitmap) {
try {
//建立一個用於存儲壓縮後Bitmap的文件
File compressedFile = FileHelper.createFileByType(mContext, destType, "compressed");
Uri uri = Uri.fromFile(compressedFile);
OutputStream os = getContentResolver().openOutputStream(uri);
Bitmap.CompressFormat format = destType == FileHelper.JPEG ?
Bitmap.CompressFormat.JPEG : Bitmap.CompressFormat.PNG;
boolean success = bitmap.compress(format, compressRate, os);
if (success) {
T.showLToast(mContext, "success");
}
final String pathName = FileHelper.stripFileProtocol(uri.toString());
showBitmapInfos(pathName);
bitmap = BitmapFactory.decodeFile(pathName);
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}複製代碼
bitmap.compress(format, compressRate, os) 會按照指定的格式和壓縮比例將壓縮後的bitmap寫入到os 所對應的文件中。compressRate的取值在0-100之間,0表示壓縮到最小尺寸。
在ProcessResult方法中,咱們獲取bitmap後,首先經過上述方法將bitmap壓縮,而後在顯示到ImageView中。咱們看一下,壓縮事後的狀況。
上面的日誌,第一個showBitmapInfos 顯示的是選擇的圖片經過BitmapFactory解析後的信息,第二個showBitmapInfos
顯示的壓縮後圖片的寬高信息,最後很意外,咱們的壓縮方法彷佛沒起到做用,佔用的內存沒有任何變化,依舊是15M。
難道是compress方法沒生效嗎?其實否則,至少從UI上看compress的確生效了, 當compressRate=0時,懶羊羊的圖片顯示到ImageView上時已經很是不清晰了,失真很是嚴重。那麼究竟是爲何呢?
這裏就得從概念上提及,一開始咱們提到了這張懶羊羊的圖片大小時658KB,這是它在手機存儲空間所佔的大小,而當咱們在選擇這張圖片,並解析爲Bitmap時,他所站的15MB是在內存中所佔的大小;而compress方法只能壓縮前一種大小,也就是所使用Bitmap的compress方法只是壓縮他在存儲空間的大小,結果就是致使圖片失真;而不能改變他在內存中所佔用的大小。
那麼怎樣才能讓Bitmap所佔用的內存變小呢?這就的從Bitmap佔用內存的計算方法入手,在這篇文章中已經對bitmap所佔用內存大小作了深刻分析,從中咱們能夠得出結論,決定一張圖片所佔內存大小的因素是圖片的寬高和Bitmap的格式。這裏咱們加載的時候對Bitmap格式未作更改,也就是默認的ARGB_8888,所以咱們就得從寬高入手,得出以下的壓縮方案。
private void CropTheImage(Uri imageUrl) {
Intent cropIntent = new Intent("com.android.camera.action.CROP");
cropIntent.setDataAndType(imageUrl, "image/*");
cropIntent.putExtra("cropWidth", "true");
cropIntent.putExtra("outputX", cropTargetWidth);
cropIntent.putExtra("outputY", cropTargetHeight);
File copyFile = FileHelper.createFileByType(mContext, destType, String.valueOf(System.currentTimeMillis()));
copyUrl = Uri.fromFile(copyFile);
cropIntent.putExtra("output", copyUrl);
startActivityForResult(cropIntent, REQUEST_CODE_CROP_PIC);
}複製代碼
這裏調用了系統自帶的圖片裁剪控件,並建立了一個copyFile 的文件,裁剪事後的圖片的地址指向就是這個文件所對應的地址。
當cropTargetWidth=1080,cropTargetHeight=920時,咱們看一下日誌:
能夠看到,Bitmap所佔用的內存終於變小了,並且因爲在裁剪時寬高各縮小了1/2,整個內存的佔用也是縮小了1/4,變成了3.9M左右。同時圖片在手機存儲空間也變小了。
固然,這裏要注意的是,com.android.camera.action.CROP 中兩個參數 "outputX" 和"outputY",決定了壓縮後圖片的大小,所以當這兩個值的大小超過原始圖片的大小時,內存佔用反而會增長,這一點應該很好理解,因此需確保傳遞合適的值,不然會拔苗助長。
採用Sample,也就是是採樣的方式壓縮圖片以前,咱們首先須要瞭解一下inSampleSize 這個參數。
inSampleSize 是BitmapFactory.Options 的一個參數,當他爲1時,採樣後的圖片大小爲圖片原始大小;當inSampleSize 爲2時,那麼採樣後的圖片其寬/高均爲原圖大小的1/2,而像素數爲原圖的1/4,其佔有的內存大小也爲原圖的1/4。inSampleSize 的取值應該是2的指數。
private Bitmap getRealCompressedBitmap(String pathName, int reqWidth, int reqHeight) {
Bitmap bitmap;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(pathName, options);
int width = options.outWidth / 2;
int height = options.outHeight / 2;
int inSampleSize = 1;
while (width / inSampleSize >= reqWidth && height / inSampleSize >= reqHeight) {
inSampleSize = inSampleSize * 2;
}
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeFile(pathName, options);
showBitmapInfos(pathName);
return bitmap;
}複製代碼
能夠以下調用這個方法:
if (needSample) {
bitmap = getRealCompressedBitmap(pathName, 200, 200);
}複製代碼
咱們但願將2160x1920像素的原圖壓縮到200x200 像素的大小,所以在getRealCompressedBitmap方法中,經過while循環inSampleSize的值最終爲8,所以內存佔用率將變爲原來的1/64,這是一個很大的降幅。咱們看一下日誌,看看究竟是否可以如咱們所願:
能夠看到,使用這種方法進行圖片壓縮後,增長的內存只有0.24M,幾乎能夠忽略不計了。固然前提是咱們要使用的圖片的確不須要很大,好比這裏,須要用這張圖片做爲用戶頭像的話,那麼將原圖縮略成200x200 px的大小是沒有問題的。
上面提到的三種壓縮方案,經過對比能夠發現,第一種方案適用於進行純粹的文件壓縮,而不適用進行圖像處理壓縮;第二種方案壓縮方案適用於進行圖像編輯時的壓縮,就像手機自帶相冊的編輯功能,能夠隨着裁剪區域的大小進行最終的壓縮;第三種方案相對來講,適應性較強,各類場景都會符合。
有時候,咱們除了從相冊獲取圖片以外,還能夠經過手機自帶的相機拍攝圖片。
private void openCamera() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//建立一個臨時文件夾存儲拍攝的照片
File file = FileHelper.createFileByType(mContext, destType, "test");
imageUrl = Uri.fromFile(file);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUrl);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PIC_CAMERA);
}
}複製代碼
不一樣於從相冊選取圖片,打開相機以前須要咱們本身定義一個存儲圖片的臨時文件file,這個臨時文件既能夠在應用的臨時存儲區也能夠在手機存儲的臨時存儲區;經過這個文件就能夠生成一個Uri對象,有了這個Uri對象,相機拍攝完照片以後就能夠在onActivityResult方法中經過這個Uri獲取到Bitmap了。
這裏咱們能夠試一下,隨便用手機拍攝一張圖片轉爲Bitmap加載會佔多大的手機內存(以我用的小米手機5爲列,拍攝一張圖片):
能夠看到這張圖片的分辨率達到了3456x4608 像素,而他加載到內存是所佔的大小竟然達到了60M,這是很是不科學的作法,也是毫無心義的作法,由於咱們的手機可見區域並無這麼大,將整張照片徹底加載是沒有意義的。所以能夠按照以前的壓縮方案進行壓縮。
bitmap = getRealCompressedBitmap(pathName, screenWidth, screenHeight);複製代碼
咱們能夠將原來的圖片壓縮到手機屏幕大小的圖片
能夠看到佔用內存有了明顯的減小。
有時須要將拍攝出來的照片添加到手機相冊中,方便從相冊直接查看
private void insertToGallery(Uri imageUrl) {
Uri galleryUri = Uri.fromFile(new File(FileHelper.getPicutresPath(destType)));
boolean result = FileHelper.copyResultToGalley(mContext, imageUrl, galleryUri);
if (result) {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(galleryUri);
sendBroadcast(mediaScanIntent);
}
}複製代碼
copyResultToGalley 方法的實現很簡單,就是將imageUri 這個地址的文件複製到galleryUri 這個地址,複製成功後發送一條
action="ACTION_MEDIA_SCANNER_SCAN_FILE" 的廣播便可。
好了,關於Bitmap的初探就說到這裏,對於上面提到的各類壓縮方案,有興趣的同窗可結合一下demo測試。Github 地址
用了好久的ImageView,發現Bitmap纔是Android中圖像處理最核心的東西,有不少東西值得去深刻了解。