Bitmap圖片壓縮柿赊,大圖加載防止OOM

前言

Android官網(wǎng)中處理位圖高效加載大型位圖
這兩篇文章中已經(jīng)做了很明確指出了如何高效的加載大圖。這篇文章只是對其中的內(nèi)容進行總結(jié)和擴展(比如圖片內(nèi)存計算绍傲、圖片壓縮等)扔傅。

為了防止加載 Bitmap 的時候造成 OOM 崩潰,我們首選要知道:

  • 一張圖片加載到 Bitmap 的時候的占用的是怎么內(nèi)存計算烫饼;
  • 占用內(nèi)存過高的時候怎么進行圖片壓縮減小內(nèi)存占用猎塞;

RGB介紹

RGB顏色模型: 最常見的顏色模型,設(shè)備相關(guān)杠纵。R荠耽、G、B分別代表紅淡诗、綠和藍色三種顏色通道骇塘,取值均為[0,255]伊履。

RGB 8位色: 表示使用8位(bit)表示顏色韩容,一共能表示2^8 = 128種顏色。
依次類推RGB 16位色唐瀑,RGB 24位色群凶,RGB 32位色,使用的位數(shù)越多哄辣,能表示的顏色越多请梢,24位能表示的顏色數(shù)量已經(jīng)很多了,稱之為“真彩色”力穗。

32位和24位能表示的顏色一樣多毅弧,多一個了透明度。

Android Bitmap使用的三種顏色格式:

  • ALPHA_8–每個像素占1個字節(jié)当窗,存儲透明度信息够坐,沒有顏色信息。
  • RGB_565--每個像素占2個字節(jié)存儲顏色信息,R 5位元咙,G 6位梯影,B 5位,能表示2^16種顏色庶香。
  • ARGB_8888--每個像素占4個字節(jié)存儲顏色信息甲棍,A R G B各一個字節(jié),能表示2^24種顏色赶掖,還有一個字節(jié)存儲透明度信息感猛。

圖片占用內(nèi)存的計算

Bitmap 所占內(nèi)存大小計算方式:圖片長度 x 圖片寬度 x 一個像素點占用的字節(jié)數(shù)。

讀取位圖尺寸和類型

BitmapFactory 類提供了幾種用于從各種來源創(chuàng)建 Bitmap 的解碼方法(decodeByteArray()倘零、decodeFile()唱遭、decodeResource()等)。根據(jù)您的圖片數(shù)據(jù)源選擇最合適的解碼方法呈驶。這些方法嘗試為構(gòu)造的位圖分配內(nèi)存拷泽,因此很容易導(dǎo)致 OutOfMemory 異常。每種類型的解碼方法都有額外的簽名袖瞻,允許您通過 BitmapFactory.Options 類指定解碼選項司致。在解碼時將inJustDecodeBounds 屬性設(shè)置為 true 可避免內(nèi)存分配,為位圖對象返回 null聋迎,但設(shè)置 outWidth脂矫、outHeightoutMimeType。此方法可讓您在構(gòu)造位圖并為其分配內(nèi)存之前讀取圖片數(shù)據(jù)的尺寸和類型霉晕。

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;

為避免出現(xiàn) java.lang.OutOfMemory 異常庭再,請先檢查位圖的尺寸,然后再對其進行解碼牺堰,除非您絕對信任該來源可為您提供大小可預(yù)測的圖片數(shù)據(jù)拄轻,以輕松適應(yīng)可用的內(nèi)存。

內(nèi)存中如果加載一張 500*500png 高清圖片.應(yīng)該是占用多少的內(nèi)存?

png 圖片應(yīng)該有alpha通道伟葫,所以 Bitmap.ConfigARGB_8888 恨搓。4個8位一種占用32位。
最終答案: 500 * 500 * 4 = 1000000Bytes = 0.95MB

如果這個圖片為本地資源圖片筏养,是否還是0.95MB呢斧抱?

先看一些基礎(chǔ)知識(后面有答案) Android官網(wǎng)-提供備用位圖 這篇文章鏈接中的有講到:

要在像素密度不同的設(shè)備上提供良好的圖形質(zhì)量,您應(yīng)該以相應(yīng)的分辨率在應(yīng)用中提供每個位圖的多個版本(針對每個密度級別提供一個版本)渐溶。否則辉浦,Android 系統(tǒng)必須縮放位圖,使其在每個屏幕上占據(jù)相同的可見空間茎辐,從而導(dǎo)致縮放失真宪郊,如模糊眉睹。

image

例如,如果您有一個可繪制位圖資源废膘,它在中密度屏幕上的大小為 48x48 像素竹海,那么它在其他各種密度的屏幕上的大小應(yīng)該為:

  • 36x36 (0.75x) - 低密度 (ldpi)
  • 48x48(1.0x 基準)- 中密度 (mdpi)
  • 72x72 (1.5x) - 高密度 (hdpi)
  • 96x96 (2.0x) - 超高密度 (xhdpi)
  • 144x144 (3.0x) - 超超高密度 (xxhdpi)
  • 192x192 (4.0x) - 超超超高密度 (xxxhdpi)

然后,將生成的圖片文件放在 res/ 下的相應(yīng)子目錄中丐黄,系統(tǒng)將根據(jù)運行應(yīng)用的設(shè)備的像素密度自動選取正確的文件斋配。之后,每當您引用@drawable/xxx時灌闺,系統(tǒng)都會根據(jù)屏幕的 dpi 選擇適當?shù)奈粓D艰争。如果您沒有為某個密度提供特定于密度的資源,那么系統(tǒng)會選取下一個最佳匹配項并對其進行縮放以適合屏幕桂对。

實測:1520 x 2688 大小為 334.28KB 圖片甩卓,屏幕密度為480的手機;

  • 放在 drawable-xxdpi 下加載到 Bitmap 中占用內(nèi)存為 16343040(1520*2688*4)蕉斜,因為圖片不需要進行縮放逾柿,所以只需要計算 ARGB_8888 占用的字節(jié)數(shù)就行;
  • 放在 drawable-mdpi 下加載到 Bitmap 中占用內(nèi)存為 147087360(1520*3*2688*3*4) 宅此,因為 mdipxxdpi 圖片的寬高分別會放大4倍机错;

nodpi 目錄中的資源被視為與密度無關(guān),系統(tǒng)將不會對它們進行縮放父腕。

Bitmap壓縮

壓縮原理

Android 中進行圖片壓縮是非常常見的開發(fā)場景弱匪,主要的壓縮方法有兩種:其一是下 采樣壓縮,其二是 質(zhì)量壓縮璧亮。

  • 前者是降低圖像尺寸萧诫,改變圖片的存儲體積;
  • 后者則是在不改變圖片尺寸的情況下枝嘶,通過損失顏色精度帘饶,達到相同目的;

壓縮Bitmap磁盤占用空間的大小

//如果成功地把壓縮數(shù)據(jù)寫入輸出流,則返回true躬络。
public boolean compress(
    Bitmap.CompressFormat format, //圖像的壓縮格式尖奔;
    int quality,//圖像壓縮率搭儒,0-100穷当。 0 壓縮100%,100意味著不壓縮淹禾;
    OutputStream stream) ;//寫入壓縮數(shù)據(jù)的輸出流馁菜;
  • Bitmap.CompressFormat.PNG ,那不管第二個值如何變化铃岔,圖片大小都不會變化汪疮,不支持 png圖片 的壓縮峭火。因為 PNG 格式是無損的,它無法再進行質(zhì)量壓縮智嚷,quality這個參數(shù)就沒有作用了卖丸,會被忽略,所以最后圖片保存成的文件大小不會有變化盏道;
  • CompressFormat.WEBP 稍浆,這個格式是 google 推出的圖片格式,它會比 JPEG 更加省空間猜嘱。官方表示能節(jié)省 25%-34% 的空間衅枫;

壓縮Bitmap占用內(nèi)存的大小

圖片尺寸的修改其實就是通過修改像素數(shù),放大的過程稱之為上采樣朗伶,縮小的過程稱之為下采樣弦撩。

要知道怎么壓縮才能使 Bitmap 占用的內(nèi)存變小,首先需要知道 Bitmap 的內(nèi)存占用怎么計算论皆。 計算圖片的內(nèi)存占用 這篇文章有詳細講解益楼。

使用inSampleSize進行壓縮

既然圖片尺寸已知,便可用于確定應(yīng)將完整圖片加載到內(nèi)存中点晴,還是應(yīng)改為加載下采樣版本偏形。以下是需要考慮的一些因素:

  • 在內(nèi)存中加載完整圖片的估計內(nèi)存使用量。
  • 根據(jù)應(yīng)用的任何其他內(nèi)存要求觉鼻,您愿意分配用于加載此圖片的內(nèi)存量俊扭。
  • 圖片要載入到的目標 ImageView 或界面組件的尺寸。
  • 當前設(shè)備的屏幕大小和密度坠陈。

例如萨惑,如果 1024x768 像素的圖片最終會在 ImageView 中顯示為 128x96 像素縮略圖,則不值得將其加載到內(nèi)存中仇矾。

要讓解碼器對圖片進行下采樣庸蔼,以將較小版本加載到內(nèi)存中,請在 BitmapFactory.Options 對象中將 inSampleSize 設(shè)置為 true贮匕。

例如姐仅,分辨率為 2048x1536 且以 4 作為 inSampleSize 進行解碼的圖片會生成大約 512x384 的位圖。將此圖片加載到內(nèi)存中需使用 0.75MB刻盐,而不是完整圖片所需的 12MB(假設(shè)位圖配置為 ARGB_8888)掏膏。

下面的方法用于計算樣本大小值,即基于目標寬度和高度的 2 的冪:

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    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;
        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) >= reqHeight
                && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}

注意:根據(jù) inSampleSize 文檔敦锌,計算 2 的冪的原因是解碼器使用的最終值將向下舍入為最接近的 2 的冪馒疹。

要使用此方法,請先將 inJustDecodeBounds 設(shè)為 true 進行解碼乙墙,傳遞選項颖变,然后使用新的 inSampleSize 值并將 設(shè)為false 再次進行解碼:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {
    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

Android 使用的 inSampleSize 計算采樣率使用的采樣算法是鄰近采樣(Nearest Neighbour Resampling)生均, xx 為 2 的倍數(shù))個像素最后對應(yīng)一個像素。比如采樣率設(shè)置為 1/2 腥刹,所以是兩個像素生成一個像素马胧。鄰近采樣的方式比較粗暴,直接選擇其中的一個像素作為生成像素衔峰,另一個像素直接拋棄漓雅。

使用createScaledBitmap或Matrix

Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.png");
Bitmap compress = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true);
//或者直接使用 matrix 進行縮放,查看Bitmap.createScaledBitmap源碼其實就是使用 matrix 縮放
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.png");
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
bm = Bitmap.createBitmap(bitmap, 0, 0, bit.getWidth(), bit.getHeight(), matrix, true);

同樣是圖片寬高各為原來的1/2朽色,這種方式采用雙線性采樣(Bilinear Resampling)邻吞,這個算法不像鄰近采樣算法直接粗暴的選擇一個像素,而是參考了源像素相應(yīng)位置周圍 2x2 個點的值葫男,根據(jù)相對位置取對應(yīng)的權(quán)重抱冷,經(jīng)過計算之后得到目標圖像。

不同的采樣算法會產(chǎn)生不同效果梢褐,除了 Android 中這兩種常用的采樣算法之外旺遮,還有比較常見如:雙立方/雙三次采樣(Bicubic Resampling)Lanczos Resampling 等。如果對 Android 使用的這兩種采樣算法效果不滿意盈咳,必要時可以引入其他的算法耿眉。

BitmapFactory.Options三件套

inScaled + inDensity + inTargetDensity

inScaled設(shè)置為true時(設(shè)置此標志時),如果inDensityinTargetDensity不為0鱼响,Bitmap 就會在加載的時候直接進行縮放以匹配 inTargetDensity 鸣剪,而不是繪制的時候進行縮放。(加載到堆內(nèi)存時已經(jīng)縮放了大小了丈积,.9圖 會忽略此標志)

inDensity:加載圖片的原始寬度筐骇,如果此密度與 inTargetDensity 不匹配,則在返回 Bitmap前會將它縮放至目標密度江滨。
inTargetDensity :目標圖片的顯示寬度铛纬,它與 inScaledinDensity 結(jié)合使用,確定如何在返回 Bitmap 前對其進行縮放唬滑。

前面講述的計算 Bitmap 大小的第二個例子告唆,就是將相同圖片加載放到不同的 drawable-dpi 的文件目錄下去加載到內(nèi)存中的 Bitmap 大小不同,其原因就是 inDensityinTargetDensity 不一致導(dǎo)致晶密。

Bitmap局部解碼

官網(wǎng)文檔-BitmapRegionDecoder 擒悬,BitmapRegionDecoder 可用于解碼圖像中的矩形區(qū)域。當原始圖像很大且只需要部分圖像時惹挟,BitmapRegionDecoder 尤其有用茄螃。 要創(chuàng)建 BitmapRegionDecoder缝驳,請調(diào)用 newInstance() 连锯。給定一個 BitmapRegionDecoder归苍,用戶可以重復(fù)調(diào)用 encodeRegio()以獲取指定區(qū)域的解碼后的 Bitmap

try {
    inputStream = getResources().getAssets().open("qq.jpg");
    BitmapRegionDecoder mRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
    BitmapFactory.Options sOptions = new BitmapFactory.Options();
    sOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
    sOptions.inSampleSize = 2;
    Rect mRect = new Rect();
    mRect.top = 0;
    mRect.left = 0;
    mRect.right = 100;
    mRect.bottom = 100;
    Bitmap bitmap = mRegionDecoder.decodeRegion(mRect, sOptions);
    //bitmap.getByteCount()=40000
} catch (IOException e) {
    e.printStackTrace();
}

這里需要注意的是 mRect 的寬高不能太大运怖,否則加載得到的 Bitmap 的時候也會出現(xiàn) OOM 的異常拼弃。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市摇展,隨后出現(xiàn)的幾起案子吻氧,更是在濱河造成了極大的恐慌,老刑警劉巖咏连,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盯孙,死亡現(xiàn)場離奇詭異,居然都是意外死亡祟滴,警方通過查閱死者的電腦和手機振惰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來垄懂,“玉大人骑晶,你說我怎么就攤上這事〔莼郏” “怎么了桶蛔?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長漫谷。 經(jīng)常有香客問我仔雷,道長,這世上最難降的妖魔是什么舔示? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任朽寞,我火速辦了婚禮,結(jié)果婚禮上斩郎,老公的妹妹穿的比我還像新娘脑融。我一直安慰自己,他們只是感情好缩宜,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布肘迎。 她就那樣靜靜地躺著,像睡著了一般锻煌。 火紅的嫁衣襯著肌膚如雪妓布。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天宋梧,我揣著相機與錄音匣沼,去河邊找鬼。 笑死捂龄,一個胖子當著我的面吹牛释涛,可吹牛的內(nèi)容都是我干的加叁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼唇撬,長吁一口氣:“原來是場噩夢啊……” “哼它匕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起窖认,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤豫柬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扑浸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烧给,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年喝噪,在試婚紗的時候發(fā)現(xiàn)自己被綠了创夜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡仙逻,死狀恐怖驰吓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情系奉,我是刑警寧澤檬贰,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站缺亮,受9級特大地震影響翁涤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜萌踱,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一葵礼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧并鸵,春花似錦鸳粉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至弯汰,卻和暖如春艰山,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背咏闪。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工曙搬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓纵装,卻偏偏與公主長得像征讲,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子搂擦,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容