Android Bitmap 優化- 圖片壓縮
一直以來Bitmap都是開發中很棘手的問題,這個問題就是傳說中的OOM(java.lang.OutofMemoryError - 記憶體溢位),那麼Bitmap為何如此喪失,令無數Android開發者所懊惱?
一、Bitmap引發OOM的原因
-
由於每個機型在編譯ROM時都設定了一個應用堆記憶體VM值上限dalvik.vm.heapgrowthlimit,用來限定每個應用可用的最大記憶體,超出這個最大值將會報OOM。這個閥值,一般根據手機螢幕dpi大小遞增,dpi越小的手機,每個應用可用最大記憶體就越低。例如我的Z3c,xhdpi的VM閥值是192M,但是到了nexus s hdpi上只有可憐的48M。這樣,當一個activity中載入多張大圖後,就很容易OOM了。有關應用記憶體閥值的參考可以看這裡3.7節:
-
圖片解析度越高,消耗的記憶體越高,當載入高解析度圖片的時候,將會非常佔用記憶體,一旦處理不當就會OOM。例如,一張500W畫素的照片的解析度是:2592x1936。如果Bitmap使用 ARGB_8888 32位來平鋪顯示的話,佔用的記憶體是2592x1936x4個位元組,佔用將近19M記憶體,my god,載入不到10張這種高質量照片,應用將直接掛掉,報OOM
- 在使用ListView, GridView等這些大量載入view的元件時,如果沒有合理的處理快取,大量載入Bitmap的時候,也將容易引發OOM
二、介紹Bitmap
工欲善其事必先利其器,想要高效載入Bitmap,瞭解Bitmap是必不可少的。Bitmap有幾個重要的成員變數和方法,下面開始介紹:
2.1 Bitmap.Config
一張圖片Bitmap所佔用的記憶體 = 圖片長度 x 圖片寬度 x 一個畫素點佔用的位元組數
而Bitmap.Config,正是指定單位畫素佔用的位元組數的重要引數。
其中,A代表透明度;R代表紅色;G代表綠色;B代表藍色。
- ALPHA_8
表示8位Alpha點陣圖,即A=8,一個畫素點佔用1個位元組,它沒有顏色,只有透明度 - ARGB_4444
表示16位ARGB點陣圖,即A=4,R=4,G=4,B=4,一個畫素點佔4+4+4+4=16位,2個位元組 - ARGB_8888
表示32位ARGB點陣圖,即A=8,R=8,G=8,B=8,一個畫素點佔8+8+8+8=32位,4個位元組 - RGB_565
表示16位RGB點陣圖,即R=5,G=6,B=5,它沒有透明度,一個畫素點佔5+6+5=16位,2個位元組
Bitmap.Config主要作用是:以何種方式畫素儲存。不同的配置將會影響影象的畫質(色彩深度),位數越高畫質越高,顯然在這裡ARGB_8888是最佔記憶體的。當然,畫質越高也就越佔記憶體了。
Tips:由於ARGB_4444的畫質慘不忍睹,一般假如對圖片沒有透明度要求的話,可以改成RGB_565,相比ARGB_8888將節省一半的記憶體開銷。
2.1.1 配置不同Bitmap.Config在相同解析度下的佔用記憶體情況
一張圖片Bitmap所佔用的記憶體 = 圖片長度 x 圖片寬度 x 一個畫素點佔用的位元組數
Bitmap.Config | 解析度100x100的圖片佔用記憶體的大小 |
---|---|
ALPHA_8 | 100x100x1 = 10000 byte ~= 9.77 KB |
ARGB_4444 | 100x100x2 = 20000 byte ~= 19.53 kb |
ARGB_8888 | 100x100x4 = 40000 byte ~= 39.06 KB |
RGB_565 | 100x100x2 = 20000 byte ~= 19.53 KB |
2.2 Bitmap.CompressFormat
從字面上理解,它的含義是:Bitmap壓縮格式
public enum CompressFormat {
JPEG (0),
PNG (1),
WEBP (2);
CompressFormat(int nativeInt) {
this.nativeInt = nativeInt;
}
final int nativeInt;
}
嗯,其實這個引數很簡單,就是指定Bitmap是以JPEG、PNG還是WEBP格式來壓縮
2.3 Bitmap.compress()方法
重磅方法來了,通過這個方法,可以實現圖片的壓縮。使用該方法需要傳三個引數進去:CompressFormat、int型別的quality、OutputStream
- CompressFormat
指定Bitmap的壓縮格式,可選擇JPEG、PNG、WEBP -
int型別的quality
指定Bitmap的壓縮品質,範圍是0 ~ 100;該值越高,畫質越高。0表示畫質最差,100畫質最高。 -
OutputStream
指定Bitmap的位元組輸出流。一般使用:ByteArrayOutputStream stream = new ByteArrayOutputStream();
// Bitmap.compress()方法 public boolean compress(CompressFormat format, int quality, OutputStream stream) { if (stream == null) { throw new NullPointerException(); } if (quality < 0 || quality > 100) { throw new IllegalArgumentException("quality must be 0..100"); } Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress"); boolean result = nativeCompress(mFinalizer.mNativeBitmap, format.nativeInt, quality, stream, new byte[WORKING_COMPRESS_STORAGE]); Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); return result; }
2.3.1 案例:將一個Bitmap壓縮成jpeg, quality為10,程式碼如下:
protected 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);
Bitmap bmp = BitmapFactory.
decodeResource(this.getResources(), R.mipmap.test_pic);
iv_1.setImageBitmap(bmp);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.JPEG, 10, bos);
byte[] bytes = bos.toByteArray();
bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
iv_2.setImageBitmap(bmp);
}
三、介紹BitmapFactory
從上面那個案例的程式碼可以發現,獲取Bitmap不是通過構造new出來的,而是通過BitmapFactory”製造”出來的。BitmapFactory是獲取Bitmap和壓縮Bitmap的重要類,下面開始介紹BitmapFactory幾個重要的成員變數和方法:
3.1 通過BitmapFactory解碼(獲取)Bitmap的幾種方式
-
decodeFile() //從SD卡檔案讀取
Bitmap bm = BitmapFactory.decodeFile(Environment. getExternalStorageDirectory().getAbsolutePath()+"/photo.jpg");
-
decodeResource() //從資原始檔res讀取
Bitmap bm = BitmapFactory. decodeResource(this.getResources(), R.mipmap.test_pic);
-
decodeStream() //從輸入流讀取
Bitmap bm = BitmapFactory.decodeStream(inputStream);
-
decodeByteArray() //從位元組陣列讀取
Bitmap bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
3.2 BitmapFactory.Options
BitmapFactory在使用方法decodeFile()、decodeResource()解碼圖片時,可以指定它的BitmapFactory.Options。這個引數作用非常大,它可以設定Bitmap的取樣率,通過改變圖片的寬度、高度、縮放比例等,以達到降低圖片的畫素的目的,這樣可以做到圖片壓縮,減少Bitmap的記憶體。
下面列出BitmapFactory.Options的部分成員變數:
public Bitmap inBitmap;
public boolean inJustDecodeBounds;
public int inSampleSize;
public int inDensity;
public int inTargetDensity;
public int inScreenDensity;
public boolean inScaled;
public int outWidth;
public int outHeight;
public String outMimeType;
看到這麼多成員變數是不是傻了?no,no,no,其實很簡單。一句話總結:in開頭的代表的就是設定某某引數;out開頭的代表的就是獲取某某引數。比如,inSampleSize就是設定Bitmap的縮放比例、outWidth就是獲取Bitmap的高度。
3.2.1 inJustDecodeBounds 設定只去讀圖片的附加資訊(寬高),不去解析真實的Bitmap
從字面上理解,它的含義是:”設定僅解碼Bitmap的邊界”。那它真正的作用是啥呢?
當inJustDecodeBounds設定為true的時候,BitmapFactory通過decodeResource或者decodeFile解碼圖片時,將會返回空(null)的Bitmap物件,這樣可以避免Bitmap的記憶體分配,但是它可以返回Bitmap的寬度、高度以及MimeType。
// 當inJustDecodeBounds設定為true時,獲取Bitmap的寬度、高度以及MimeType
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true ;
BitmapFactory.decodeResource (getResources(), R.id.myimage, options);
int imageHeight = options.outHeight ;
int imageWidth = options.outWidth ;
String imageType = options.outMimeType ;
那麼這樣做有何意義呢?看完下面這段程式碼,你就知道這樣做有啥意義了。意義就在於,可以先不用產生Bitmap記憶體,從而獲得圖片的寬高資訊,儘可能的做到節約記憶體。
3.2.1.1 通過BitmapFactory.Options根據手機螢幕尺寸設定圖片的縮放比例
// 根據手機螢幕尺寸設定圖片的縮放比例【將大圖縮放】
public class TestThreadActivity3 extends Activity {
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
protected 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; //只去讀圖片的頭資訊,不去解析真實的點陣圖
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 + "");
//計算圖片縮放比例
int dx = picWidth/screenWidth;
int dy = picHeight/screenheight;
Log.e("dx,dy",dx+","+dy+"");
int scale = 1;
if(dx>=dy&&dy>=1){
Log.e("按照水平方向縮放:" ,dx+"");
scale = dx;
}
if(dy>dx&&dx>=1){
Log.e("按照豎直方向縮放:", dy + "");
scale = dy;
}
opts.inSampleSize = scale;//設定縮放比例
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("壓縮後的圖佔用記憶體:",bmp.getByteCount()+"");
iv_2.setImageBitmap(bmp);
}
}
我們讀取一張3840x2400的圖片執行結果:
原圖直接佔用36M記憶體,如果直接設定的話將瞬間爆炸報OOM。所以我們這裡先不載入Bitmap,而是隻獲取寬和高,待縮放後,再進行真實的載入Bitmap。
3.2.2 inSampleSize 設定圖片的縮放比例(寬和高)
在這裡著重講一下這個inSampleSize。從字面上理解,它的含義是:”設定取樣大小“。它的作用是:
設定inSampleSize的值(int型別)後,假如設為4,則寬和高都為原來的1/4,寬高都減少了,自然記憶體也降低了。
如何理解”設定取樣大小“呢?如果你認真看了上面的內容話,聰明的你一定知道,肯定需要配合inJustDecodeBounds,先獲取圖片的寬、高【這個過程就是取樣】,然後通過獲取的寬高,動態的設定inSampleSize的值。
【當然,你也可以不動態,可以寫死inSampleSize的值。比如設定inSampleSize = 4的話,一張解析度為2048x1536px的影象將使用inSampleSize值為4的設定來解碼,產生的Bitmap大小約為512*384px。相較於完整圖片佔用12M的記憶體,這種方式只需0.75M記憶體(假設Bitmap配置為ARGB_8888)。】
這裡再舉例演示一個動態設定inSampleSize的案例程式碼,可以通過設定圖片寬高來縮放圖片尺寸:
public class TestThreadActivity3 extends Activity {
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
protected 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; //只去讀圖片的附加資訊,不去解析真實的點陣圖
Bitmap bmp = BitmapFactory.
decodeResource(this.getResources(), R.mipmap.test_pic,opts);
Log.e("原圖佔用記憶體:", bmp.getByteCount() + "");
iv_1.setImageBitmap(bmp);
int picWidth = opts.outWidth;// 得到圖片寬度
int picHeight = opts.outHeight;// 得到圖片高度
Log.e("原圖片高度:",picHeight+"");
Log.e("原圖片寬度:",picWidth+"");
//根據100*100的寬高,設定縮放比例
opts.inSampleSize = calculateInSampleSize(opts,100,100);
opts.inJustDecodeBounds = false;//真正的去解析點陣圖
bmp = BitmapFactory.decodeResource(this.getResources(), R.mipmap.test_pic, opts);
Log.e("壓縮後的圖佔用記憶體:",bmp.getByteCount()+"");
iv_2.setImageBitmap(bmp);
}
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
Log.e("inSampleSize:",inSampleSize+"");
return inSampleSize;
}
}
3.2.3 inBitmap 重用Bitmap
inBitmap的主要作用是複用之前bitmap在記憶體中申請的記憶體,其實這是物件池的原理,以解決物件頻繁建立再回收的效率問題。
使用inBitmap前,每建立一個bitmap需要獨佔一塊記憶體
所以使用inBitmap能夠大大提高記憶體的利用效率,但是它也有幾個限制條件:
-
inBitmap只能在3.0以後使用。在2.3上,bitmap的資料是儲存在native C的記憶體區域,並不是在java dalvik的記憶體堆上。
-
在SDK 11 -> 18之間,重用的bitmap大小必須是一致的,例如給inBitmap賦值的圖片大小為100-100,那麼新申請的bitmap必須也為100-100才能夠被重用。從SDK 19開始,新申請的bitmap大小必須小於或者等於已經賦值過的bitmap大小。
-
新申請的bitmap與舊的bitmap必須有相同的解碼格式,例如大家都是8888的,如果前面的bitmap是8888,那麼就不能支援4444與565格式的bitmap了,不過可以通過建立一個包含多種典型可重用bitmap的物件池,這樣後續的bitmap建立都能夠找到合適的“模板”去進行重用。
四、總結(Bitmap壓縮的幾種方法)
1、Bitmap壓縮的兩種常用方法
- 質量壓縮法 Bitmap.compress()
參考2.3.1節的程式碼 - 取樣壓縮法 設定inSampleSize的值
參考3.2.1.1 和 3.2.2的程式碼
2、在實際使用中可以結合質量壓縮法和取樣壓縮法一起用,以達到最佳壓縮效果。
3、看完了這篇內容,其實說白了,Bitmap壓縮都是圍繞這個來做文章:Bitmap所佔用的記憶體 = 圖片長度 x 圖片寬度 x 一個畫素點佔用的位元組數。3個引數,任意減少一個的值,就達到了壓縮的效果。