在日常開發中,可以說和Bitmap低頭不見抬頭見,基本上每個應用都會直接或間接的用到,而這裡面又涉及到大量的相關知識。 所以這裡把Bitmap的常用知識做個梳理,限於經驗和能力,不做太深入的分析。
Bitmap記憶體模型
- 在Android 2.2(API8)之前,當GC工作時,應用的執行緒會暫停工作,同步的GC會影響效能。而Android2.3之後,GC變成了併發的,意味著Bitmap沒有引用的時候其佔有的記憶體會很快被回收。
- 在Android 2.3.3(API10)之前,Bitmap的畫素資料存放在Native記憶體,而Bitmap物件本身則存放在Dalvik Heap中。Native記憶體中的畫素資料並不會以可預測的方式進行同步回收,有可能會導致記憶體升高甚至OOM。而在Android3.0之後,Bitmap的畫素資料也被放在了Dalvik Heap中。
Bitmap記憶體佔用
手動計算
計算Bitmap記憶體佔用分為兩種情況:
-
使用
BitmapFactory.decodeResource()
載入本地資原始檔的方式無論是使用
decodeResource(Resources res, int id)
還是使用decodeResource(Resources res, int id, BitmapFactory.Options opts)
其記憶體佔用的計算方式都是:width * height * inTargetDensity / inDensity * inTargetDensity / inDensity * 一個畫素所佔的記憶體。
-
使用
BitmapFactory.decodeResource()
以外的方式,計算方式是:width * height *一個畫素所佔的記憶體。
所用引數解釋一下:
- width:圖片的原始畫素寬度。
- height:圖片的原始畫素高度。
- inTargetDensity:目標裝置的螢幕密度,例如一臺手機的螢幕密度是640dp,那麼
inTargetDensity
的值就是640dp。- inDensity:這個值跟這張圖片的放置的目錄有關(比如 hdpi 是240,xxhdpi 是480)。
- 一個畫素所佔的記憶體:使用
Bitmap.Config
來描述一個畫素所佔用的記憶體,Bitmap.Config
有四個取值,分別是:
- ARGB_8888: 每個畫素4位元組,每個通道8位,四通道共32位,圖片質量是最高的,但是佔用的記憶體也是最大的,是
預設設定
。- RGB_565:共16位,2位元組,只儲存RGB值,圖片失真小,沒有透明度,可用於不需要透明度是圖片。
- Alpha_8: 只有A通道,沒有顏色值,即只儲存透明度,共8位,1位元組,可用於設定遮蓋效果。
- ARGB_4444: ,每個通道均佔用4位,共16位,2位元組,嚴重失真,基本不使用。
Android API 的方法
getByteCount()
getByteCount()方法是在API12加入的,代表儲存Bitmap的色素需要的最少記憶體。API19開始getAllocationByteCount()方法代替了getByteCount()。
getAllocationByteCount()
API19之後,Bitmap加了一個Api:getAllocationByteCount();代表在記憶體中為Bitmap分配的記憶體大小。
public final int getAllocationByteCount() {
if (mBuffer == null) {
//mBuffer代表儲存Bitmap畫素資料的位元組陣列。
return getByteCount();
}
return mBuffer.length;
}
複製程式碼
getByteCount()與getAllocationByteCount()的區別
- 一般情況下兩者是相等的;
- 通過複用Bitmap來解碼圖片,如果被複用的Bitmap的記憶體比待分配記憶體的Bitmap大,那麼getByteCount()表示新解碼圖片佔用記憶體的大小(並非實際記憶體大小,實際大小是複用的那個Bitmap的大小),getAllocationByteCount()表示被複用Bitmap真實佔用的記憶體大小(即mBuffer的長度)。
Bitmap的建立
通常我們可以利用Bitmap的靜態方法createBitmap()
和BitmapFactory
的decode系列靜態方法建立Bitmap物件。
Bitmap.createBitmap
主要用於圖片的操作,例如圖片的縮放,裁剪等。
BitmapFactory
注意
:decodeFile
和decodeResource
其實最終都會呼叫decodeStream
方法來解析Bitmap
。有一個特別有意思的事情是,在decodeResource
呼叫decodeStream
之前還會呼叫decodeResourceStream
這個方法,這個方法主要對Options
進行處理,在得到opts.inDensity
的屬性前提下,如果沒有對該屬性的設定值,那麼opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
這個值預設為標準dpi的基值:160。如果沒有設定opts.inTargetDensity
的值時,opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
該值為當前裝置的 densityDpi,這個值是根據你放置在 drawable 下的檔案不同而不同的。所以說 decodeResourceStream 這個方法主要對 opts.inDensity 和 opts.inTargetDensity進行賦值。
儘量不要使用setImageBitmap
或setImageResource
或BitmapFactory.decodeResource
來設定一張大圖,因為這些函式在完成decode後,最終都是通過java層的createBitmap來完成的,需要消耗更多記憶體,可以通過BitmapFactory.decodeStream
方法,建立出一個bitmap,再將其設為ImageView的 source。
Resource資源載入的方式相當的耗費記憶體,建議採用通過InputStream ins = resources.openRawResource(resourcesId);
然後使用decodeStream
代替decodeResource
獲取Bitmap。這麼做的好處是:
- BitmapFactory.decodeResource 載入的圖片可能會經過縮放,該縮放目前是放在 java 層做的,效率比較低,而且需要消耗 java 層的記憶體。因此,如果大量使用該介面載入圖片,容易導致OOM錯誤。
- BitmapFactory.decodeStream 不會對所載入的圖片進行縮放,相比之下佔用記憶體少,效率更高。
這兩個介面各有用處,如果對效能要求較高,則應該使用 decodeStream;如果對效能要求不高,且需要 Android 自帶的圖片自適應縮放功能,則可以使用 decodeResource。
Bitmap 於 drawable 的相互轉換
Bitmap 轉 drawable
Drawable newBitmapDrawable = new BitmapDrawable(bitmap);
還可以從BitmapDrawable中獲取Bitmap物件
Bitmap bitmap = new BitmapDrawable.getBitmap();
複製程式碼
drawable 轉 Bitmap
-
BitmapFactory 中的 decodeResource 方法
Resources res = getResources(); Bitmap bmp = BitmapFactory.decodeResource(res, R.drawable.ic_drawable); 複製程式碼
-
將 Drable 物件先轉化成 BitmapDrawable ,然後呼叫 getBitmap 方法 獲取
Resource res = gerResource(); Drawable drawable = res.getDrawable(R.drawable.ic_drawable);//獲取drawable BitmapDrawable bd = (BitmapDrawable) drawable; Bitmap bm = bd.getBitmap(); 複製程式碼
-
根據已有的Drawable建立一個新的Bitmap
public static Bitmap drawableToBitmap(Drawable drawable) { int w = drawable.getIntrinsicWidth(); int h = drawable.getIntrinsicHeight(); System.out.println("Drawable轉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 ,那麼在解碼的時候將不會返回 Bitmap ,只會返回這個 Bitmap 的尺寸。這個屬性的目的是,如果你只想知道一個 Bitmap 的尺寸,但又不想將其載入到記憶體中時,是一個非常好用的屬性。
- outWidth和outHeight:表示這個 Bitmap 的寬和高,一般和 inJustDecodeBounds 一起使用來獲得 Bitmap的寬高,但是不載入到記憶體。
- inSampleSize:壓縮圖片時取樣率的值,如果這個值大於1,那麼就會按照比例(1 / inSampleSize)來縮小 Bitmap 的寬和高。如果這個值為 2,那麼 Bitmap 的寬為原來的1/2,高為原來的1/2,那麼這個 Bitmap 是所佔記憶體畫素值會縮小為原來的 1/4。
- inDensity:表示這個 Bitmap 的畫素密度,對應的是 DisplayMetrics 中的 densityDpi,不是 density。(如果不明白它倆之間的異同,可以看我的 Android 螢幕各種引數的介紹和學習 )
- inTargetDensity:表示要被新 Bitmap 的目標畫素密度,對應的是 DisplayMetrics 中的 densityDpi。
- inScreenDensity:表示實際裝置的畫素密度,對應的是 DisplayMetrics 中的 densityDpi。
- inPreferredConfig:這個值是設定色彩模式,預設值是 ARGB_8888,這個模式下,一個畫素點佔用 4Byte 。RGB_565 佔用 2Byte,ARGB_4444 佔用 4Byte(以廢棄)。
- inPremultiplied:這個值和透明度通道有關,預設值是 true,如果設定為 true,則返回的 Bitmap 的顏色通道上會預先附加上透明度通道。
- inScaled:設定這個Bitmap 是否可以被縮放,預設值是 true,表示可以被縮放。
- inMutable:若為true,則返回的Bitmap是可變的,可以作為Canvas的底層Bitmap使用。 若為false,則返回的Bitmap是不可變的,只能進行讀操作。 如果要修改Bitmap,那就必須返回可變的bitmap,例如:修改某個畫素的顏色值(setPixel)
- inBitmap:這個引數用來實現 Bitmap 記憶體的複用,但複用存在一些限制,具體體現在:在 Android 4.4 之前只能重用相同大小的 Bitmap 的記憶體,而 Android 4.4 及以後版本則只要後來的 Bitmap 比之前的小即可。使用 inBitmap 引數前,每建立一個 Bitmap 物件都會分配一塊記憶體供其使用,而使用了 inBitmap 引數後,多個 Bitmap 可以複用一塊記憶體,這樣可以提高效能。
Bitmap如何複用
使用inBitmap能夠大大提高記憶體的利用效率,但是它也有幾個限制條件:
-
Bitmap複用首選需要其 mIsMutable 屬性為 true , mIsMutable 的表面意思為:易變的
在Bitmap中的意思為: 控制bitmap的setPixel方法能否使用,也就是外界能否修改bitmap的畫素。mIsMutable 屬性為 true 那麼就可以修改Bitmap的畫素資料,這樣也就可以實現Bitmap物件的複用了。
-
在SDK 11 -> 18之間,重用的bitmap大小必須是一致的,例如給inBitmap賦值的圖片大小為100-100,那麼新申請的bitmap必須也為100-100才能夠被重用。
-
被複用的Bitmap必須是Mutable,即inMutable的值為true。違反此限制,不會丟擲異常,且會返回新申請記憶體的Bitmap。
-
從SDK 19開始,新申請的bitmap大小必須小於或者等於已經賦值過的bitmap大小。違反此限制,將會導致複用失敗,丟擲異常IllegalArgumentException(Problem decoding into existing bitmap)
-
新申請的bitmap與舊的bitmap必須有相同的解碼格式,例如大家都是8888的,如果前面的bitmap是8888,那麼就不能支援4444與565格式的bitmap了,不過可以通過建立一個包含多種典型可重用bitmap的物件池,這樣後續的bitmap建立都能夠找到合適的“模板”去進行重用。
Bitmap如何壓縮
質量壓縮
質量壓縮不會改變圖片的畫素點,即我們使用完質量壓縮後,在轉換Bitmap
時佔用記憶體依舊不會減小。但是可以減少我們儲存在本地檔案的大小,即放到 disk上的大小。
/**
* 質量壓縮方法,並不能減小載入到記憶體時所佔用記憶體的空間,應該是減小的所佔用磁碟的空間
* @param image
* @param compressFormat
* @return
*/
public static Bitmap compressbyQuality(Bitmap image, Bitmap.CompressFormat compressFormat) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//質量壓縮方法,這裡100表示不壓縮,把壓縮後的資料存放到baos中
image.compress(compressFormat, 100, baos);
int quality = 100;
//迴圈判斷如果壓縮後圖片是否大於100kb,大於繼續壓縮
while ( baos.toByteArray().length / 1024 > 100) {
baos.reset();//重置baos即清空baos
if(quality > 10){
quality -= 20;//每次都減少20
}else {
break;
}
//這裡壓縮options%,把壓縮後的資料存放到baos中
image.compress(Bitmap.CompressFormat.JPEG,quality,baos);
}
//把壓縮後的資料baos存放到ByteArrayInputStream中
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
//把ByteArrayInputStream資料生成圖片
Bitmap bmp = BitmapFactory.decodeStream(isBm, null, options);
return bmp;
}
複製程式碼
取樣壓縮
這個方法主要用在圖片資源本身較大,或者適當地取樣並不會影響視覺效果的條件下,這時候我們輸出的目標可能相對的較小,對圖片的大小和解析度都減小。
壓縮格式 CompressFormat
- Bitmap.CompressFormat.JPEG
- 一種有失真壓縮(JPEG2000既可以有損也可以無損),".jpg"或者".jpeg";
- 優點:採用了直接色,有豐富的色彩,適合儲存照片和生動影像效果;缺點:有損,不適合用來儲存logo、線框類圖
- Bitmap.CompressFormat.PNG
- 一種無失真壓縮,".png";
- PNG 格式是無損的,它無法再進行質量壓縮,quality 這個引數就沒有作用了,會被忽略,所以最後圖片儲存成的檔案大小不會有變化;
- 優點:支援透明、無損,主要用於小圖示,透明背景等;
- 缺點:若色彩複雜,則圖片生成後檔案很大;
- Bitmap.CompressFormat.WEBP
- 以WebP演算法進行壓縮;
- Google開發的新的圖片格式,同時支援無損和有失真壓縮,使用直接色。
- 無失真壓縮,相同質量的webp比PNG小大約26%;
- 有失真壓縮,相同質量的webp比JPEG小25%-34% 支援動圖,基本取代gif
- 缺點:解壓速度慢
**
* 取樣率壓縮,這個和矩陣來實現縮放有點類似,但是有一個原則是“大圖小用用取樣,小圖大用用矩陣”。
* 也可以先用取樣來壓縮圖片,這樣記憶體小了,可是圖的尺寸也小。如果要是用 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();
//質量壓縮方法,這裡100表示不壓縮,把壓縮後的資料存放到baos中
image.compress(compressFormat,100,baos);
//把壓縮後的資料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資料生成圖片
}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){
// 計算出實際寬高和目標寬高的比率
final int heightRatio = Math.round((float) originalHeight / (float) reqHeight);
final int widthRatio = Math.round((float) originalWidth / (float) reqWidth);
// 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高
// 一定都會大於等於目標的寬和高。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
複製程式碼
使用矩陣
前面我們採用了取樣壓縮,Bitmap 所佔用的記憶體是小了,可是圖的尺寸也小了。當我們需要尺寸較大時該怎麼辦?我們要用用 Canvas 繪製怎麼辦?當然可以用矩陣(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;
}
複製程式碼