1.概述
在開發(fā)中,對于圖片的操作掠剑,稍有不慎屈芜,可能就會消耗大量的內(nèi)存,導致程序崩潰,所以了解一種通用的技術(shù)去處理和加載圖片井佑,同時保證UI流暢避免OOM現(xiàn)象属铁,是非常有必要的。那么為什么在Android中對于圖片的處理會如此棘手呢毅糟?主要有以下一些原因:
- .通常情況下红选,移動設(shè)備的內(nèi)存資源是有限的,Android系統(tǒng)會根據(jù)手機的屏幕大小和密度姆另,為每個程序設(shè)置一個最大內(nèi)存限制喇肋,應用程序消耗的內(nèi)存不能超過這個最大內(nèi)存限制,否則就會出現(xiàn)OOM現(xiàn)象迹辐。當然蝶防,這個內(nèi)存限制是跟手機配置相關(guān)聯(lián)的。
- 圖片的操作會消耗大量的內(nèi)存明吩,特別是細節(jié)豐富的圖片间学,例如照片。以Galaxy Nexus相機為例子印荔,它拍攝一張2592x1936像素的照片低葫,如果使用的位圖配置是ARGB_8888(默認從Android 2.3開始),那么這張照片加載到內(nèi)存仍律,大約會消耗19MB的內(nèi)存(2592 x 1936 x 4字節(jié))嘿悬,僅僅是圖片消耗內(nèi)存的數(shù)值可能已經(jīng)超過了某些設(shè)備的內(nèi)存限制
- Android的UI經(jīng)常會一次加載多張圖片,例如水泉,ListView善涨、GridView、ViewPager等等
圖片有各種形狀和大小草则。通常情況下钢拧,它們普遍比設(shè)備所需要的圖片要大一些,例如手機相冊顯示手機拍攝的照片炕横,而手機的相機分辨率大多時候是要高于手機屏幕的分辨率源内。鑒于手機的內(nèi)存有限,我們只需要在內(nèi)存中加載一個低分辨率的照片版本就可以了看锉,而這個低分辨率的照片應該與顯示它的控件相匹配姿锭,這就需要對圖片進行壓縮處理了。
Android中有兩種壓縮圖片的方法伯铣。
- 第一種是針對圖片的長寬進行壓縮呻此,在將圖片加載到內(nèi)存過程中將圖片的長寬進行壓縮,獲取長寬壓縮版的的圖片
- 第二種是針對圖片的像素進行壓縮腔寡,圖片加載到內(nèi)存后焚鲜,針對圖片質(zhì)量進行壓縮,會導致圖片質(zhì)量下降。
2. 圖片長寬壓縮
2.1 獲取加載圖片的屬性
Android 中的BitmapFactory
類提供了一些解碼方法忿磅,decodeByteArray()
糯彬、decodeFile()
、decodeResource()
等等葱她,根據(jù)不通的圖片源選擇不同的解碼方法加載圖片創(chuàng)建出Bitmap
撩扒。這些方法中都會傳入一個BitmapFactory.Options
實例化對象,通過這個對象吨些,可以更改一些加載圖片的設(shè)置搓谆。由于這些解碼方法用于解碼加載圖片,會占用內(nèi)存構(gòu)建 Bitmap豪墅,因此很容易導致 OOM 的異常泉手。
如果將 options.inJustDecodeBounds
設(shè)置為 true,在解碼過程中就不會申請內(nèi)存去創(chuàng)建 Bitmap偶器,返回的是一個空的 Bitmap斩萌,但是可以獲取圖片的一些屬性,例如圖片寬高屏轰,圖片類型等等颊郎。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 設(shè)置為true,不將圖片解碼到內(nèi)存中
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);//將解碼出來的bitmap的屬性給了option
int imageHeight = options.outHeight; // 圖片高度
int imageWidth = options.outWidth; // 圖片寬度
String imageType = options.outMimeType; // 圖片類型
一般來說霎苗,為了避免OOM的異常袭艺,在加載圖片到內(nèi)存之前,會先檢查圖片的尺寸叨粘,除非你能確保圖片源不會導致OOM。
2.2 縮小圖片的長寬來壓縮圖片
我們知道圖片的大小之后瘤睹,就可以決定是否將完整的圖片加載到內(nèi)存或者加載壓縮版的圖片到內(nèi)存升敲。可以基于以下幾點做出決定:
- 估計完整圖片加載到內(nèi)存中所使用內(nèi)存
- 可分配給加載圖片的內(nèi)存
- 用于顯示圖片的控件的大小
- 當前設(shè)備的屏幕大小和密度
例如轰传,如果顯示圖片的控件大小為128x96像素驴党,就沒有必要將一個1024x768像素的圖片加載到內(nèi)存中。
設(shè)置 options.inSampleSize
的數(shù)值获茬,來控制壓縮圖片程度港庄。例如,將 options.inSampleSize
設(shè)置為4恕曲,將一個2048x1536像素的圖片解碼加載到內(nèi)存后產(chǎn)生的 Bitmap 大約為512x384像素鹏氧,如果使用的位圖配置是ARGB_8888
,那么僅僅需要0.75M 就加載了縮小版的圖片到內(nèi)存佩谣,而加載完整的圖片需要 12M把还。
也就是說,如果我們設(shè)置 inSampleSize == 2
,解碼出來的位圖的寬高是原圖的1/2吊履,圖片所占用內(nèi)存縮小了1/4(1/2 x 1/2)安皱。如果 inSampleSize
設(shè)置的值小于等1,都會當做inSampleSize == 1
來解碼加載圖片艇炎。
于是我們可以在加載圖片的時候酌伊,根據(jù)控件的大小(顯示到屏幕上的大凶鹤佟)來計算出加壓縮版圖片的 inSampleSize
值居砖。
/**
* 計算inSampleSize值
*
* @param options
* 用于獲取原圖的長寬
* @param reqWidth
* 要求壓縮后的圖片寬度
* @param reqHeight
* 要求壓縮后的圖片長度
* @return
* 返回計算后的inSampleSize值
*/
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;
// 計算inSampleSize值
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
有人可能會疑問為什么每次 inSampleSize
都是乘以2,指數(shù)增長辜贵。這是因為在加載圖片過程中悯蝉,解析器使用的inSampleSize
都是2的指數(shù)倍,如果 inSampleSize
是其他值托慨,則找一個離這個值最近的2的指數(shù)值鼻由。
上面已經(jīng)獲取了inSampleSize
,然后就可以根據(jù)這個值來加載壓縮版的圖片了
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 先將inJustDecodeBounds設(shè)置為true來獲取圖片的長寬屬性
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 計算inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 加載壓縮版圖片
options.inJustDecodeBounds = false;
// 根據(jù)具體情況選擇具體的解碼方法
return BitmapFactory.decodeResource(res, resId, options);
}
獲取到了壓縮版的 Bitmap 之后就可以直接設(shè)置到屏幕的控件上了厚棵。
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
3. 圖片質(zhì)量壓縮
3.1 方法介紹
上面一種方法是通過縮放圖片的大小來達到壓縮效果蕉世,基本不會對圖片的顯示效果有影響。但是現(xiàn)在介紹的這一種方法婆硬,可能會導致圖片質(zhì)量下降狠轻。
使用的是下面這個方法來進行壓縮。
Bitmap.compress(CompressFormat format, int quality, OutputStream stream)
這個方法有三個參數(shù)彬犯,是布爾類型的返回值
- CompressFormat 指定的 Bitmap 被壓縮成的圖片格式向楼,只支持
JPEG,PNG谐区,WEBP
三種 - quality 圖片壓縮質(zhì)量的控制湖蜕,范圍為 0~100,0表示壓縮后體積最小宋列,但是質(zhì)量也是最差昭抒,100表示壓縮后體積最大,但是質(zhì)量也是最好的(個人認為相當于未壓縮)炼杖,有些格式灭返,例如 png,它是無損的坤邪,所以會忽略這個值熙含。
- OutputStream 壓縮后的數(shù)據(jù)會寫入這個字節(jié)流中
返回值表示返回的字節(jié)流是否可以使用BitmapFactory.decodeStream()
解碼成 Bitmap,至于返回值是怎么得到的艇纺,因為是Native的代碼婆芦,沒法找到邏輯怕磨。
3.2 色位深度介紹
接下來說說為什么用這個方法可能會導致圖片質(zhì)量下降。在 Bitmap 中有一個 Config 的屬性消约,這個屬性是用來描述每個像素被儲存的大小肠鲫。目前 Config
有四個值:ALPHA_8、RGB_565或粮、ARGB_4444导饲、ARGB_8888
。這個說明一下(我個人的理解氯材,真心不好解釋)渣锦,每一個像素會可能由四個屬性組成,R(Red紅色通道)氢哮、G(Green綠色通道)袋毙、B(Blue藍色通道)、A(Alpha透明度通道)冗尤。
Config | 每個像素占用的字節(jié) | 說明 |
---|---|---|
ALPHA_8 | 1 bytes | 每個像素僅僅儲存透明度通道 |
RGB_565 | 2 bytes | 每個像素的RGB通道會保存听盖,透明度不會保存,紅色通道5位裂七,有25=32種表現(xiàn)形式皆看;綠色通道6位,有26=64種表現(xiàn)形式背零;藍色通道5位腰吟,有2^5=32種表現(xiàn)形式 |
ARGB_4444 | 2 bytes | 每個像素的ARGB通道都會保存,透明度/紅色/綠色/藍色通道4位徙瓶,有2^4=16種表現(xiàn)形式 |
ARGB_8888 | 4 bytes | 每個像素的ARGB通道都會保存毛雇,透明度/紅色/綠色/藍色通道8位,有2^8=256種表現(xiàn)形式 |
有什么區(qū)別呢侦镇?最簡單的禾乘,當一個顏色表現(xiàn)形式越多,那么畫面整體的色彩就會更豐富虽缕,圖片質(zhì)量就會越高,當然蒲稳,圖片占用的儲存空間也越大氮趋。
3.3 圖片質(zhì)量下降的原因
前面提到過調(diào)用Bitmap.compress()
方法時候,會傳入一個壓縮后的圖片格式江耀,但是由于并不是所有的圖片格式都支持上面說的 Config
的所有通道剩胁,比如說,JPEG格式的圖片祥国,是不支持Alpha(透明度)屬性的昵观,這樣將壓縮后返回的字節(jié)流通過
BitmapFactory.decodeStream()
轉(zhuǎn)換成Bitmap的過程中晾腔,會將透明度屬性給丟棄,導致圖片質(zhì)量下降啊犬。
3.4 壓縮過程介紹
壓縮過程如下灼擂,通過依次減少圖片質(zhì)量,將圖片大小控制在限制值范圍內(nèi)觉至。
/**
* 壓縮圖片
*
* @param bitmap
* 被壓縮的圖片
* @param sizeLimit
* 大小限制
* @return
* 壓縮后的圖片
*/
private Bitmap compressBitmap(Bitmap bitmap, long sizeLimit) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int quality = 100;
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
// 循環(huán)判斷壓縮后圖片是否超過限制大小
while(baos.toByteArray().length / 1024 > sizeLimit) {
// 清空baos
baos.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
quality -= 10;
}
Bitmap newBitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(baos.toByteArray()), null, null);
return newBitmap;
}
4. 進一步優(yōu)化
上面提到的很多壓縮方法剔应,如果是在UI線程執(zhí)行的話,很有可能阻塞到主線程语御,這是在開發(fā)過程中非常不愿意見到的事情峻贮,所以我們需要在后臺線程去執(zhí)行這些壓縮圖片比較耗時的操作,然后獲取到壓縮后的圖片应闯,設(shè)置到屏幕中纤控。使用AsyncTask可以幫助我們很好的實現(xiàn)。
/**
* 壓縮圖片
*
* @param bitmap
* 被壓縮的圖片
* @param sizeLimit
* 大小限制
* @return
* 壓縮后的圖片
*/
private Bitmap compressBitmap(Bitmap bitmap, long sizeLimit) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int quality = 100;
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
// 循環(huán)判斷壓縮后圖片是否超過限制大小
while(baos.toByteArray().length / 1024 > sizeLimit) {
// 清空baos
baos.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
quality -= 10;
}
Bitmap newBitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(baos.toByteArray()), null, null);
return newBitmap;
}
5. 總結(jié)
圖片的處理碉纺,時刻都需要注意船万,因為機型配置的不同,以及現(xiàn)場設(shè)備內(nèi)存使用的情況惜辑,都有可能導致OOM的現(xiàn)象唬涧,上述提到了壓縮方法,基本適用與大部分圖片壓縮情況盛撑。當然如果對圖片畫質(zhì)顯示有要求碎节,可能就需要特殊的處理了,這個就不在大部分場景的考慮內(nèi)抵卫。