性能優化:Bitmap內存大小優化的幾種常見方式
Android中的bitmap是比較佔用內存的,bitmap的大小直接影響到了應用佔用內存的大小。bitmap佔用內存大小的計算方式爲:
bitmap內存大小 = bitmap寬度(px) * bitmap長度(px) * 一個像素點佔用的字節數
BitmapFactory給我們提供了多個decode方法,我們可以從不同的數據源中加載bitmap,如下圖所示:
一個像素點佔用的字節數
對應的參數是Bitmap.Config,它是一個枚舉類,具體取值如下:
而每個新建的Bitmap的默認Config的值就Bitmap.Config.ARGB_8888,它表示一個像素點佔用4個字節((8 + 8 + 8 + 8) / 8 = 4byte)。
改變Bitmap內存大小
根據上面的原理,我們可以從兩個方面減少Bitmap的內存佔用,一個是改變Bitmap的寬高,另一個是改變Bitmap.Config的值,將Bitmap.Config.ARGB_8888改爲佔用字節更少的Bitmap.Config.ARGB_4444或者Bitmap.Config.RGB_565。
具體有以下幾種方法:
1、採樣率壓縮(改變Bitmap大小)。
2、通過martix進行壓縮(改變Bitmap大小)。Bitmap.createBitmap或者Bitmap.createScaledBitmap方法。
3、更改Bitmap.Config格式。
具體示例如下:
我們先來看下原圖大小:
原圖大小爲640px * 360px,且是放置在assets目錄下的,表示系統不會有縮放操作;如果是放在對應的drawable目錄下,則會由系統進行相應的縮放操作。
1、採樣率壓縮(改變Bitmap大小)。
val assetFileDescriptor = assets.openFd("maomi.jpg")
val fileInputStream = assetFileDescriptor.createInputStream()
val bmpInSampleSizeJpg = BitmapOptionsUtil.decodeSampledBitmapFromFileStream(
fileInputStream,
ScreenUtils.dip2px(this, 80f),
ScreenUtils.dip2px(this, 45f)
)
val descBmpConfigJpg =
"height:${bmpInSampleSizeJpg?.height},\nwidth:${bmpInSampleSizeJpg?.width},\nallocationByteCount:${bmpInSampleSizeJpg?.allocationByteCount}byte,\n" +
"byteCount:${bmpInSampleSizeJpg?.byteCount}byte,\nrowBytes:${bmpInSampleSizeJpg?.rowBytes}byte,\ndensity:${bmpInSampleSizeJpg?.density}"
tvInSampleSizeJpgInfo.text = "Jpg通過inSampleSize壓縮後的信息:$descBmpConfigJpg"
ivInSampleSizeJpg.setImageBitmap(bmpInSampleSizeJpg)
可以明顯的看到,圖片的寬高各自縮小爲原來的1/4,bitmap佔用內存大小爲原來的1/16。
採樣率壓縮Bitmap大小的方式,適用於與原有的圖片對象寬高比較大,而目標Bitmap的尺寸比較小,此時就沒有必要將Bitmap按照原有的大尺寸加載進來,可以有效的避免內存浪費和OOM。具體採樣率(inSampleSize)的計算,請看BitmapOptionsUtil。
2、通過martix進行壓縮(改變Bitmap大小)。
val matrix = Matrix()
matrix.setScale(0.1f, 0.1f)
val bmpMatrixJpg = Bitmap.createBitmap(
bmpOriginJpg, 0, 0, bmpOriginJpg.getWidth(),
bmpOriginJpg.getHeight(), matrix, true
)
// Bitmap.createScaledBitmap內部也會使用Matrix進行縮放
// val bmpMatrixJpg = Bitmap.createScaledBitmap(
// bmpOriginJpg,
// ScreenUtils.dip2px(this, 60f),
// ScreenUtils.dip2px(this, 45f),
// true
// )
val descBmpConfigJpg =
"height:${bmpMatrixJpg?.height},\nwidth:${bmpMatrixJpg?.width},\nallocationByteCount:${bmpMatrixJpg?.allocationByteCount}byte,\n" +
"byteCount:${bmpMatrixJpg?.byteCount}byte,\nrowBytes:${bmpMatrixJpg?.rowBytes}byte,\ndensity:${bmpMatrixJpg?.density}"
tvMatrixJpgInfo.text = "Jpg通過Matrix壓縮後的信息:$descBmpConfigJpg"
ivMatrixJpg.setImageBitmap(bmpMatrixJpg)
由於我們設置的縮放比是0.1f,也就是寬高均是之前的1/10,所以壓縮後的bitmap佔用的內存大小變爲原來的1/100。
這種情況適用原圖大小和目標bitmap大小均已知的情況。
3、更改Bitmap.Config格式。
val option = BitmapFactory.Options()
option.inPreferredConfig = Bitmap.Config.ARGB_4444
// option.inPreferredConfig = Bitmap.Config.RGB_565 // 對透明度沒要求的話可以試一下rgb_565
val bmpBmpConfigJpg = BitmapFactory.decodeStream(assets.open("maomi.jpg"), null, option)
val descBmpConfigJpg =
"height:${bmpBmpConfigJpg?.height},\nwidth:${bmpBmpConfigJpg?.width},\nallocationByteCount:${bmpBmpConfigJpg?.allocationByteCount}byte,\n" +
"byteCount:${bmpBmpConfigJpg?.byteCount}byte,\nrowBytes:${bmpBmpConfigJpg?.rowBytes}byte,\ndensity:${bmpBmpConfigJpg?.density}"
tvBmpConfigJpgInfo.text = "Jpg通過Bitmap.Config壓縮後的信息:$descBmpConfigJpg"
ivBmpConfigJpg.setImageBitmap(bmpBmpConfigJpg)
我們將Bitmap.Config的值改爲了根據輸出的byteCount的值改爲了Bitmap.Config.ARGB_4444,根據byteCount輸出的值可以明顯的看到,bitmap的內存大小減少了一半。
如果對透明度沒要求的話可以試一下Bitmap.Config.RGB_565。
這種情況適用於對圖片分辨率要求不高的情況。
通過Bitmap#compress方法壓縮,質量壓縮。
還有一種很重要的壓縮方式,通過Bitmap#compress方法,修改quality的值,來改變Bitmap生成的字節流的大小。這種方法不會改變Bitmap佔用的內存大小。
質量壓縮不會減少圖片的像素,它是在保持像素的前提下改變圖片的位深及透明度等,來達到壓縮圖片的目的。圖片的長,寬,像素都不變,那麼bitmap所佔內存大小是不會變的。這裏改變的是bitmap對應的字節數組的大小,適合去傳遞二進制的圖片數據,比如微信分享。
val bytearray = getBytesFromCompressBitmap(bmpOriginJpg, 32 * 1024)
val bmpQualityJpg = BitmapFactory.decodeByteArray(bytearray, 0, bytearray.size)
val descQualityJpg =
"height:${bmpQualityJpg.height},\nwidth:${bmpQualityJpg.width},\nallocationByteCount:${bmpQualityJpg.allocationByteCount}byte,\n" +
"byteCount:${bmpQualityJpg.byteCount}byte,\nrowBytes:${bmpQualityJpg.rowBytes}byte,\ndensity:${bmpQualityJpg.density},\n" +
"bytearray:${bytearray.size}"
tvQualityJpgInfo.text = "Jpg進行Quality壓縮後的信息:$descQualityJpg"
ivQualityJpg.setImageBitmap(bmpQualityJpg)
/**
* 將Bitmap的字節流壓縮爲目標大小
* @targetSize 單位爲Byte
*/
private fun getBytesFromCompressBitmap(
bitmap: Bitmap,
targetSize: Int
): ByteArray {
val baos = ByteArrayOutputStream()
var quality = 100
bitmap.compress(Bitmap.CompressFormat.PNG, quality, baos)
var bytes = baos.toByteArray()
while (bytes.size > targetSize && quality >= 5) {
quality -= 5
if (quality < 0) {
quality = 0
}
// 重置,不然會累加
baos.reset()
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos)
bytes = baos.toByteArray()
}
try {
baos.close()
} catch (e: Exception) {
e.printStackTrace()
}
return bytes
}
可以看到,質量壓縮不會改變原有bitmap的大小,它改變的是通過Bitmap#compress方法的字節流。
具體開發過程中,可以根據需要自行選擇合適的方式。
項目地址
具體頁面地址:提供了jpg和png兩種圖片格式的demo。