最近一段時(shí)間的開(kāi)發(fā)中和Bitmap接觸較多炼邀,就Bitmap的使用有了一些新的認(rèn)識(shí)讶舰,如何對(duì)Bitmap進(jìn)行壓縮捞蚂,減少內(nèi)存占用有了一些總結(jié)妇押。
背景##
社交類(或者說(shuō)是包含用戶系統(tǒng))的APP基本上都會(huì)包含用戶自定義頭像的功能,可以讓用戶從相冊(cè)選擇或拍攝一張圖片作為自己的頭像姓迅,這樣才能顯現(xiàn)出每個(gè)人的個(gè)性嘛敲霍!每個(gè)用戶的手機(jī)里各種各樣不可描述的照片俊马,從尺寸到大小各不相同,因此如何把用戶選擇的圖片正確的加載到ImageView里就成了一件值得探討的事情肩杈。好了柴我,廢話不說(shuō),下面就讓我們一步步揭開(kāi)Bitmap的神秘面紗扩然。
從相冊(cè)加載一張圖片##
我們先從簡(jiǎn)單的入手艘儒,看看從手機(jī)相冊(cè)加載一張圖片到ImageView的正確方式。
我們就以上圖為列夫偶,這張圖片在我手機(jī)里的信息如下:
可以看到界睁,圖片大小不足1M。那么把他加載到手機(jī)內(nèi)存中時(shí)又會(huì)發(fā)生什么呢兵拢?
打開(kāi)相冊(cè)加載圖片
/**
* 打開(kāi)手機(jī)相冊(cè)
*/
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 中打開(kāi)相冊(cè)是一件非常方便的事情翻斟,選擇好圖片之后就可以在onActivityResult中接收這張圖片
if (resultCode == Activity.RESULT_OK) {
Uri uri = data.getData();
if (uri != null) {
ProcessResult(uri);
}
}
根據(jù)Uri得到Bitmap
@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èi)存大小:\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;
}
這里的處理很簡(jiǎn)單,需要注意的一點(diǎn)是onActivityResult 方法中返回的Intent返回的圖片地址是一個(gè)Uri類型说铃,包含具體協(xié)議访惜,為了方便使用BitmapFactory的decode方法,需要將這個(gè)個(gè)Uri類型的地址轉(zhuǎn)換為普通的地址腻扇,stripFileProtocol具體實(shí)現(xiàn)可參考源碼疾牲。
showBitmapInfos 這個(gè)方法就是很簡(jiǎn)單,就是獲取一下所要加載圖片的信息衙解。這里主要還是靠inJustDecodeBounds 這個(gè)參數(shù)阳柔,當(dāng)此參數(shù)為true時(shí),BitmapFactory 只會(huì)解析圖片的原始寬/高信息蚓峦,并不會(huì)去真正的加載圖片舌剂。
我們看一下輸出日志及內(nèi)存變化:
關(guān)于getByteCount和getAllocationByteCount的區(qū)別,這里暫時(shí)不討論暑椰,只要知道他們都可以獲取Bitmap占用內(nèi)存大小
可以看到霍转,由于這張圖片是放在手機(jī)內(nèi)部SD卡上,所以showBitmapInfos 解析后獲取的圖片寬高信息和之前是一致的一汽,寬x高為 2160x1920避消。看到所占用的內(nèi)存 15M召夹,是不是有點(diǎn)意外岩喷,一張658KB 的加載后居然要占這么大的內(nèi)存。在看一下monitor檢測(cè)的內(nèi)存變化监憎,在20s后選擇圖片后纱意,占用內(nèi)存有了一個(gè)明顯的上升。占用這么大的內(nèi)存鲸阔,顯然是不好的偷霉∑可能很多人和我一樣,在這個(gè)時(shí)候想到的第一個(gè)詞是壓縮圖片类少,把圖片變小他占的內(nèi)存不就會(huì)變小了嗎叙身?好,那就壓縮圖片
壓縮圖片
壓縮圖片方案一(Compress)
因?yàn)槲覀円幚淼氖荁itmap硫狞,首先從他自帶的方法出發(fā)曲梗,果然找到了一個(gè)compress方法。
private Bitmap getCompressedBitmap(Bitmap bitmap) {
try {
//創(chuàng)建一個(gè)用于存儲(chǔ)壓縮后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) 會(huì)按照指定的格式和壓縮比例將壓縮后的bitmap寫(xiě)入到os 所對(duì)應(yīng)的文件中妓忍。compressRate的取值在0-100之間,0表示壓縮到最小尺寸愧旦。
在ProcessResult方法中世剖,我們獲取bitmap后,首先通過(guò)上述方法將bitmap壓縮笤虫,然后在顯示到ImageView中旁瘫。我們看一下,壓縮過(guò)后的情況琼蚯。
上面的日志酬凳,第一個(gè)showBitmapInfos 顯示的是選擇的圖片通過(guò)BitmapFactory解析后的信息,第二個(gè)showBitmapInfos
顯示的壓縮后圖片的寬高信息遭庶,最后很意外宁仔,我們的壓縮方法似乎沒(méi)起到作用,占用的內(nèi)存沒(méi)有任何變化峦睡,依舊是15M翎苫。
難道是compress方法沒(méi)生效嗎?其實(shí)不然榨了,至少?gòu)腢I上看compress的確生效了煎谍, 當(dāng)compressRate=0時(shí),懶羊羊的圖片顯示到ImageView上時(shí)已經(jīng)非常不清晰了龙屉,失真非常嚴(yán)重呐粘。那么到底是為什么呢?
這里就得從概念上說(shuō)起转捕,一開(kāi)始我們提到了這張懶羊羊的圖片大小時(shí)658KB作岖,這是它在手機(jī)存儲(chǔ)空間所占的大小,而當(dāng)我們?cè)谶x擇這張圖片五芝,并解析為Bitmap時(shí)鳍咱,他所站的15MB是在內(nèi)存中所占的大小与柑;而compress方法只能壓縮前一種大小谤辜,也就是所使用Bitmap的compress方法只是壓縮他在存儲(chǔ)空間的大小蓄坏,結(jié)果就是導(dǎo)致圖片失真;而不能改變他在內(nèi)存中所占用的大小丑念。
那么怎樣才能讓Bitmap所占用的內(nèi)存變小呢涡戳?這就的從Bitmap占用內(nèi)存的計(jì)算方法入手,在這篇文章中已經(jīng)對(duì)bitmap所占用內(nèi)存大小做了深入分析脯倚,從中我們可以得出結(jié)論渔彰,決定一張圖片所占內(nèi)存大小的因素是圖片的寬高和Bitmap的格式。這里我們加載的時(shí)候?qū)itmap格式未做更改推正,也就是默認(rèn)的ARGB_8888恍涂,因此我們就得從寬高入手,得出如下的壓縮方案植榕。
壓縮圖片方案二 (Crop)
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);
}
這里調(diào)用了系統(tǒng)自帶的圖片裁剪控件再沧,并創(chuàng)建了一個(gè)copyFile 的文件,裁剪過(guò)后的圖片的地址指向就是這個(gè)文件所對(duì)應(yīng)的地址尊残。
當(dāng)cropTargetWidth=1080炒瘸,cropTargetHeight=920時(shí),我們看一下日志:
可以看到寝衫,Bitmap所占用的內(nèi)存終于變小了顷扩,而且由于在裁剪時(shí)寬高各縮小了1/2,整個(gè)內(nèi)存的占用也是縮小了1/4慰毅,變成了3.9M左右隘截。同時(shí)圖片在手機(jī)存儲(chǔ)空間也變小了。
當(dāng)然汹胃,這里要注意的是技俐,com.android.camera.action.CROP 中兩個(gè)參數(shù) "outputX" 和"outputY",決定了壓縮后圖片的大小统台,因此當(dāng)這兩個(gè)值的大小超過(guò)原始圖片的大小時(shí)雕擂,內(nèi)存占用反而會(huì)增加,這一點(diǎn)應(yīng)該很好理解贱勃,所以需確保傳遞合適的值井赌,否則會(huì)適得其反。
圖片壓縮方案三 (Sample )
采用Sample贵扰,也就是是采樣的方式壓縮圖片之前仇穗,我們首先需要了解一下inSampleSize 這個(gè)參數(shù)。
inSampleSize 是BitmapFactory.Options 的一個(gè)參數(shù)戚绕,當(dāng)他為1時(shí)纹坐,采樣后的圖片大小為圖片原始大小舞丛;當(dāng)inSampleSize 為2時(shí)耘子,那么采樣后的圖片其寬/高均為原圖大小的1/2果漾,而像素?cái)?shù)為原圖的1/4,其占有的內(nèi)存大小也為原圖的1/4谷誓。inSampleSize 的取值應(yīng)該是2的指數(shù)绒障。
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;
}
可以如下調(diào)用這個(gè)方法:
if (needSample) {
bitmap = getRealCompressedBitmap(pathName, 200, 200);
}
我們希望將2160x1920像素的原圖壓縮到200x200 像素的大小,因此在getRealCompressedBitmap方法中捍歪,通過(guò)while循環(huán)inSampleSize的值最終為8户辱,因此內(nèi)存占用率將變?yōu)樵瓉?lái)的1/64,這是一個(gè)很大的降幅糙臼。我們看一下日志庐镐,看看到底是否能夠如我們所愿:
可以看到,使用這種方法進(jìn)行圖片壓縮后变逃,增加的內(nèi)存只有0.24M必逆,幾乎可以忽略不計(jì)了。當(dāng)然前提是我們要使用的圖片的確不需要很大韧献,比如這里,需要用這張圖片作為用戶頭像的話研叫,那么將原圖縮略成200x200 px的大小是沒(méi)有問(wèn)題的锤窑。
三種方案對(duì)比
上面提到的三種壓縮方案,通過(guò)對(duì)比可以發(fā)現(xiàn)嚷炉,第一種方案適用于進(jìn)行純粹的文件壓縮渊啰,而不適用進(jìn)行圖像處理壓縮;第二種方案壓縮方案適用于進(jìn)行圖像編輯時(shí)的壓縮申屹,就像手機(jī)自帶相冊(cè)的編輯功能绘证,可以隨著裁剪區(qū)域的大小進(jìn)行最終的壓縮;第三種方案相對(duì)來(lái)說(shuō)哗讥,適應(yīng)性較強(qiáng)嚷那,各種場(chǎng)景都會(huì)符合。
從Camera 獲取Bitmap
有時(shí)候杆煞,我們除了從相冊(cè)獲取圖片之外魏宽,還可以通過(guò)手機(jī)自帶的相機(jī)拍攝圖片。
private void openCamera() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//創(chuàng)建一個(gè)臨時(shí)文件夾存儲(chǔ)拍攝的照片
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);
}
}
不同于從相冊(cè)選取圖片决乎,打開(kāi)相機(jī)之前需要我們自己定義一個(gè)存儲(chǔ)圖片的臨時(shí)文件file队询,這個(gè)臨時(shí)文件既可以在應(yīng)用的臨時(shí)存儲(chǔ)區(qū)也可以在手機(jī)存儲(chǔ)的臨時(shí)存儲(chǔ)區(qū);通過(guò)這個(gè)文件就可以生成一個(gè)Uri對(duì)象构诚,有了這個(gè)Uri對(duì)象蚌斩,相機(jī)拍攝完照片之后就可以在onActivityResult方法中通過(guò)這個(gè)Uri獲取到Bitmap了。
這里我們可以試一下范嘱,隨便用手機(jī)拍攝一張圖片轉(zhuǎn)為Bitmap加載會(huì)占多大的手機(jī)內(nèi)存(以我用的小米手機(jī)5為列送膳,拍攝一張圖片):
可以看到這張圖片的分辨率達(dá)到了3456x4608 像素员魏,而他加載到內(nèi)存是所占的大小居然達(dá)到了60M,這是非常不科學(xué)的做法肠缨,也是毫無(wú)意義的做法逆趋,因?yàn)槲覀兊氖謾C(jī)可見(jiàn)區(qū)域并沒(méi)有這么大,將整張照片完全加載是沒(méi)有意義的晒奕。因此可以按照之前的壓縮方案進(jìn)行壓縮闻书。
bitmap = getRealCompressedBitmap(pathName, screenWidth, screenHeight);
我們可以將原來(lái)的圖片壓縮到手機(jī)屏幕大小的圖片
可以看到占用內(nèi)存有了明顯的減少。
將拍攝的圖片添加到手機(jī)相冊(cè)中
有時(shí)需要將拍攝出來(lái)的照片添加到手機(jī)相冊(cè)中脑慧,方便從相冊(cè)直接查看
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 方法的實(shí)現(xiàn)很簡(jiǎn)單魄眉,就是將imageUri 這個(gè)地址的文件復(fù)制到galleryUri 這個(gè)地址,復(fù)制成功后發(fā)送一條
action="ACTION_MEDIA_SCANNER_SCAN_FILE" 的廣播即可闷袒。
好了坑律,關(guān)于Bitmap的初探就說(shuō)到這里,對(duì)于上面提到的各種壓縮方案囊骤,有興趣的同學(xué)可結(jié)合一下demo測(cè)試晃择。Github 地址
總結(jié)##
用了很久的ImageView,發(fā)現(xiàn)Bitmap才是Android中圖像處理最核心的東西也物,有很多東西值得去深入了解宫屠。