前言
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
脂矫、outHeight
和 outMimeType
。此方法可讓您在構(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*500
的 png
高清圖片.應(yīng)該是占用多少的內(nèi)存?
png
圖片應(yīng)該有alpha通道伟葫,所以 Bitmap.Config
是 ARGB_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)致縮放失真宪郊,如模糊眉睹。
例如,如果您有一個可繪制位圖資源废膘,它在中密度屏幕上的大小為 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)
宅此,因為mdip
到xxdpi
圖片的寬高分別會放大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)生均, x
(x
為 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è)置此標志時),如果inDensity與inTargetDensity不為0鱼响,Bitmap
就會在加載的時候直接進行縮放以匹配 inTargetDensity
鸣剪,而不是繪制的時候進行縮放。(加載到堆內(nèi)存時已經(jīng)縮放了大小了丈积,.9圖
會忽略此標志)
inDensity:加載圖片的原始寬度筐骇,如果此密度與 inTargetDensity
不匹配,則在返回 Bitmap
前會將它縮放至目標密度江滨。
inTargetDensity :目標圖片的顯示寬度铛纬,它與 inScaled
與 inDensity
結(jié)合使用,確定如何在返回 Bitmap
前對其進行縮放唬滑。
前面講述的計算 Bitmap
大小的第二個例子告唆,就是將相同圖片加載放到不同的 drawable-dpi
的文件目錄下去加載到內(nèi)存中的 Bitmap
大小不同,其原因就是 inDensity
和 inTargetDensity
不一致導(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
的異常拼弃。