一直以來(lái)Bitmap都是開(kāi)發(fā)中很棘手的問(wèn)題躏精,這個(gè)問(wèn)題就是傳說(shuō)中的OOM(java.lang.OutofMemoryError - 內(nèi)存溢出)逾滥,那么Bitmap為何如此喪失乔外,令無(wú)數(shù)Android開(kāi)發(fā)者所懊惱视卢?
由于每個(gè)機(jī)型在編譯ROM時(shí)都設(shè)置了一個(gè)應(yīng)用堆內(nèi)存VM值上限dalvik.vm.heapgrowthlimit巢株,用來(lái)限定每個(gè)應(yīng)用可用的最大內(nèi)存槐瑞,超出這個(gè)最大值將會(huì)報(bào)OOM。這個(gè)閥值阁苞,一般根據(jù)手機(jī)屏幕dpi大小遞增困檩,dpi越小的手機(jī)祠挫,每個(gè)應(yīng)用可用最大內(nèi)存就越低。例如我的Z3c悼沿,xhdpi的VM閥值是192M等舔,但是到了nexus s hdpi上只有可憐的48M。這樣糟趾,當(dāng)一個(gè)activity中加載多張大圖后慌植,就很容易OOM了。有關(guān)應(yīng)用內(nèi)存閥值的參考可以看這里3.7節(jié):
http://static.googleusercontent.com/media/source.android.com/en//compatibility/android-cdd.pdf
圖片分辨率越高义郑,消耗的內(nèi)存越高蝶柿,當(dāng)加載高分辨率圖片的時(shí)候,將會(huì)非常占用內(nèi)存非驮,一旦處理不當(dāng)就會(huì)OOM交汤。例如,一張500W像素的照片的分辨率是:2592x1936院尔。如果Bitmap使用 ARGB_8888 32位來(lái)平鋪顯示的話蜻展,占用的內(nèi)存是2592x1936x4個(gè)字節(jié)喉誊,占用將近19M內(nèi)存邀摆,my god,加載不到10張這種高質(zhì)量照片伍茄,應(yīng)用將直接掛掉栋盹,報(bào)OOM
在使用ListView, GridView等這些大量加載view的組件時(shí),如果沒(méi)有合理的處理緩存敷矫,大量加載Bitmap的時(shí)候例获,也將容易引發(fā)OOM
工欲善其事必先利其器曹仗,想要高效加載Bitmap榨汤,了解Bitmap是必不可少的。Bitmap有幾個(gè)重要的成員變量和方法怎茫,下面開(kāi)始介紹:
一張圖片Bitmap所占用的內(nèi)存 =圖片長(zhǎng)度 x 圖片寬度 x 一個(gè)像素點(diǎn)占用的字節(jié)數(shù)
而B(niǎo)itmap.Config收壕,正是指定單位像素占用的字節(jié)數(shù)的重要參數(shù)。
其中轨蛤,A代表透明度蜜宪;R代表紅色;G代表綠色祥山;B代表藍(lán)色圃验。
ALPHA_8
表示8位Alpha位圖,即A=8,一個(gè)像素點(diǎn)占用1個(gè)字節(jié),它沒(méi)有顏色,只有透明度
ARGB_4444
表示16位ARGB位圖,即A=4,R=4,G=4,B=4,一個(gè)像素點(diǎn)占4+4+4+4=16位缝呕,2個(gè)字節(jié)
ARGB_8888
表示32位ARGB位圖斧散,即A=8,R=8,G=8,B=8,一個(gè)像素點(diǎn)占8+8+8+8=32位栗精,4個(gè)字節(jié)
RGB_565
表示16位RGB位圖,即R=5,G=6,B=5,它沒(méi)有透明度,一個(gè)像素點(diǎn)占5+6+5=16位鹿寨,2個(gè)字節(jié)
Bitmap.Config主要作用是:以何種方式像素存儲(chǔ)脚草。不同的配置將會(huì)影響圖像的畫(huà)質(zhì)(色彩深度)原献,位數(shù)越高畫(huà)質(zhì)越高馏慨,顯然在這里ARGB_8888是最占內(nèi)存的。當(dāng)然姑隅,畫(huà)質(zhì)越高也就越占內(nèi)存了。
Tips:由于ARGB_4444的畫(huà)質(zhì)慘不忍睹讲仰,一般假如對(duì)圖片沒(méi)有透明度要求的話慕趴,可以改成RGB_565冕房,相比ARGB_8888將節(jié)省一半的內(nèi)存開(kāi)銷(xiāo)详拙。
2.1.1 配置不同Bitmap.Config在相同分辨率下的占用內(nèi)存情況
一張圖片Bitmap所占用的內(nèi)存 = 圖片長(zhǎng)度 x 圖片寬度 x 一個(gè)像素點(diǎn)占用的字節(jié)數(shù)
Bitmap.Config分辨率100x100的圖片占用內(nèi)存的大小
ALPHA_8100x100x1 = 10000 byte ~= 9.77 KB
ARGB_4444100x100x2 = 20000 byte ~= 19.53 kb
ARGB_8888100x100x4 = 40000 byte ~= 39.06 KB
RGB_565100x100x2 = 20000 byte ~= 19.53 KB
在Android里面可以通過(guò)下面的代碼來(lái)設(shè)置解碼率:
從字面上理解蹋宦,它的含義是:Bitmap壓縮格式
publicenumCompressFormat {? ? JPEG? ? (0),? ? PNG? ? (1),? ? WEBP? ? (2);? ? CompressFormat(intnativeInt) {this.nativeInt = nativeInt;? ? }? ? finalintnativeInt;}
嗯拇泛,其實(shí)這個(gè)參數(shù)很簡(jiǎn)單泰偿,就是指定Bitmap是以JPEG熄守、PNG還是WEBP格式來(lái)壓縮
重磅方法來(lái)了,通過(guò)這個(gè)方法耗跛,可以實(shí)現(xiàn)圖片的壓縮。使用該方法需要傳三個(gè)參數(shù)進(jìn)去:CompressFormat紊扬、int類(lèi)型的quality蜒茄、OutputStream
CompressFormat
指定Bitmap的壓縮格式檀葛,可選擇JPEG腹缩、PNG屿聋、WEBP
int類(lèi)型的quality
指定Bitmap的壓縮品質(zhì),范圍是0 ~ 100楚殿;該值越高竿痰,畫(huà)質(zhì)越高脆粥。0表示畫(huà)質(zhì)最差砌溺,100畫(huà)質(zhì)最高。
OutputStream
指定Bitmap的字節(jié)輸出流变隔。一般使用:
ByteArrayOutputStream stream = new ByteArrayOutputStream();
//Bitmap.compress()方法public boolean compress(CompressFormatformat,intquality,OutputStreamstream) {if(stream == null) {? ? ? ? throw newNullPointerException();? ? }if(quality <0|| quality >100) {? ? ? ? throw newIllegalArgumentException("quality must be 0..100");? ? }Trace.traceBegin(Trace.TRACE_TAG_RESOURCES,"Bitmap.compress");? ? booleanresult= nativeCompress(mFinalizer.mNativeBitmap, format.nativeInt,? ? ? ? ? ? quality, stream, new byte[WORKING_COMPRESS_STORAGE]);Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);returnresult;}
2.3.1 案例:將一個(gè)Bitmap壓縮成jpeg, quality為10规伐,代碼如下:
protectedvoid onCreate(BundlesavedInstanceState) {? ? super.onCreate(savedInstanceState);setContentView(R.layout.activity_test3);ImageView iv_1 = (ImageView) findViewById(R.id.iv_1);ImageView iv_2 = (ImageView) findViewById(R.id.iv_2);Bitmapbmp=BitmapFactory.decodeResource(this.getResources(), R.mipmap.test_pic);iv_1.setImageBitmap(bmp);ByteArrayOutputStreambos= newByteArrayOutputStream();bmp.compress(Bitmap.CompressFormat.JPEG,10,bos);byte[]bytes=bos.toByteArray();bmp=BitmapFactory.decodeByteArray(bytes,0,bytes.length);iv_2.setImageBitmap(bmp);}
運(yùn)行效果圖:【下面那張圖明顯要比上一張畫(huà)質(zhì)差了很多】
從上面那個(gè)案例的代碼可以發(fā)現(xiàn)匣缘,獲取Bitmap不是通過(guò)構(gòu)造new出來(lái)的猖闪,而是通過(guò)BitmapFactory”制造”出來(lái)的。BitmapFactory是獲取Bitmap和壓縮Bitmap的重要類(lèi)肌厨,下面開(kāi)始介紹BitmapFactory幾個(gè)重要的成員變量和方法:
3.1 通過(guò)BitmapFactory解碼(獲取)Bitmap的幾種方式
decodeFile()//從SD卡文件讀取
Bitmapbm=BitmapFactory.decodeFile(Environment.getExternalStorageDirectory().getAbsolutePath()+"/photo.jpg");
decodeResource()//從資源文件res讀取
Bitmapbm=BitmapFactory.decodeResource(this.getResources(), R.mipmap.test_pic);
decodeStream()//從輸入流讀取
Bitmapbm=BitmapFactory.decodeStream(inputStream);
decodeByteArray()//從字節(jié)數(shù)組讀取
Bitmapbm=BitmapFactory.decodeByteArray(bytes,0,bytes.length);
BitmapFactory在使用方法decodeFile()萧朝、decodeResource()解碼圖片時(shí),可以指定它的BitmapFactory.Options夏哭。這個(gè)參數(shù)作用非常大检柬,它可以設(shè)置Bitmap的采樣率,通過(guò)改變圖片的寬度竖配、高度何址、縮放比例等,以達(dá)到降低圖片的像素的目的进胯,這樣可以做到圖片壓縮用爪,減少Bitmap的內(nèi)存。
下面列出BitmapFactory.Options的部分成員變量:
publicBitmap inBitmap;publicbooleaninJustDecodeBounds;publicintinSampleSize;publicintinDensity;publicintinTargetDensity;publicintinScreenDensity;publicbooleaninScaled;publicintoutWidth;publicintoutHeight;publicStringoutMimeType;
看到這么多成員變量是不是傻了胁镐?no,no,no偎血,其實(shí)很簡(jiǎn)單。一句話總結(jié):in開(kāi)頭的代表的就是設(shè)置某某參數(shù);out開(kāi)頭的代表的就是獲取某某參數(shù)盯漂。比如颇玷,inSampleSize就是設(shè)置Bitmap的縮放比例、outWidth就是獲取Bitmap的高度就缆。
3.2.1 inJustDecodeBounds 設(shè)置只去讀圖片的附加信息(寬高),不去解析真實(shí)的Bitmap
從字面上理解帖渠,它的含義是:”設(shè)置僅解碼Bitmap的邊界”。那它真正的作用是啥呢竭宰?
當(dāng)inJustDecodeBounds設(shè)置為true的時(shí)候空郊,BitmapFactory通過(guò)decodeResource或者decodeFile解碼圖片時(shí),將會(huì)返回空(null)的Bitmap對(duì)象切揭,這樣可以避免Bitmap的內(nèi)存分配狞甚,但是它可以返回Bitmap的寬度、高度以及MimeType廓旬。
// 當(dāng)inJustDecodeBounds設(shè)置為true時(shí)哼审,獲取Bitmap的寬度、高度以及MimeTypeBitmapFactory.Optionsoptions=newBitmapFactory.Options();options.inJustDecodeBounds =true; BitmapFactory.decodeResource (getResources(), R.id.myimage,options);intimageHeight =options.outHeight ;intimageWidth =options.outWidth ; String imageType =options.outMimeType ;
那么這樣做有何意義呢?看完下面這段代碼棺蛛,你就知道這樣做有啥意義了怔蚌。意義就在于,可以先不用產(chǎn)生Bitmap內(nèi)存旁赊,從而獲得圖片的寬高信息桦踊,盡可能的做到節(jié)約內(nèi)存。
3.2.1.1 通過(guò)BitmapFactory.Options根據(jù)手機(jī)屏幕尺寸設(shè)置圖片的縮放比例
// 根據(jù)手機(jī)屏幕尺寸設(shè)置圖片的縮放比例【將大圖縮放】public class TestThreadActivity3 extends Activity {@TargetApi(Build.VERSION_CODES.KITKAT)@Overrideprotected void onCreate(Bundle savedInstanceState){? ? super.onCreate(savedInstanceState);? ? setContentView(R.layout.activity_test3);? ? ImageView iv_1 =(ImageView)findViewById(R.id.iv_1);? ? ImageView iv_2 =(ImageView)findViewById(R.id.iv_2);? ? BitmapFactory.Options opts = new BitmapFactory.Options();? ? opts.inJustDecodeBounds =true;//只去讀圖片的頭信息,不去解析真實(shí)的位圖Bitmap bmp = BitmapFactory.decodeResource(this.getResources(),? ? R.mipmap.test_pic2,opts);? ? WindowManager wm? = getWindowManager();? ? int screenWidth = wm.getDefaultDisplay().getWidth();//得到屏幕的寬度int screenheight = wm.getDefaultDisplay().getHeight();//得到屏幕的高度Log.e("屏幕寬度:",screenWidth+"");? ? Log.e("屏幕高度:", screenheight +"");? ? int picWidth = opts.outWidth;// 得到圖片寬度int picHeight = opts.outHeight;// 得到圖片高度Log.e("原圖片高度:",picHeight+"");? ? Log.e("原圖片寬度:", picWidth +"");//計(jì)算圖片縮放比例int dx = picWidth/screenWidth;? ? int dy = picHeight/screenheight;? ? Log.e("dx,dy",dx+","+dy+"");? ? intscale=1;if(dx>=dy&&dy>=1){? ? ? ? Log.e("按照水平方向縮放:",dx+"");scale= dx;? ? }if(dy>dx&&dx>=1){? ? ? ? Log.e("按照豎直方向縮放:", dy +"");scale= dy;? ? }? ? opts.inSampleSize =scale;//設(shè)置縮放比例opts.inJustDecodeBounds =false;//真正的去解析位圖bmp = BitmapFactory.decodeResource(this.getResources(), R.mipmap.test_pic2,opts);? ? int picWidth2 = opts.outWidth;// 得到圖片寬度int picHeight2 = opts.outHeight;// 得到圖片高度Log.e("壓縮后的圖片寬度:",picWidth2+"");? ? Log.e("壓縮后的圖片高度:", picHeight2 +"");? ? Log.e("壓縮后的圖占用內(nèi)存:",bmp.getByteCount()+"");? ? iv_2.setImageBitmap(bmp);}}
我們讀取一張3840x2400的圖片運(yùn)行結(jié)果:
原圖直接占用36M內(nèi)存终畅,如果直接設(shè)置的話將瞬間爆炸報(bào)OOM籍胯。所以我們這里先不加載Bitmap,而是只獲取寬和高离福,待縮放后杖狼,再進(jìn)行真實(shí)的加載Bitmap。
3.2.2 inSampleSize 設(shè)置圖片的縮放比例(寬和高)
在這里著重講一下這個(gè)inSampleSize妖爷。從字面上理解蝶涩,它的含義是:”設(shè)置取樣大小“。它的作用是:
設(shè)置inSampleSize的值(int類(lèi)型)后絮识,假如設(shè)為4绿聘,則寬和高都為原來(lái)的1/4,寬高都減少了次舌,自然內(nèi)存也降低了熄攘。
如圖所示:
在這里參考Google官方文檔來(lái)解釋?zhuān)?/p>
http://developer.android.com/intl/zh-cn/training/displaying-bitmaps/load-bitmap.html#load-bitmap
如何理解”設(shè)置取樣大小“呢?如果你認(rèn)真看了上面的內(nèi)容話彼念,聰明的你一定知道挪圾,肯定需要配合inJustDecodeBounds,先獲取圖片的寬逐沙、高【這個(gè)過(guò)程就是取樣】哲思,然后通過(guò)獲取的寬高,動(dòng)態(tài)的設(shè)置inSampleSize的值酱吝。
【當(dāng)然也殖,你也可以不動(dòng)態(tài),可以寫(xiě)死inSampleSize的值务热。比如設(shè)置inSampleSize = 4的話,一張分辨率為2048x1536px的圖像將使用inSampleSize值為4的設(shè)置來(lái)解碼己儒,產(chǎn)生的Bitmap大小約為512*384px崎岂。相較于完整圖片占用12M的內(nèi)存,這種方式只需0.75M內(nèi)存(假設(shè)Bitmap配置為ARGB_8888)闪湾〕甯剩】
這里再舉例演示一個(gè)動(dòng)態(tài)設(shè)置inSampleSize的案例代碼,可以通過(guò)設(shè)置圖片寬高來(lái)縮放圖片尺寸:
publicclassTestThreadActivity3extendsActivity{@TargetApi(Build.VERSION_CODES.KITKAT)@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);? ? setContentView(R.layout.activity_test3);? ? ImageView iv_1 = (ImageView) findViewById(R.id.iv_1);? ? ImageView iv_2 = (ImageView) findViewById(R.id.iv_2);? ? BitmapFactory.Options opts =newBitmapFactory.Options();? ? opts.inJustDecodeBounds =true;//只去讀圖片的附加信息,不去解析真實(shí)的位圖Bitmap bmp = BitmapFactory.? ? decodeResource(this.getResources(), R.mipmap.test_pic,opts);? ? Log.e("原圖占用內(nèi)存:", bmp.getByteCount() +"");? ? iv_1.setImageBitmap(bmp);intpicWidth = opts.outWidth;// 得到圖片寬度intpicHeight = opts.outHeight;// 得到圖片高度Log.e("原圖片高度:",picHeight+"");? ? Log.e("原圖片寬度:",picWidth+"");//根據(jù)100*100的寬高,設(shè)置縮放比例opts.inSampleSize = calculateInSampleSize(opts,100,100);? ? opts.inJustDecodeBounds =false;//真正的去解析位圖bmp = BitmapFactory.decodeResource(this.getResources(), R.mipmap.test_pic, opts);? ? Log.e("壓縮后的圖占用內(nèi)存:",bmp.getByteCount()+"");? ? iv_2.setImageBitmap(bmp);}publicstaticintcalculateInSampleSize(? ? ? ? BitmapFactory.Options options,intreqWidth,intreqHeight){finalintheight = options.outHeight;finalintwidth = options.outWidth;intinSampleSize =1;if(height > reqHeight || width > reqWidth) {finalinthalfHeight = height /2;finalinthalfWidth = width /2;while((halfHeight / inSampleSize) > reqHeight? ? ? ? ? ? ? ? && (halfWidth / inSampleSize) > reqWidth) {? ? ? ? ? ? inSampleSize *=2;? ? ? ? }? ? }? ? Log.e("inSampleSize:",inSampleSize+"");returninSampleSize;}}
將圖片壓縮成100x100px分辨率的運(yùn)行結(jié)果:
inBitmap的主要作用是復(fù)用之前bitmap在內(nèi)存中申請(qǐng)的內(nèi)存江醇,其實(shí)這是對(duì)象池的原理濒憋,以解決對(duì)象頻繁創(chuàng)建再回收的效率問(wèn)題。
使用inBitmap前陶夜,每創(chuàng)建一個(gè)bitmap需要獨(dú)占一塊內(nèi)存
使用inBitmap后凛驮,多個(gè)bitmap會(huì)復(fù)用同一塊內(nèi)存
所以使用inBitmap能夠大大提高內(nèi)存的利用效率,但是它也有幾個(gè)限制條件:
inBitmap只能在3.0以后使用条辟。在2.3上黔夭,bitmap的數(shù)據(jù)是存儲(chǔ)在native C的內(nèi)存區(qū)域,并不是在java dalvik的內(nèi)存堆上羽嫡。
在SDK 11 -> 18之間本姥,重用的bitmap大小必須是一致的,例如給inBitmap賦值的圖片大小為100-100杭棵,那么新申請(qǐng)的bitmap必須也為100-100才能夠被重用婚惫。從SDK 19開(kāi)始,新申請(qǐng)的bitmap大小必須小于或者等于已經(jīng)賦值過(guò)的bitmap大小魂爪。
新申請(qǐng)的bitmap與舊的bitmap必須有相同的解碼格式辰妙,例如大家都是8888的,如果前面的bitmap是8888甫窟,那么就不能支持4444與565格式的bitmap了密浑,不過(guò)可以通過(guò)創(chuàng)建一個(gè)包含多種典型可重用bitmap的對(duì)象池,這樣后續(xù)的bitmap創(chuàng)建都能夠找到合適的“模板”去進(jìn)行重用粗井。
下面是如何使用inBitmap的代碼示例:
1、Bitmap壓縮的兩種常用方法
質(zhì)量壓縮法Bitmap.compress()
參考2.3.1節(jié)的代碼
取樣壓縮法設(shè)置inSampleSize的值
參考3.2.1.1 和 3.2.2的代碼
2浇衬、在實(shí)際使用中可以結(jié)合質(zhì)量壓縮法和取樣壓縮法一起用懒构,以達(dá)到最佳壓縮效果。
3耘擂、看完了這篇內(nèi)容胆剧,其實(shí)說(shuō)白了,Bitmap壓縮都是圍繞這個(gè)來(lái)做文章:Bitmap所占用的內(nèi)存 = 圖片長(zhǎng)度 x 圖片寬度 x 一個(gè)像素點(diǎn)占用的字節(jié)數(shù)醉冤。3個(gè)參數(shù)秩霍,任意減少一個(gè)的值,就達(dá)到了壓縮的效果蚁阳。