性能優化:Bitmap內存大小優化的幾種常見方式

性能優化:Bitmap內存大小優化的幾種常見方式

Android中的bitmap是比較佔用內存的,bitmap的大小直接影響到了應用佔用內存的大小。bitmap佔用內存大小的計算方式爲:

bitmap內存大小 = bitmap寬度(px) * bitmap長度(px) * 一個像素點佔用的字節數

BitmapFactory給我們提供了多個decode方法,我們可以從不同的數據源中加載bitmap,如下圖所示:

BitmapFactory加載Bitmap

一個像素點佔用的字節數對應的參數是Bitmap.Config,它是一個枚舉類,具體取值如下:

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)

Matrix縮放Bitmap

由於我們設置的縮放比是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減少內存佔用

我們將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方法的字節流。

具體開發過程中,可以根據需要自行選擇合適的方式。

項目地址

tinyvampirepudge/AndroidStudy

具體頁面地址:提供了jpg和png兩種圖片格式的demo。

BitmapCompressActivity

參考

How to get FileInputStream to File in assets folder

bitmap的六種壓縮方式,Android圖片壓縮

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章