Android Bitmap 詳解:關(guān)于 Bitamp 你所要知道的一切

Android.jpg

本文已授權(quán)微信公眾號(hào) : code小生
(codexiaosheng) 在微信公眾平臺(tái)原創(chuàng)首發(fā)

前言

在平時(shí)的 Android 開(kāi)發(fā)中,與 Bitmap 打交道可以說(shuō)是再常見(jiàn)不過(guò)的事了。我在寫這篇文章之前,對(duì)于 Bitmap 相關(guān)的一些東西總是模模糊糊,比如 Bitmap 的文件大小還有占用內(nèi)存大小的區(qū)別,還有對(duì) Bitmap 壓縮的幾種方法各自的區(qū)別和用途是什么,等等

在這篇文章中,我將會(huì)把在 Bitmap 中相關(guān)的知識(shí)點(diǎn)都一一介紹,如果你也是對(duì) Bitmap 總是感覺(jué)模模糊糊的話, 相信你看完這篇文章后一定會(huì)有所收獲

目錄

一、Bitmap 的創(chuàng)建
二、Bitmap 的顏色配置信息與壓縮方式信息
三、Bitmap 的轉(zhuǎn)換與保存
四、Bitmap 的文件大小
五、Bitmap 占用內(nèi)存的大小
六、影響 Bitmap 占用內(nèi)存大小的因素
七、Bitmap 的加載優(yōu)化與壓縮
八、Bitmap 的其他操作

一、Bitmap 的創(chuàng)建

我們?nèi)绾蝿?chuàng)建一個(gè) Bitamap 對(duì)象呢?Google 給我們提供了兩種方式:

    1. Bitmap 的靜態(tài)方法 createBitmap(XX)


      image.png
    1. BitmapFactory 的 decodeXX 系列靜態(tài)方法


      image.png

二、Bitmap 的顏色配置信息與壓縮方式信息

Bitmap 中有兩個(gè)內(nèi)部枚舉類:Config 和 CompressFormat,Config 是用來(lái)設(shè)置顏色配置信息的,CompressFormat 是用來(lái)設(shè)置壓縮方式的

image.png

Config

Config 類描述了一個(gè) Bitmap 是如何存儲(chǔ)像素信息的,它影響了圖片的質(zhì)量(顏色深度)以及顯示透明/不透明顏色的能力

顏色格式 描述 每個(gè)像素占用內(nèi)存大小
Bitmap.Config.ALPHA_8 顏色信息只由透明度組成 8 位,即 1 字節(jié)
Bitmap.Config.ARGB_4444 顏色信息由透明度與R(Red),G(Green),B(Blue)四部分組成,每個(gè)部分都占4位,總共占16位 16 位,即 2 字節(jié)
Bitmap.Config.ARGB_8888 顏色信息由透明度與R(Red),G(Green),B(Blue)四部分組成,每個(gè)部分都占8位,總共占32位。是Bitmap默認(rèn)的顏色配置信息,也是最占空間的一種配置 32 位,即 4 字節(jié)
Bitmap.Config.RGB_565 顏色信息由R(Red),G(Green),B(Blue)三部分組成,R占5位,G占6位,B占5位,總共占16位 16 位,即 2 字節(jié)

關(guān)于圖片的顏色格式,有幾點(diǎn)需要注意:

  1. Bitmap 默認(rèn)的圖片格式是 ARGB_8888
  2. 圖片占用內(nèi)存的大小與圖片的顏色格式相關(guān), 占用內(nèi)存的大小 = 圖片的寬度 × 圖片的高度 × 每個(gè)像素占用的內(nèi)存大小
  3. 當(dāng)我們需要做性能優(yōu)化或者防止 OOM 的時(shí)候,可以將 Bitamp 的顏色配置該為 RGB_565 ,它的占用內(nèi)存大小是 ARGB_8888的一半
    例如:
  val options = BitmapFactory.Options()
  options.inPreferredConfig = Bitmap.Config.RGB_565  // 設(shè)置bitmap的顏色格式
  val bitmap = BitmapFactory.decodeResource(resources, R.drawable.pic, options)

注意: RGB_565 是不支持透明度的,如果你需要顯示帶有透明度的圖片,不要用此格式

CompressFormat

CompressFormat 描述了將 Bitmap 以什么方式壓縮,它有3個(gè)值:

壓縮方式 描述
Bitmap.CompressFormat.JPEG 表示以JPEG壓縮算法進(jìn)行圖像壓縮,壓縮后的格式可以是".jpg"或者".jpeg",是一種有損壓縮
Bitmap.CompressFormat.PNG 表示以PNG壓縮算法進(jìn)行圖像壓縮,壓縮后的格式可以是".png",是一種無(wú)損壓縮
Bitmap.CompressFormat.WEBP 表示以WebP壓縮算法進(jìn)行圖像壓縮,壓縮后的格式可以是".webp",是一種有損壓縮,質(zhì)量相同的情況下,WebP格式圖像的體積要比JPEG格式圖像小40%。美中不足的是,WebP格式圖像的編碼時(shí)間“比JPEG格式圖像長(zhǎng)8倍”

例如:

  fun bitmapToByteArray(bitmap: Bitmap): ByteArray {
        val baos = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
        return baos.toByteArray()
    }

三、Bitmap 的保存和轉(zhuǎn)換

前面介紹了如何創(chuàng)建一個(gè) Bitmap,當(dāng)我們拿到一個(gè) Bitmap 對(duì)象后,通常還有有以下操作:

1. 將 Bitamap 轉(zhuǎn)換為 byte 數(shù)組

  fun bitmapToByteArray(bitmap: Bitmap): ByteArray {
        val baos = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
        return baos.toByteArray()
    }

2. 將 Bitamap 保存為 文件

   fun bitmapToFile(bitmap: Bitmap, file: File): Boolean {
        val baos = ByteArrayOutputStream()
        val fileOutputStream = FileOutputStream(file)

        return try {
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
            fileOutputStream.write(baos.toByteArray())
            true
        } catch (e: Exception) {
            e.printStackTrace()
            false
        } finally {
            baos.close()
            fileOutputStream.close()
        }
    }

四、Bitmap 的文件大小

說(shuō)到 Bitmap 大小這一塊的時(shí)候,我們一定要先搞清楚幾個(gè)概念:

  • Bitmap 原始的文件大小

  • 把一個(gè) Bitamp 通過(guò)壓縮保存到本地的文件大小

  • Bitmap 加載到內(nèi)存中占用的內(nèi)存大小

注意:通常情況下,這三個(gè)值不相等!

我們以一張 寬高為 1080 * 1920 ,圖片原始大小為 705 kb 的圖片為例(本文均以此圖片為例),逐個(gè)解釋和驗(yàn)證這三個(gè)數(shù)據(jù):

1. Bitmap 原始的文件大小

這個(gè)很好理解,就是圖片的自身的大小嘛,沒(méi)有經(jīng)過(guò)任何處理,通過(guò)下圖我們可以看到,這張圖片的原始大小是 705 kb:

image.png
image.png

注意,如果我們直接在 Android Studio 中打開(kāi)這張圖片的話,上面顯示的圖片大小是 721.96 kB,而在 Windows 中屬性顯示的是705 KB,這兩者為什么不同呢?
如果仔細(xì)觀察的話,會(huì)發(fā)現(xiàn),這兩個(gè)數(shù)值的單位不一樣!一個(gè)是 kB,一個(gè)是 KB。

kB(Kilobyte),是“千字節(jié)”(" kilobyte")的一種廣泛運(yùn)用的縮寫,其意義是1000字節(jié)。根據(jù)國(guó)際單位制標(biāo)準(zhǔn),1kB = 1000B(字節(jié), Byte)
由于計(jì)算機(jī)學(xué)家長(zhǎng)期使用二進(jìn)制系統(tǒng),一個(gè)千字節(jié)是基于2的冪次的,事實(shí)上一千字節(jié)是2或者說(shuō)是1024個(gè)字節(jié)。
千字節(jié)也常指1024 (2^10)字節(jié),因?yàn)?000約等于1024。Microsoft Windows 系統(tǒng)中仍在大量使用公制前綴的二進(jìn)制寫法(即 1千字節(jié) = 1024 B)
所以上面兩張圖片的大小顯示不一致的情況。

下面我們通過(guò)代碼驗(yàn)證一下:

  1. 把圖片放到工程的 assets 目錄下
  2. 通過(guò)下面代碼加載圖片,然后打印出圖片的大?。?/li>
val bytes = assets.open("pic.jpg").readBytes()
log("原始文件大小 :${bytes.size / 1024} kb")

日志輸出如下:
原始文件大小 :705 kb

2. Bitamp 通過(guò)壓縮保存到本地的文件大小

通過(guò)上面的驗(yàn)證,我們知道,這張圖片的原始大小是 705 kb。如果我們把這張圖片保存到手機(jī)上,那么它的大小還會(huì)是 705 kb 么?

把這張圖片保存到手機(jī)上,分兩種情況:

1). 直接拿到圖片的輸入流或者說(shuō) byte 數(shù)組,然后保存到本地

  val bytes = assets.open("pic.jpg").readBytes()
  log("assets 中讀取的大小 : ${bytes.size / 1024} kb")

  val file = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "pic.jpg")
  if (!file.exists()) {
       file.createNewFile()
     }

  FileUtils.writeToFile(bytes, file)
  log("保存到本地的圖片大小 ${file.readBytes().size / 1024} kb")


   // FileUtil 類中的 writeToFile 方法:
  fun writeToFile(data: ByteArray, file: File) {
      val fileOutputStream = FileOutputStream(file)
        try {
            fileOutputStream.write(data)
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            fileOutputStream.close()
        }
    }

日志輸出如下:

assets 中讀取的大小 : 705 kb
保存到本地的圖片大小 705 kb

然后我們?cè)衮?yàn)證一下保存的圖片信息:

image.png

2.) 創(chuàng)建一個(gè) Bitmap,然后保存到本地

  val bytes = assets.open("pic.jpg").readBytes()
  log("assets 中讀取的大小 : ${bytes.size / 1024} kb")

  val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
  val baos = ByteArrayOutputStream()
  bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)  //壓縮圖片并將數(shù)據(jù)存儲(chǔ)到 ByteArrayOutputStream 中

  val file = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "pic.jpg")
  if (!file.exists()) {
         file.createNewFile()
     }
  val fileOutputStream = FileOutputStream(file)
  fileOutputStream.write(baos.toByteArray())
        
  log("保存到本地的圖片大小 : ${file.readBytes().size / 1024} kb")

日志輸出如下:

assets 中讀取的大小 : 705 kb
保存到本地的圖片大小 :  817 kb

再查看一下保存的圖片信息:

image.png

到這里,我們就會(huì)有疑問(wèn)了,為什么通過(guò) Bitmap 轉(zhuǎn)換之后圖片大小就不一樣了呢?

關(guān)鍵就在這一句,

 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)  //壓縮圖片并將數(shù)據(jù)存儲(chǔ)到 ByteArrayOutputStream 中

當(dāng)我們需要把一個(gè) Bitmap 對(duì)象保存到本地時(shí),需要先將其轉(zhuǎn)換成 byte 數(shù)組,這個(gè)過(guò)程是通過(guò) Bitmap 的 compress 方法完成的。

這個(gè)方法中第一個(gè)參數(shù)代表保存的圖片類型,第二個(gè)參數(shù)代表圖片的質(zhì)量。這個(gè)值的范圍是 0~100,數(shù)值越大圖片質(zhì)量越高,同時(shí)保存后的圖片大小也越大。

也就是說(shuō),當(dāng)通過(guò)這個(gè)方法把一張 Bitmap 保存到本地時(shí),第二參數(shù)控制了保存的圖片質(zhì)量,同時(shí)也就影響了保存圖片的大小

3. Bitmap 加載到內(nèi)存中占用的內(nèi)存大小

請(qǐng)看第五部分

小結(jié)

  • Bitmap 原始的文件大小Bitamp 壓縮保存到本地的大小Bitmap 加載到內(nèi)存中占用的內(nèi)存大小,這三者是三個(gè)不同的概念,且通常這三者并不相等

  • 將 Bitmap 保存到本地時(shí),可以通過(guò) compress 方法的第二個(gè)參數(shù)控制圖片的質(zhì)量,從而達(dá)到控制圖片大小的目的。(用于圖片壓縮,后面會(huì)介紹)

五、Bitmap 占用內(nèi)存的大小

終于到了大家最最關(guān)心的點(diǎn),Bitmap 占用內(nèi)存的大小!很多時(shí)候,我們只是朦朦朧朧的知道,加載大的圖片要注意,防止OOM。

但是,加載一張圖片到底占用多少內(nèi)存呢?

如何計(jì)算加載一張圖片到底占用多少內(nèi)存

來(lái)人,上公式:

總內(nèi)存 = 寬 × 高 × 色彩空間

把上面的公式再詳細(xì)描述一下就是:

總內(nèi)存 = 寬的像素?cái)?shù) × 高的像素?cái)?shù) × 每個(gè)像素點(diǎn)占用的大小

這個(gè)公式也很好理解,寬 × 高 即圖片總共有多少像素點(diǎn),然后乘 每個(gè)像素點(diǎn)占用的大小 就得出了總內(nèi)存。

Bitmap 中直接提供了相關(guān)方法得到圖片所占用的內(nèi)存大?。?/p>

  • getAllocationByteCount() // API 19 以后使用
  • getByteCount()

除了系統(tǒng)提供的方法,我們也可以根據(jù)上面的公式自己計(jì)算。

接下來(lái)我們就通過(guò)系統(tǒng)提供的方法和我們自己計(jì)算來(lái)驗(yàn)證一下:

1). 從 assets 目錄中加載圖片,并計(jì)算占用的內(nèi)存大?。?/p>

// 加載圖片
val bytes = assets.open("pic.jpg").readBytes()
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
ivPic.setImageBitmap(bitmap)

log("占用內(nèi)存大小: ${Bitmaps.getMemorySize(rawBitmap)} kb \n")
log("計(jì)算占用內(nèi)存大小: ${Bitmaps.calculateMemorySize(rawBitmap)} kb \n")


// 使用系統(tǒng) api 提供的方法計(jì)算
// Bitmaps 中的 getMemorySize 方法
 fun getMemorySize(bitmap: Bitmap, sizeType: SizeType = SizeType.KB): Int {
        val bytes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {  //Since API 19
            bitmap.allocationByteCount
        } else {
            bitmap.byteCount
        }

        return when (sizeType) {
            SizeType.B -> bytes
            SizeType.KB -> bytes / 1024
            SizeType.MB -> bytes / 1024 / 1024
            SizeType.GB -> bytes / 1024 / 1024 / 1024
        }
 }

// 根據(jù)公式手動(dòng)計(jì)算
// Bitmaps 中的 calculateMemorySize方法
  fun calculateMemorySize(bitmap: Bitmap, sizeType: SizeType = SizeType.KB): Int {
        val pixels = bitmap.width * bitmap.height
        val bytes = when (bitmap.config) {
            Bitmap.Config.ALPHA_8 -> pixels * 1
            Bitmap.Config.ARGB_4444 -> pixels * 2
            Bitmap.Config.ARGB_8888 -> pixels * 4
            Bitmap.Config.RGB_565 -> pixels * 2
            else -> pixels * 4
        }

        return when (sizeType) {
            SizeType.B -> bytes
            SizeType.KB -> bytes / 1024
            SizeType.MB -> bytes / 1024 / 1024
            SizeType.GB -> bytes / 1024 / 1024 / 1024
        }
    }

    // 單位的枚舉類
    enum class SizeType {
        B,
        KB,
        MB,
        GB
    }

注: Bitmaps 是我自己定義的一個(gè)工具類,并不是系統(tǒng)的一個(gè)類。源碼在文章最下面

2). 計(jì)算結(jié)果如下:


image.png

我們可以看到,利用系統(tǒng)提供的 api 與 我們自己用公式計(jì)算得出的占用內(nèi)存大小是一樣的。

對(duì)于這張圖片來(lái)說(shuō),寬高為 1080 * 1920,圖片的顏色格式是 ARGB_8888,證明每個(gè)像素占用 4 個(gè)字節(jié)的內(nèi)存,所以加載它占用的內(nèi)存就是:

總內(nèi)存 = 寬 * 高 * 色彩空間 = 1080 * 1920 * 4 = 8294400 byte = 8100 KB = 7.9 MB

注:
1 byte = 8 bit
1 KB = 1024 byte
1 MB = 1024 KB
1 GB = 1024 MB

六、影響 Bitmap 占用內(nèi)存大小的因素

根據(jù)公式:

總內(nèi)存 = 寬的像素?cái)?shù) × 高的像素?cái)?shù) × 每個(gè)像素點(diǎn)占用的大小

可以得出,影響占用內(nèi)存的大小因素有:

  • 寬高
  • 色彩空間

所以,當(dāng)我們需要對(duì) Bitamp 加載進(jìn)行優(yōu)化的時(shí)候,就可以從這兩個(gè)方面進(jìn)行著手:

  • 減少 Bitmap 的寬高
  • 使用占用更少內(nèi)存的色彩模式

除了上面兩點(diǎn),還有一個(gè)因素也會(huì)影響到 Bitamp 占用的內(nèi)存大小,它就是 縮放

縮放

1. 什么是縮放

根據(jù)前面幾部分的介紹,我們知道,加載一張 1080 * 1920 的圖片,然后通過(guò) bitmap.getWidth() 和 bitmap.getHeight() 得到的也是 1080 * 1920

如果圖片的原始大小是 1080 * 1920,那邊加載出來(lái)的 Bitmap 對(duì)象也一定是 1080 * 1920 么?

答案是否定的。在加載 Bitamp 對(duì)象時(shí)可以手動(dòng)設(shè)置 inSampleSize 來(lái)進(jìn)行縮放。另外,如果是從 Drawable 目錄下加載圖片的話,系統(tǒng)會(huì)默認(rèn)地根據(jù)圖片所在的 Drawable 目錄以及手機(jī)的 DPI 對(duì)加載的圖片進(jìn)行縮放

2. 縮放是如何影響影響占用內(nèi)存的

當(dāng)我們對(duì)圖片進(jìn)行縮放時(shí),實(shí)際上造成的結(jié)果是圖片寬高的改變,通過(guò)上面的公式我們可以知道,寬高改變了,占用的內(nèi)存也就改變了。

所以圖片的縮放對(duì)內(nèi)存的影響本質(zhì)上還是寬高對(duì)占用內(nèi)存的影響

3. Bitmap 中如何對(duì)圖片進(jìn)行縮放

1)、 手動(dòng)設(shè)置縮放參數(shù)

當(dāng)我們創(chuàng)建一個(gè) Bitmap 對(duì)象的時(shí)候,會(huì)有一個(gè)可選的 Options 對(duì)象,其中的 inSampleSize 參數(shù)可以控制縮放的比例,inSampleSize 的值代表 圖片的寬度、高度分別變?yōu)樵瓉?lái)的 1/inSampleSize

比如一張 1080 * 1920 的圖片,如果加載時(shí)設(shè)置了 inSampleSize = 2,證明圖片的寬度變?yōu)樵瓉?lái)的 1/2,高度也變?yōu)樵瓉?lái)的 1/2,所以得到的 Bitmap 對(duì)象的寬高是 540 * 860

根據(jù)上面的占用內(nèi)存的計(jì)算公式,它占用的內(nèi)存大小就變?yōu)樵瓉?lái)的 1/2 * 1/2 = 1/4

下面我們來(lái)驗(yàn)證一下,還是那張圖片,在加載的時(shí)候設(shè)置 inSampleSize = 2 ,然后看一下圖片的寬高和占用內(nèi)存的情況:

 val bytes = assets.open("pic.jpg").readBytes()

 val options = BitmapFactory.Options()
 ptions.inSampleSize = 2
 val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)

 ivPic.setImageBitmap(bitmap)
 showInfo(bitmap)

結(jié)果如下:


image.png

我們可以看到,圖片的寬高由原來(lái)的 1080 * 1920 變成了 540 * 960,寬高分別變?yōu)樵瓉?lái)的 1/2,占用內(nèi)存的大小由原來(lái)的 8100 變成了 2025,內(nèi)存大小變?yōu)榱嗽瓉?lái)的 1/2 * 1/2 = 1/4

根據(jù)這個(gè)特性,我們可以在加載大圖的時(shí)候進(jìn)行縮放處理,防止OOM的發(fā)生

注意,inSampleSize 的值要求必須大于1,且只能是2的整數(shù)倍

2)、 從 Drawable 目錄中加載圖片的自動(dòng)縮放

當(dāng)我們從 assets 目錄中或者網(wǎng)絡(luò)上加載一張圖片的時(shí)候,默認(rèn)情況下得到的 Bitmap 對(duì)象的寬高是與原圖片的寬高一致的。比如前面的我們舉的例子,寬高都是 1080 * 1920

如果我們從 Drawable 目錄下加載圖片的話,系統(tǒng)會(huì)根據(jù)圖片所在的目錄以及手機(jī)的DPI對(duì)圖片進(jìn)行縮放

下面是手機(jī) dpi 與 Drawable 目錄的對(duì)應(yīng)關(guān)系圖:

DPI 分辨率 系統(tǒng)dpi 基準(zhǔn)比例 對(duì)應(yīng)Drawable目錄
ldpi 240x320 120 0.75 drawable-ldpi(低密度)
mdpi 320x480 160 1 drawable-mdpi(中等密度)
hdpi 480x800 240 1.5 drawable-hdpi(高密度)
xhdpi 720x1280 320 2 drawable-xhdpi(超高密度)
xxhdpi 1080x1920 480 3 drawable-xxhdpi(超超高密度)
xxxhdpi 2160 x3840 640 4 drawable-xxxhdpi(超超超高密度)

Drawable 目錄的選擇流程

當(dāng)我們從 Drawable 目錄中加載一張圖片的時(shí)候:

  1. 比如在一個(gè)中等分辨率的手機(jī)上,Android 就會(huì)選擇d rawable-mdpi 文件夾下的圖片,文件夾下有這張圖就會(huì)優(yōu)先被使用,在這種情況下,圖片是不會(huì)被縮放的

  2. 但是如果沒(méi)有在 drawable-mdpi 的文件夾下找到相應(yīng)圖片的話,
    Android 系統(tǒng)會(huì)首先從更高一級(jí)的 drawable-hdpi 文件夾中查找,
    如果找到圖片資源就進(jìn)行縮放處理(縮小),顯示在屏幕上

  3. 如果 drawable-hdpi 文件夾下也沒(méi)有的話,就依次往 drawable-xhdp i文件夾、drawable-xxhdpi 文件夾、
    drawable-xxxhdpi 文件夾、drawable-nodpi 文件夾中尋找

  4. 如果更高密度的文件夾里都沒(méi)有找到,就往更低密度的文件夾 drawable-ldpi 文件夾下查找。如果找到圖片資源就進(jìn)行縮放處理(放大),顯示在屏幕上

  5. 如果都沒(méi)找到,最終會(huì)在默認(rèn)的drawable文件夾中尋找,如果默認(rèn)的drawable文件夾中也沒(méi)有那就會(huì)報(bào)錯(cuò)啦

Drawable 縮放規(guī)則小結(jié)

  • 如果圖片所在的文件夾 dpi 剛好是手機(jī)屏幕密度所對(duì)應(yīng)的文件夾(比如:手機(jī) dpi 為 xxhdpi,圖片在 drawable-xxhdpi 文件夾中),
    則該圖片不會(huì)被壓縮

  • 如果圖片所在目錄 dpi 低于匹配目錄,那么該圖片被認(rèn)為是為低密度設(shè)備需要的,現(xiàn)在要顯示在高密度設(shè)備上,圖片會(huì)被放大,寬和高,以及占用的內(nèi)存都會(huì)變大

注意:如果圖片本身就比較大,而又放在了密度較低的文件夾中,
加載時(shí)會(huì)導(dǎo)致占用內(nèi)存變得非常大,導(dǎo)致OOM

  • 如果圖片所在目錄 dpi 高于匹配目錄,那么該圖片被認(rèn)為是為高密度設(shè)備需要的,現(xiàn)在要顯示在低密度設(shè)備上,圖片會(huì)被縮小,寬和高,以及占用的內(nèi)存都會(huì)變小

  • 如果圖片所在目錄為 drawable-nodpi,則無(wú)論設(shè)備 dpi 為多少,保留原圖片大小,不進(jìn)行縮放

驗(yàn)證

以我的手機(jī)為例,屏幕分辨率是 1080 * 1920,DPI 是 480,對(duì)應(yīng)的 Drawable 目錄是 drawable-xxhdpi(超超高密度)

  1. 把圖片拷貝到 drawable-xxhdpi 目錄下,然后加載圖片并顯示其信息
 val bitmap = BitmapFactory.decodeResource(resources, R.drawable.pic)

ivPic.setImageBitmap(bitmap)
showInfo(bitmap)  //顯示圖片信息

根據(jù)上面的介紹的規(guī)則,我們加載圖片所對(duì)應(yīng)的 Drawable 與我們的手機(jī) DPI 相匹配,所以圖片不會(huì)進(jìn)行縮放

image.png
  1. 把圖片放拷貝到 drawable-xxxhdpi 目錄下(高于手機(jī)DPI),然后加載圖片并顯示其信息,此時(shí)圖片會(huì)被縮小
image.png
  1. 把圖片放拷貝到 drawable-xhdpi 目錄下(低于手機(jī)DPI),然后加載圖片并顯示其信息,此時(shí)圖片會(huì)被放大
image.png

注意:
在做測(cè)試的時(shí)候,要保證同時(shí)只有一個(gè) drawable 文件夾中存在需要加載的那張圖片

4. 小結(jié)

  • 總內(nèi)存 = 寬的像素?cái)?shù) × 高的像素?cái)?shù) × 每個(gè)像素點(diǎn)占用的大小
  • 由以上公式可以知道影響內(nèi)存占用大小的因素是 寬高和色彩空間
  • 加載 一個(gè) Bitamap 可以通過(guò)設(shè)置 inSampleSize 的值控制加載得到的圖片的大小
  • 從 Drawable 目錄中加載圖片時(shí),系統(tǒng)會(huì)根據(jù)手機(jī) DPI 和 Drawable 目錄對(duì)圖片進(jìn)行縮放

七、Bitmap 的加載優(yōu)化與壓縮

1. 質(zhì)量壓縮

    /**
     * 將圖片 [bitmap] 壓縮到指定大小 [targetSize] 以內(nèi) ,單位是 kb
     * 這里的大小指的是 “文件大小”,而不是 “內(nèi)存大小”
     **/
   fun compressQuality(bitmap: Bitmap, targetSize: Int, declineQuality: Int = 10): ByteArray {

        val baos = ByteArrayOutputStream()

        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
        log("壓縮前文件大?。?{baos.toByteArray().size / 1024} kb")

        var quality = 100
        while ((baos.toByteArray().size / 1024) > targetSize) {
            baos.reset()
            quality -= declineQuality
            bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos)
        }

        log("壓縮后文件大?。?{baos.toByteArray().size / 1024} kb")

        return baos.toByteArray()
    }
  1. 質(zhì)量壓縮不會(huì)減少圖片的像素,它是在保持像素的前提下改變圖片的位深及透明度,來(lái)達(dá)到壓縮圖片的目的
  2. 壓縮后圖片的長(zhǎng),寬,像素都不會(huì)改變,那么 bitmap 所占內(nèi)存大小是不會(huì)變的
  3. 由于圖片的質(zhì)量變低了,所以壓縮后圖片的大小會(huì)變小
  4. 質(zhì)量壓縮 png 格式這種圖片沒(méi)有作用,因?yàn)?png 是無(wú)損壓縮


    image.png

2. 采樣率壓縮


  /**
   * 將圖片 [byteArray] 壓縮到 寬度小于 [targetWidth]、高度小于 [targetHeight]
   *
   **/
  fun compressInSampleSize(byteArray: ByteArray, targetWidth: Int, targetHeight: Int): ByteArray {

        val options = BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)

        var inSampleSize = 1
        while (options.outWidth / inSampleSize > targetWidth || options.outHeight / inSampleSize > targetHeight) {
            inSampleSize *= 2
        }

        options.inJustDecodeBounds = false
        options.inSampleSize = inSampleSize
        val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)

        val compressedByreArray = bitmapToByteArray(bitmap)

        log("壓縮前文件大小 :${byteArray.size / 1024} kb")
        log("采樣率 :$inSampleSize ")
        log("壓縮后文件大小 :${compressedByreArray.size / 1024} kb")

        return compressedByreArray
    }
  1. 采樣率壓縮其原理是縮放 bitmap 的尺寸
  2. 壓縮后圖片的 寬度、高度以及占用的內(nèi)存都會(huì)變小,文件大小也會(huì)變小(指壓縮后保存到本地的文件)
  3. 采樣率 inSampleSize 代表 寬度、高度變?yōu)樵瓉?lái)的幾分之一,
    比如 inSampleSize 為 2,代表 寬度、高度都變?yōu)樵瓉?lái)的 1/2,占用的內(nèi)存就會(huì)變?yōu)樵瓉?lái)的 1/4
  4. 采樣率 inSampleSize 只能為 2 的整次冪,比如:2、4、8、16 ...
  5. 由于 inSampleSize 只能為 2 的整次冪,所以無(wú)法精確控制大小
image.png

3. 縮放壓縮

    /**
     * 將圖片 [bitmap] 壓縮到指定寬高范圍內(nèi)
    **/
    fun compressScale(bitmap: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
        return try {
            val scale = Math.min(targetWidth * 1.0f / bitmap.width, targetHeight * 1.0f / bitmap.height)

            val matrix = Matrix()
            matrix.setScale(scale, scale)

            val scaledBitmap = Bitmap.createScaledBitmap(bitmap, (bitmap.width * scale).toInt(), (bitmap.height * scale).toInt(), true)

            val rawBytes = bitmapToByteArray(bitmap)
            val scaledBytes = bitmapToByteArray(scaledBitmap)
            log("壓縮前文件大小 :${rawBytes.size / 1024} kb")
            log("縮放率 :$scale ")
            log("壓縮后文件大小 :${scaledBytes.size / 1024} kb")

            scaledBitmap
        } catch (e: Exception) {
            e.printStackTrace()
            bitmap
        }
    }
  1. 放縮法壓縮使用的是通過(guò)矩陣對(duì)圖片進(jìn)行縮放
  2. 縮放后圖片的 寬度、高度以及占用的內(nèi)存都會(huì)變小,文件大小也會(huì)變小(指壓縮后保存到本地的文件,原始文件不會(huì)改變)
image.png

4. 色彩模式壓縮(RGB565)

    /**
     * 將圖片格式更改為 Bitmap.Config.RGB_565,減少圖片占用的內(nèi)存大小
    **/
    fun compressRGB565(byteArray: ByteArray): Bitmap {

        return try {
            val options = BitmapFactory.Options()
            options.inPreferredConfig = Bitmap.Config.RGB_565
            val compressedBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)

            log("壓縮前文件大小 :${byteArray.size / 1024} kb")
            log("壓縮后文件大小 :${byteArray.size / 1024} kb")
            compressedBitmap
        } catch (e: Exception) {
            e.printStackTrace()
            BitmapFactory.decodeByteArray(ByteArray(0), 0, 0)
        }
    }
  1. 由于圖片的存儲(chǔ)格式改變,與 ARGB_8888 相比,每個(gè)像素的占用的字節(jié)由 8 變?yōu)?4 , 所以圖片占用的內(nèi)存也為原來(lái)的一半
  2. 圖片的寬高不發(fā)生變化
  3. 如果圖片不包含透明信息的話,可以使用此方法進(jìn)行壓縮
image.png

八、Bitmap 的其他操作

1. 旋轉(zhuǎn)

    /**
     * 旋轉(zhuǎn)
     *
     * 注意:如果 [degree] 不是90的倍數(shù)的話,會(huì)導(dǎo)致旋轉(zhuǎn)后圖片變成"斜的",
     * 然而此時(shí)計(jì)算圖片的寬高時(shí)仍然是按照水平和豎直方向計(jì)算,所以會(huì)導(dǎo)致最終旋轉(zhuǎn)后的圖片變大
     * 如果進(jìn)行多次旋轉(zhuǎn)的話,最終會(huì)出現(xiàn)OMM
     */
    fun rotate(bitmap: Bitmap, degree: Float): Bitmap {
        val matrix = Matrix()
        matrix.postRotate(degree)
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)
    }

2. 鏡像

    /**
     * 水平鏡像
     */
    fun mirrorX(bitmap: Bitmap): Bitmap {
        val matrix = Matrix()
        matrix.setScale(-1f, 1f)
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)
    }

    /**
     * 豎直鏡像
     */
    fun mirrorY(bitmap: Bitmap): Bitmap {
        val matrix = Matrix()
        matrix.setScale(1f, -1f)
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)
    }

3. 裁切

  /**
     * 從圖片中間位置裁剪出一個(gè)寬高為的 [width] [height]圖片
     */
    fun crop(bitmap: Bitmap, width: Int, height: Int): Bitmap {
        return if (bitmap.width < width || bitmap.height < height) {
            bitmap
        } else {
            Bitmap.createBitmap(bitmap, (bitmap.width - width) / 2, (bitmap.height - height) / 2, width, height)
        }
    }

    /**
     * 從圖片中間位置裁剪出一個(gè)半徑為 [radius] 的圓形圖片
     */
    fun cropCircle(bitmap: Bitmap, radius: Int): Bitmap {

        val realRadius: Int = if (bitmap.width / 2 < radius || bitmap.height / 2 < radius) {
            Math.min(bitmap.width, bitmap.height) / 2
        } else {
            radius
        }

        val src = crop(bitmap, realRadius * 2, realRadius * 2)
        val circle = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)

        val canvas = Canvas(circle)
        canvas.drawARGB(0, 0, 0, 0)
        val paint = Paint()
        paint.isAntiAlias = true

        canvas.drawCircle((circle.width / 2).toFloat(), (circle.height / 2).toFloat(), realRadius.toFloat(), paint)

        val rect = Rect(0, 0, circle.width, circle.height)
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
        canvas.drawBitmap(src, rect, rect, paint)

        return circle
    }

九、 總結(jié)

  1. Bitmap 的顏色配置,以及不同格式占用內(nèi)存的大小
  2. 注意區(qū)分 原始圖片大小、Bitmap 對(duì)象的大小(寬、高)、Bitmap 占用內(nèi)存的大小、將 Bitmap 保存成文件的大小
  3. Bitmap 占用內(nèi)存:總內(nèi)存 = 寬的像素?cái)?shù) × 高的像素?cái)?shù) × 每個(gè)像素點(diǎn)占用的大小
  4. Bitmap 的縮放和從 Drawable 目錄中加載圖片的規(guī)則
  5. Bitmap 的幾種壓縮方法和各自的特點(diǎn)

相關(guān)代碼

https://github.com/smashinggit/Study

注:此工程包含多個(gè)module,本文所用代碼均在 bitmap module 下

注:由于本人水平有限,所以難免會(huì)有理解偏差或者使用不正確的問(wèn)題。如果小伙伴們有更好的理解或者發(fā)現(xiàn)有什么問(wèn)題,歡迎留言批評(píng)指正~

參考文章:

玩轉(zhuǎn)Android Bitmap
怎樣計(jì)算Bitmap的內(nèi)存占用和Bitmap加載優(yōu)化
Android 適配(drawable文件夾)圖片適配

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容