深度解剖Bitmap
一.Bitmap的相關使用
關於Bitmap
,之前以為它和Drawable
差不多,就是一種圖片,直到淚水打濕了我胸前的紅領巾,我決定整理一波關於Bitmap
的姿勢!
Bitmap
相關的使用主要有兩種:
- 1.給
ImageView
設置背景 - 2.當做畫布來使用
分別對應下面兩個方法
imageView.setImageBitmap(Bitmap bm);
Canvas canvas = new Canvas(Bitmap bm)
二.Bitmap的格式
我們知道Bitmap
是位圖,是由像素點組成的,這就涉及到兩個問題,
- 第一:如何存儲每個像素點?
- 第二:怎么壓縮像素點?
2.1 存儲格式
Bitmap
有四種存儲方式,對應Bitmap.Config
中的四個常量
- ALPHA_8:只存儲透明度,不存儲色值,1個像素點占1個字節
- ARGB_4444:ARGB各用4位存儲,1個像素點16位占2個字節
- ARGB_8888:ARGB各用8位存儲,1個像素點32位占4個字節
- RGB_565:只存儲色值,不存儲透明度,默認不透明,RGB分別占5,6,5位,一個像素點占用16位2個字節。
一般情況下,我們不會使用ALPHA_8
,他只存儲透明度,沒啥用處。對於ARGB_4444
,它的畫質又太感人了,ARGB_8888
畫質高但是占內存,RGB_565
還行,就是不可以設置透明度。
注意以下三點即可:
- 1.一般情況下用
ARGB_8888
格式存儲Bitmap
- 2.
ARGB_4444
畫面慘不忍睹,被棄用 - 3.假如對圖片沒有透明度要求,可以使用
RGB_565
,比ARGB_8888
節省一半的內存開銷
2.2 壓縮格式
我們不妨來計算一下,如果一張和手機屏幕大小一樣的Bitmap圖片,采用ARGB_8888格式存儲需要多大的內存!
按照1024*768的屏幕大小來計算,每個像素需要32位也就是4個字節,
result = 1024*768*32B=25165824B=24MB
一張手機屏幕大小的Bitmap圖片竟然要24M? 那就不奇怪我的app為什么一直閃退了,只不過用for循環創建了幾十個用在滑動列表里面。
所以我們必須要對圖片進行壓縮呀,壓縮格式使用枚舉類Bitmap.CompressFormat中,有以下三種:
Bitmap.CompressFormat.JPEG:采用
JPEG
壓縮算法,是一種有損壓縮格式,會在壓縮過程中改變圖像原本質量,畫質越差,對原來的圖片質量損傷越大,但是得到的文件比較小,而且JPEG
不支持透明度,當遇到透明度像素時,會以黑色背景填充。
Bitmap.CompressFormat.PNG:采用
PNG
算法,是一種支持透明度的無損壓縮格式。
Bitmap.CompressFormat.WEBP:
WEBP
是一種同時提供了有損壓縮和無損壓縮的圖片文件格式,在14<=api<=17時,WEBP
是一種有損壓縮格式,而且不支持透明度,在api18以后WEBP
是一種無損壓縮格式,而且支持透明度,有損壓縮時,在質量相同的情況下,WEBP
格式的圖片體積比JPEG
小40%,但是編碼時間比JPEG
長8倍。在無損壓縮時,無損的WEBP
圖片比PNG
壓縮小26%,但是WEBP的壓縮時間是PNG
格式壓縮時間的5倍。
三.Bitmap創建方法
3.1 Bitmap.Options
想要創建一個Bitmap
有很多種方法,其中很多方法都要求傳入一個Bitmap.Options
,它是什么呢,有什么作用呢?
這個參數的作用非常大,他可以設置Bitmap的采樣率,通過改變圖片的寬度高度和縮放比例等,以達到減少圖片像素數的目的,一言以蔽之,通過設置這個參數我們可以很好的控制顯示和使用Bitmap
。實際開發過程中,可以靈活設置該值,以降低OOM
發生的概率。
介紹幾個重要的成員變量
-
inJustDecodeBounds:
boolean
類型,設為true
時,無需要把圖片加載入內存就可以獲取圖片的高度,寬度和圖片的MIME
類型。
高度通過options.outWidth
獲取 寬度通過options.outHeight
獲取
MIME
通過options.outMineType
獲取 -
inSampleSize:這個字段表示采樣率,打個比方說,設置為4,則是從原本圖片的四個像素中取一個像素作為結果返回。其余的都被丟棄。可見,采樣率越大,圖片越小,失真越嚴重。 如何計算采樣率呢?看一下這段代碼你就會明白
public int getSampleSize(BitmapFactory.Options options , int dstWidth,int dstHeight){
//dstWidth:表示目前ImageView的寬度
//dstHeight:表示目標ImageView的高度
//option中獲取bitmap圖片的信息
int rawWidth = options.outWidth;
int rawHeight = options.outHeight;
int sampleSize=1;
if(rawWidth>dstWidth||rawHeight>dstHeight){
float ratioHeight = (float) (rawHeight/dstHeight);
float ratioWidth = (float) (rawWidth/dstWidth);
sampleSize = (int) Math.min(rawHeight, ratioWidth);
}
return sampleSize;
}
為了更清楚的介紹下面的知識,先補充幾點:
- 不同名稱的資源文件夾是為了適配不同的屏幕分辨率的,當屏幕分辨率與文件所在資源文件夾對應的分辨率相等時,直接使用圖片,不需要進行放縮。
- 當屏幕分辨率與圖片所在文件夾所對應的分辨率不同時,會進行縮放,縮放比例是屏幕分辨率/文件夾所對應的分辨率。
- 從本地文件中加載圖片時,不會對圖片進行縮放噢。
inScald:這個參數表示,在可以縮放時,是否對當前文件進行放縮,如果設置為false就不放縮。設置為true,則會根據文件夾分辨率和屏幕分辨率進行動態縮放。
inPreferredConfig:這個參數是用來設置像素的存儲格式的。
關於Options就介紹這幾個關鍵的字段,下面進入重頭戲,創建Bitmap。
3.2 BitmapFactory
BitmapFactory提供了多種創建bitmap的靜態方法
//從資源文件中通過id加載bitmap
//Resources res:資源文件,可以context.getResources()獲得
//id:資源文件的id,如R.drawable.xxx
public static Bitmap decodeResources(Resources res,int id)
//第二種只是第一種的重載方法,多了個Options參數
public static Bitmap decodeResources(Resources res,int id,Options opt)
//傳入文件路徑加載,比如加載sd卡中的文件
//pathName:文件的全路徑名
public static Bitmap decodeFile(String pathName);
public static Bitmap decodeFile(String pathName,Options opt);
//從byte數組中加載
//offset:對應data數組的起始下標
//length:截取的data數組的長度
public static Bitmap decodeByteArray(byte[] data,int offset , int length);
public static Bitmap decodeByteArray(byte[] data,int offset , int length,Options opt);
//從輸入流中加載圖片
//InputStream is:輸入流
//Rect outPadding:用於返回矩形的內邊距
public static Bitmap decodeStream(InputStream is);
public static Bitmap decodeStream(InputStream is,Rect outPadding,Options opt);
//FileDescriptor :包含解碼位圖的數據文件的路徑
//通過該方式從路徑加載bitmap比decodeFile更節省內存,原因不解釋了。
public static Bitmap decodeFileDescriptor(FileDescriptor fd);
public static Bitmap decodeFileDescriptor(FileDescriptor fd,Rect outPadding,Options opt);
平時用這些函數都是糊里糊塗的,今天整理了一遍發現其實有規律可尋,也更加清楚了。
3.3 Bitmap靜態方法
//width和height是長和寬單位px,config是存儲格式
static Bitmap createBitmap(int width , int height Bitmap.Config config)
// 根據一幅圖像創建一份一模一樣的實例
static Bitmap createBitmap(Bitmap bm)
//截取一幅bitmap,起點是(x,y),width和height分別對應寬高
static Bitmap createBitmap(Bitmap bm,int x,int y,int width,int height)
//比上面的裁剪函數多了兩個參數,Matrix:給裁剪后的圖像添加矩陣 boolean filter:是否給圖像添加濾波效果
static Bitmap createBitmap(Bitmap bm,int x,int y,int width,int height,Matrix m,boolean filter);
//用於縮放bitmap,dstWidth和dstHeight分別是目標寬高
createScaledBitmap(Bitmap bm,int dstWidth,int dstHeight,boolean filter)
3.4 創建Bitmap的總結
1.加載圖像可以使用BitmapFactory
和Bitmap.create
系列方法
2.可以通過Options
實現縮放圖片,獲取圖片信息,配置縮放比例等功能
3.如果需要裁剪或者縮放圖片,只能使用create
系列函數
4.注意加載和創建bitmap
事通過try catch
捕捉OOM
異常
四.常見函數
4.1 函數及其參數
copy(Config config,boolean isMutable)
//根據原圖像創建一個副本,但可以指定副本的像素存儲格式
//參數含義。
// config:像素在內存中的存儲格式,但可以指定副本的像素存儲格式
// boolean isMutable:新建的bitmap是否可以修改其中的像素值
extractAlpha()
//主要作用是從bitmap中獲取Alpha值,生成一幅只有Alpha值得圖像,存儲格式是ALPHA_8
getByteCount()//獲取bitmap的字節數
recycle()://不用的bitmap必須要及時回收,以免造成oom
isRecycled()//判斷bitmap是否被回收,被收回不可使用會造成crash
4.2 綜合案例演示
String items[] = {"copy","extractAlpha 1","extractAlpha 2","bitmap大小","recycle","isRecycled()"};
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item,items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
switch (position){
case 0:
//copy
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
Bitmap copy = bm.copy(Bitmap.Config.ARGB_8888, true);
imageView.setImageBitmap(copy);
bm.recycle();
break;
case 1:
//extractAlpha 不帶參數
Bitmap bp = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
Bitmap alpha = bp.extractAlpha();
imageView.setImageBitmap(alpha);
bp.recycle();
break;
case 2:
//extractAlpha 帶參數
Bitmap bp1 = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
Paint paint = new Paint();
BlurMaskFilter blurMaskFilter = new BlurMaskFilter(6, BlurMaskFilter.Blur.NORMAL);
paint.setMaskFilter(blurMaskFilter);
int[] offsetXY = new int[2];
Bitmap alpha1 = bp1.extractAlpha(paint, offsetXY);
imageView.setImageBitmap(alpha1);
break;
case 3:
//獲取bitmap大小
Bitmap b = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
Toast.makeText(getApplicationContext(), "圖片大小為:"+b.getByteCount()+"字節", Toast.LENGTH_SHORT).show();
break;
case 4:
//回收bitmap
Bitmap b1 = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
b1.recycle();
if(b1.isRecycled()){
Toast.makeText(getApplicationContext(), "已經被回收", Toast.LENGTH_SHORT).show();
}
//isRecycled()判斷是否被回收
break;
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
五.常見問題
5.1 Bitmap與Canvas,View,Drawable的關系
1.我們在創建一個Canvas
時,可以傳入一個Bitmap
,Paint
在Canvas
上的繪制實際上就是繪制在Bitmap
對象上的。
2.我們自定義空間所顯示的View
也是通過Canvas
中的Bitmap
來顯示的。
3.Drawable
在內存占用和繪制速度這兩個非常關鍵的點上勝過Bitmap
.
5.2 使用Bitmap如何造成內存溢出的?
個人認為,Bitmap
容易造成內存溢出是由於位圖較大,一張屏幕大小的ARGB_8888
存儲格式的圖片竟然有24M,如果有幾個這種量級的圖片在內存中,並且沒有及時回收,那會非常容易造成OOM
5.3怎么解決或者避免Bitmap內存溢出?
1.我們可以對位圖進行壓縮,壓縮手段有PNG,JPEG,WEBP
2.對不使用的Bitmap
一定要及時回收。
3.在創建Bitmap
時使用try catch
步驟OOM
異常,使程序更健壯,即使發生了OOM
也不會閃退,造成不好的使用體驗.
5.4Bitmap與Drawable的轉換
5.4.1 Drawable轉換成Bitmap
public static Bitmap drawableToBitmap(Drawable drawable) {
// 取 drawable 的長寬
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
// 取 drawable 的顏色格式
Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888
: Bitmap.Config.RGB_565;
// 建立對應 bitmap
Bitmap bitmap = Bitmap.createBitmap(w, h, config);
// 建立對應 bitmap 的畫布
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, w, h);
// 把 drawable 內容畫到畫布中
drawable.draw(canvas);
return bitmap;
}
5.4.2 Bitmap轉換成Drawable
Bitmap bm=Bitmap.createBitmap(xxx);
BitmapDrawable bd= new BitmapDrawable(getResource(), bm);
六.小結
以前使用bitmap全靠cv,現在掌握了這么多知識,bitmap隨便用都不會出現問題,媽媽再也不用擔心我內存溢出,太棒了!
相關閱讀: