在日常開發(fā)中,可以說和Bitmap低頭不見抬頭見廓俭,基本上每個應(yīng)用都會直接或間接的用到云石,而這里面又涉及到大量的相關(guān)知識。 所以這里把Bitmap的常用知識做個梳理研乒,限于經(jīng)驗(yàn)和能力汹忠,不做太深入的分析。
Bitmap內(nèi)存模型
- 在Android 2.2(API8)之前,當(dāng)GC工作時宽菜,應(yīng)用的線程會暫停工作谣膳,同步的GC會影響性能。而Android2.3之后赋焕,GC變成了并發(fā)的参歹,意味著Bitmap沒有引用的時候其占有的內(nèi)存會很快被回收。
- 在Android 2.3.3(API10)之前隆判,Bitmap的像素數(shù)據(jù)存放在Native內(nèi)存犬庇,而Bitmap對象本身則存放在Dalvik Heap中。Native內(nèi)存中的像素數(shù)據(jù)并不會以可預(yù)測的方式進(jìn)行同步回收侨嘀,有可能會導(dǎo)致內(nèi)存升高甚至OOM臭挽。而在Android3.0之后,Bitmap的像素數(shù)據(jù)也被放在了Dalvik Heap中咬腕。
Bitmap內(nèi)存占用
手動計算
計算Bitmap內(nèi)存占用分為兩種情況:
- 使用BitmapFactory.decodeResource()加載本地資源文件的方式
無論是使用decodeResource(Resources res, int id)還是使用decodeResource(Resources res, int id, BitmapFactory.Options opts)其內(nèi)存占用的計算方式都是: width * height * inTargetDensity / inDensity * inTargetDensity / inDensity * 一個像素所占的內(nèi)存欢峰。
- 使用BitmapFactory.decodeResource()以外的方式,計算方式是: width * height *一個像素所占的內(nèi)存。
所用參數(shù)解釋一下:
- width:圖片的原始像素寬度涨共。
- height:圖片的原始像素高度纽帖。
- inTargetDensity:目標(biāo)設(shè)備的屏幕密度,例如一臺手機(jī)的屏>幕密度是640dp,那么inTargetDensity的值就是640dp举反。
- inDensity:這個值跟這張圖片的放置的目錄有關(guān)(比如 hdpi >240懊直,xxhdpi 是480)。
- 一個像素所占的內(nèi)存:使用Bitmap.Config來描述一個像素所>占用的內(nèi)存火鼻,Bitmap.Config有四個取值,分別是:
- ARGB_8888: 每個像素4字節(jié),每個通道8位室囊,四通道共32位,圖片質(zhì)量是最高的魁索,但是占用的內(nèi)存也是最大的融撞,是 默認(rèn)設(shè)置。
- RGB_565:共16位粗蔚,2字節(jié)尝偎,只存儲RGB值,圖片失真小,沒有透明度支鸡,可用于不需要透明度是圖片冬念。
- Alpha_8: 只有A通道,沒有顏色值牧挣,即只保存透明度急前,共8位,1字節(jié)瀑构,可用于設(shè)置遮蓋效果裆针。
- ARGB_4444: 刨摩,每個通道均占用4位,共16位世吨,2字節(jié)澡刹,嚴(yán)重失真,基本不使用耘婚。
Android API 的方法
getByteCount()
getByteCount()方法是在API12加入的罢浇,代表存儲Bitmap的色素需要的最少內(nèi)存。API19開始getAllocationByteCount()方法代替了getByteCount()沐祷。
getAllocationByteCount()
API19之后嚷闭,Bitmap加了一個Api:getAllocationByteCount();代表在內(nèi)存中為Bitmap分配的內(nèi)存大小赖临。
public final int getAllocationByteCount() {
if (mBuffer == null) {
//mBuffer代表存儲Bitmap像素數(shù)據(jù)的字節(jié)數(shù)組胞锰。
return getByteCount();
}
return mBuffer.length;
}
getByteCount()與getAllocationByteCount()的區(qū)別
- 一般情況下兩者是相等的;
- 通過復(fù)用Bitmap來解碼圖片兢榨,如果被復(fù)用的Bitmap的內(nèi)存比待分配內(nèi)存的Bitmap大,那么getByteCount()表示新解碼圖片占用內(nèi)存的大行衢拧(并非實(shí)際內(nèi)存大小,實(shí)際大小是復(fù)用的那個Bitmap的大小)吵聪,getAllocationByteCount()表示被復(fù)用Bitmap真實(shí)占用的內(nèi)存大辛枘恰(即mBuffer的長度)。
Bitmap的創(chuàng)建
通常我們可以利用Bitmap的靜態(tài)方法createBitmap()和BitmapFactory的decode系列靜態(tài)方法創(chuàng)建Bitmap對象吟逝。
Bitmap.createBitmap
主要用于圖片的操作,例如圖片的縮放案怯,裁剪等。
BitmapFactory
注意:decodeFile 和 decodeResource 其實(shí)最終都會調(diào)用 decodeStream 方法來解析Bitmap 澎办。有一個特別有意思的事情是,在 decodeResource 調(diào)用 decodeStream 之前還會調(diào)用 decodeResourceStream 這個方法,這個方法主要對 Options進(jìn)行處理金砍,在得到opts.inDensity的屬性前提下局蚀,如果沒有對該屬性的設(shè)定值,那么opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;這個值默認(rèn)為標(biāo)準(zhǔn)dpi的基值:160恕稠。如果沒有設(shè)定opts.inTargetDensity的值時琅绅,opts.inTargetDensity = res.getDisplayMetrics().densityDpi; 該值為當(dāng)前設(shè)備的 densityDpi,這個值是根據(jù)你放置在 drawable 下的文件不同而不同的鹅巍。所以說 decodeResourceStream 這個方法主要對 opts.inDensity 和 opts.inTargetDensity進(jìn)行賦值千扶。
盡量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource來設(shè)置一張大圖,因?yàn)檫@些函數(shù)在完成decode后骆捧,最終都是通過java層的createBitmap來完成的澎羞,需要消耗更多內(nèi)存,可以通過BitmapFactory.decodeStream方法敛苇,創(chuàng)建出一個bitmap妆绞,再將其設(shè)為ImageView的 source。
Resource資源加載的方式相當(dāng)?shù)暮馁M(fèi)內(nèi)存,建議采用通過InputStream ins = resources.openRawResource(resourcesId);然后使用decodeStream代替decodeResource獲取Bitmap括饶。這么做的好處是:
- BitmapFactory.decodeResource 加載的圖片可能會經(jīng)過縮放株茶,該縮放目前是放在 java 層做的,效率比較低图焰,而且需要消耗 java 層的內(nèi)存启盛。因此,如果大量使用該接口加載圖片技羔,容易導(dǎo)致OOM錯誤僵闯。
- BitmapFactory.decodeStream 不會對所加載的圖片進(jìn)行縮放,相比之下占用內(nèi)存少堕阔,效率更高棍厂。
這兩個接口各有用處,如果對性能要求較高超陆,則應(yīng)該使用 decodeStream牺弹;如果對性能要求不高,且需要 Android 自帶的圖片自適應(yīng)縮放功能时呀,則可以使用 decodeResource张漂。
Bitmap 于 drawable 的相互轉(zhuǎn)換
Bitmap 轉(zhuǎn) drawable
Drawable newBitmapDrawable = new BitmapDrawable(bitmap);
還可以從BitmapDrawable中獲取Bitmap對象
Bitmap bitmap = new BitmapDrawable.getBitmap();
drawable 轉(zhuǎn) Bitmap
- BitmapFactory 中的 decodeResource 方法
Resources res = getResources();
Bitmap bmp = BitmapFactory.decodeResource(res, R.drawable.ic_drawable);
- 將 Drable 對象先轉(zhuǎn)化成 BitmapDrawable ,然后調(diào)用 getBitmap 方法 獲取
Resource res = gerResource();
Drawable drawable = res.getDrawable(R.drawable.ic_drawable);//獲取drawable
BitmapDrawable bd = (BitmapDrawable) drawable;
Bitmap bm = bd.getBitmap();
- 根據(jù)已有的Drawable創(chuàng)建一個新的Bitmap
public static Bitmap drawableToBitmap(Drawable drawable) {
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
System.out.println("Drawable轉(zhuǎn)Bitmap");
Bitmap.Config config =
drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888
: Bitmap.Config.RGB_565;
Bitmap bitmap = Bitmap.createBitmap(w, h, config);
//注意谨娜,下面三行代碼要用到航攒,否則在View或者SurfaceView里的canvas.drawBitmap會看不到圖
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, w, h);
drawable.draw(canvas);
return bitmap;
}
BitmapFactory.Options的屬性解析
- inJustDecodeBounds:如果這個值為 true ,那么在解碼的時候?qū)⒉粫祷?Bitmap 趴梢,只會返回這個 Bitmap 的尺寸漠畜。這個屬性的目的是,如果你只想知道一個 Bitmap 的尺寸坞靶,但又不想將其加載到內(nèi)存中時憔狞,是一個非常好用的屬性。
- outWidth和outHeight:表示這個 Bitmap 的寬和高彰阴,一般和 inJustDecodeBounds 一起使用來獲得 Bitmap的寬高瘾敢,但是不加載到內(nèi)存
- inSampleSize:壓縮圖片時采樣率的值,如果這個值大于1尿这,那么就會按照比例(1 / inSampleSize)來縮小 Bitmap 的寬和高簇抵。如果這個值為 2,那么 Bitmap 的寬為原來的1/2射众,高為原來的1/2碟摆,那么這個 Bitmap 是所占內(nèi)存像素值會縮小為原來的 1/4。
- inDensity:表示這個 Bitmap 的像素密度叨橱,對應(yīng)的是 DisplayMetrics 中的 densityDpi焦履,不是 density拓劝。(如果不明白它倆之間的異同,可以看我的 Android 屏幕各種參數(shù)的介紹和學(xué)習(xí) )
- inTargetDensity:表示要被新 Bitmap 的目標(biāo)像素密度嘉裤,對應(yīng)的是 DisplayMetrics 中的 densityDpi郑临。
- inScreenDensity:表示實(shí)際設(shè)備的像素密度,對應(yīng)的是 DisplayMetrics 中的 densityDpi屑宠。
- inPreferredConfig:這個值是設(shè)置色彩模式厢洞,默認(rèn)值是 ARGB_8888,這個模式下,一個像素點(diǎn)占用 4Byte 典奉。RGB_565 占用 2Byte躺翻,ARGB_4444 占用 4Byte(以廢棄)。
- inPremultiplied:這個值和透明度通道有關(guān)卫玖,默認(rèn)值是 true公你,如果設(shè)置為 true,則返回的 Bitmap 的顏色通道上會預(yù)先附加上透明度通道假瞬。
- inScaled:設(shè)置這個Bitmap 是否可以被縮放陕靠,默認(rèn)值是 true,表示可以被縮放脱茉。
- inMutable:若為true剪芥,則返回的Bitmap是可變的,可以作為Canvas的底層Bitmap使用琴许。
若為false税肪,則返回的Bitmap是不可變的,只能進(jìn)行讀操作榜田。
如果要修改Bitmap益兄,那就必須返回可變的bitmap,例如:修改某個像素的顏色值(setPixel) - inBitmap:這個參數(shù)用來實(shí)現(xiàn) Bitmap 內(nèi)存的復(fù)用箭券,但復(fù)用存在一些限制偏塞,具體體現(xiàn)在:在 Android 4.4 之前只能重用相同大小的 Bitmap 的內(nèi)存收班,而 Android 4.4 及以后版本則只要后來的 Bitmap 比之前的小即可但指。使用 inBitmap 參數(shù)前成艘,每創(chuàng)建一個 Bitmap 對象都會分配一塊內(nèi)存供其使用,而使用了 inBitmap 參數(shù)后丰榴,多個 Bitmap 可以復(fù)用一塊內(nèi)存,這樣可以提高性能。
Bitmap如何復(fù)用
使用inBitmap能夠大大提高內(nèi)存的利用效率滔以,但是它也有幾個限制條件:
- Bitmap復(fù)用首選需要其 mIsMutable 屬性為 true , mIsMutable 的表面意思為:易變的
在Bitmap中的意思為: 控制bitmap的setPixel方法能否使用,也就是外界能否修改bitmap的像素氓拼。mIsMutable 屬性為 true 那么就可以修改Bitmap的像素數(shù)據(jù)你画,這樣也就可以實(shí)現(xiàn)Bitmap對象的復(fù)用了抵碟。
在SDK 11 -> 18之間,重用的bitmap大小必須是一致的坏匪,例如給inBitmap賦值的圖片大小為100-100拟逮,那么新申請的bitmap必須也為100-100才能夠被重用。
被復(fù)用的Bitmap必須是Mutable,即inMutable的值為true适滓。違反此限制敦迄,不會拋出異常,且會返回新申請內(nèi)存的Bitmap凭迹。
從SDK 19開始罚屋,新申請的bitmap大小必須小于或者等于已經(jīng)賦值過的bitmap大小。違反此限制嗅绸,將會導(dǎo)致復(fù)用失敗脾猛,拋出異常IllegalArgumentException(Problem decoding into existing bitmap)
新申請的bitmap與舊的bitmap必須有相同的解碼格式,例如大家都是8888的鱼鸠,如果前面的bitmap是8888猛拴,那么就不能支持4444與565格式的bitmap了,不過可以通過創(chuàng)建一個包含多種典型可重用bitmap的對象池瞧柔,這樣后續(xù)的bitmap創(chuàng)建都能夠找到合適的“模板”去進(jìn)行重用漆弄。
Bitmap如何壓縮
質(zhì)量壓縮
質(zhì)量壓縮不會改變圖片的像素點(diǎn),即我們使用完質(zhì)量壓縮后造锅,在轉(zhuǎn)換Bitmap時占用內(nèi)存依舊不會減小撼唾。但是可以減少我們存儲在本地文件的大小,即放到 disk上的大小哥蔚。
/**
* 質(zhì)量壓縮方法,并不能減小加載到內(nèi)存時所占用內(nèi)存的空間倒谷,應(yīng)該是減小的所占用磁盤的空間
* @param image
* @param compressFormat
* @return
*/
public static Bitmap compressbyQuality(Bitmap image, Bitmap.CompressFormat compressFormat) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//質(zhì)量壓縮方法,這里100表示不壓縮糙箍,把壓縮后的數(shù)據(jù)存放到baos中
image.compress(compressFormat, 100, baos);
int quality = 100;
//循環(huán)判斷如果壓縮后圖片是否大于100kb,大于繼續(xù)壓縮
while ( baos.toByteArray().length / 1024 > 100) {
baos.reset();//重置baos即清空baos
if(quality > 10){
quality -= 20;//每次都減少20
}else {
break;
}
//這里壓縮options%渤愁,把壓縮后的數(shù)據(jù)存放到baos中
image.compress(Bitmap.CompressFormat.JPEG,quality,baos);
}
//把壓縮后的數(shù)據(jù)baos存放到ByteArrayInputStream中
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
//把ByteArrayInputStream數(shù)據(jù)生成圖片
Bitmap bmp = BitmapFactory.decodeStream(isBm, null, options);
return bmp;
}
采樣壓縮
這個方法主要用在圖片資源本身較大,或者適當(dāng)?shù)夭蓸硬⒉粫绊懸曈X效果的條件下深夯,這時候我們輸出的目標(biāo)可能相對的較小抖格,對圖片的大小和分辨率都減小。
壓縮格式 CompressFormat
- Bitmap.CompressFormat.JPEG
- 一種有損壓縮(JPEG2000既可以有損也可以無損)咕晋,".jpg"或者".jpeg";
- 優(yōu)點(diǎn):采用了直接色雹拄,有豐富的色彩,適合存儲照片和生動圖像效果掌呜;缺點(diǎn):有損滓玖,不適合用來存儲logo、線框類圖
- Bitmap.CompressFormat.PNG
- 一種無損壓縮质蕉,".png";
- PNG 格式是無損的势篡,它無法再進(jìn)行質(zhì)量壓縮翩肌,quality 這個參數(shù)就沒有作用了,會被忽略禁悠,所以最后圖片保存成的文件大小不會有變化念祭;
- 優(yōu)點(diǎn):支持透明、無損绷蹲,主要用于小圖標(biāo)棒卷,透明背景等;
- 缺點(diǎn):若色彩復(fù)雜祝钢,則圖片生成后文件很大比规;
- Bitmap.CompressFormat.WEBP
- 以WebP算法進(jìn)行壓縮;
- Google開發(fā)的新的圖片格式拦英,同時支持無損和有損壓縮蜒什,使用直接色。
- 無損壓縮疤估,相同質(zhì)量的webp比PNG小大約26%灾常;
- 有損壓縮,相同質(zhì)量的webp比JPEG小25%-34% 支持動圖铃拇,基本取代gif
- 缺點(diǎn):解壓速度慢
**
* 采樣率壓縮,這個和矩陣來實(shí)現(xiàn)縮放有點(diǎn)類似钞瀑,但是有一個原則是“大圖小用用采樣,小圖大用用矩陣”慷荔。
* 也可以先用采樣來壓縮圖片雕什,這樣內(nèi)存小了,可是圖的尺寸也小显晶。如果要是用 Canvas 來繪制這張圖時贷岸,再用矩陣放大
* @param image
* @param compressFormat
* @param requestWidth 要求的寬度
* @param requestHeight 要求的長度
* @return
*/
public static Bitmap compressbySample(Bitmap image, Bitmap.CompressFormat compressFormat, int requestWidth, int requestHeight){
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//質(zhì)量壓縮方法,這里100表示不壓縮磷雇,把壓縮后的數(shù)據(jù)存放到baos中
image.compress(compressFormat,100,baos);
//把壓縮后的數(shù)據(jù)baos存放到ByteArrayInputStream中
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inPurgeable = true;
//只讀取圖片的頭信息偿警,不去解析真是的位圖
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(isBm,null,options);
options.inSampleSize = calculateInSampleSize(options,requestWidth,requestHeight);
//-------------inBitmap------------------
options.inMutable = true;
try{
Bitmap inBitmap = Bitmap.createBitmap(options.outWidth, options.outHeight, Bitmap.Config.RGB_565);
if (inBitmap != null && canUseForInBitmap(inBitmap, options)) {
options.inBitmap = inBitmap;
}
}catch (OutOfMemoryError e){
options.inBitmap = null;
System.gc();
}
//---------------------------------------
options.inJustDecodeBounds = false;//真正的解析位圖
isBm.reset();
Bitmap compressBitmap;
try{
compressBitmap = BitmapFactory.decodeStream(isBm, null, options);//把ByteArrayInputStream數(shù)據(jù)生成圖片
}catch (OutOfMemoryError e){
compressBitmap = null;
System.gc();
}
return compressBitmap;
}
/**
* 采樣壓縮比例
* @param options
* @param reqWidth 要求的寬度
* @param reqHeight 要求的長度
* @return
*/
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
int originalWidth = options.outWidth;
int originalHeight = options.outHeight;
int inSampleSize = 1;
if (originalHeight > reqHeight || originalWidth > reqHeight){
// 計算出實(shí)際寬高和目標(biāo)寬高的比率
final int heightRatio = Math.round((float) originalHeight / (float) reqHeight);
final int widthRatio = Math.round((float) originalWidth / (float) reqWidth);
// 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高
// 一定都會大于等于目標(biāo)的寬和高唯笙。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
使用矩陣
前面我們采用了采樣壓縮螟蒸,Bitmap 所占用的內(nèi)存是小了,可是圖的尺寸也小了崩掘。當(dāng)我們需要尺寸較大時該怎么辦七嫌?我們要用用 Canvas 繪制怎么辦?當(dāng)然可以用矩陣(Matrix)
/**
* 矩陣縮放圖片
* @param sourceBitmap
* @param width 要縮放到的寬度
* @param height 要縮放到的長度
* @return
*/
private Bitmap getScaleBitmap(Bitmap sourceBitmap,float width,float height){
Bitmap scaleBitmap;
//定義矩陣對象
Matrix matrix = new Matrix();
float scale_x = width/sourceBitmap.getWidth();
float scale_y = height/sourceBitmap.getHeight();
matrix.postScale(scale_x,scale_y);
try {
scaleBitmap = Bitmap.createBitmap(sourceBitmap,0,0,sourceBitmap.getWidth(),sourceBitmap.getHeight(),matrix,true);
}catch (OutOfMemoryError e){
scaleBitmap = null;
System.gc();
}
return scaleBitmap;
}