Android Bitmap 詳解:關于 Bitamp 你所要知道的一切

Android.jpg

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

前言

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

在這篇文章中,我將會把在 Bitmap 中相關的知識點都一一介紹,如果你也是對 Bitmap 總是感覺模模糊糊的話, 相信你看完這篇文章后一定會有所收獲

目錄

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

一、Bitmap 的創(chuàng)建

我們如何創(chuàng)建一個 Bitamap 對象呢?Google 給我們提供了兩種方式:

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


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


      image.png

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

Bitmap 中有兩個內部枚舉類:Config 和 CompressFormat,Config 是用來設置顏色配置信息的,CompressFormat 是用來設置壓縮方式的

image.png

Config

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

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

關于圖片的顏色格式,有幾點需要注意:

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

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

CompressFormat

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

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

例如:

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

三、Bitmap 的保存和轉換

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

1. 將 Bitamap 轉換為 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 的文件大小

說到 Bitmap 大小這一塊的時候,我們一定要先搞清楚幾個概念:

  • Bitmap 原始的文件大小

  • 把一個 Bitamp 通過壓縮保存到本地的文件大小

  • Bitmap 加載到內存中占用的內存大小

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

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

1. Bitmap 原始的文件大小

這個很好理解,就是圖片的自身的大小嘛,沒有經過任何處理,通過下圖我們可以看到,這張圖片的原始大小是 705 kb:

image.png
image.png

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

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

下面我們通過代碼驗證一下:

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

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

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

通過上面的驗證,我們知道,這張圖片的原始大小是 705 kb。如果我們把這張圖片保存到手機上,那么它的大小還會是 705 kb 么?

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

1). 直接拿到圖片的輸入流或者說 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

然后我們再驗證一下保存的圖片信息:

image.png

2.) 創(chuàng)建一個 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ù)存儲到 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

到這里,我們就會有疑問了,為什么通過 Bitmap 轉換之后圖片大小就不一樣了呢?

關鍵就在這一句,

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

當我們需要把一個 Bitmap 對象保存到本地時,需要先將其轉換成 byte 數(shù)組,這個過程是通過 Bitmap 的 compress 方法完成的。

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

也就是說,當通過這個方法把一張 Bitmap 保存到本地時,第二參數(shù)控制了保存的圖片質量,同時也就影響了保存圖片的大小

3. Bitmap 加載到內存中占用的內存大小

請看第五部分

小結

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

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

五、Bitmap 占用內存的大小

終于到了大家最最關心的點,Bitmap 占用內存的大?。『芏鄷r候,我們只是朦朦朧朧的知道,加載大的圖片要注意,防止OOM。

但是,加載一張圖片到底占用多少內存呢?

如何計算加載一張圖片到底占用多少內存

來人,上公式:

總內存 = 寬 × 高 × 色彩空間

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

總內存 = 寬的像素數(shù) × 高的像素數(shù) × 每個像素點占用的大小

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

Bitmap 中直接提供了相關方法得到圖片所占用的內存大?。?/p>

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

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

接下來我們就通過系統(tǒng)提供的方法和我們自己計算來驗證一下:

1). 從 assets 目錄中加載圖片,并計算占用的內存大小:

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

log("占用內存大?。?${Bitmaps.getMemorySize(rawBitmap)} kb \n")
log("計算占用內存大?。?${Bitmaps.calculateMemorySize(rawBitmap)} kb \n")


// 使用系統(tǒng) api 提供的方法計算
// 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ù)公式手動計算
// 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 是我自己定義的一個工具類,并不是系統(tǒng)的一個類。源碼在文章最下面

2). 計算結果如下:


image.png

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

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

總內存 = 寬 * 高 * 色彩空間 = 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 占用內存大小的因素

根據(jù)公式:

總內存 = 寬的像素數(shù) × 高的像素數(shù) × 每個像素點占用的大小

可以得出,影響占用內存的大小因素有:

  • 寬高
  • 色彩空間

所以,當我們需要對 Bitamp 加載進行優(yōu)化的時候,就可以從這兩個方面進行著手:

  • 減少 Bitmap 的寬高
  • 使用占用更少內存的色彩模式

除了上面兩點,還有一個因素也會影響到 Bitamp 占用的內存大小,它就是 縮放

縮放

1. 什么是縮放

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

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

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

2. 縮放是如何影響影響占用內存的

當我們對圖片進行縮放時,實際上造成的結果是圖片寬高的改變,通過上面的公式我們可以知道,寬高改變了,占用的內存也就改變了。

所以圖片的縮放對內存的影響本質上還是寬高對占用內存的影響

3. Bitmap 中如何對圖片進行縮放

1)、 手動設置縮放參數(shù)

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

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

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

下面我們來驗證一下,還是那張圖片,在加載的時候設置 inSampleSize = 2 ,然后看一下圖片的寬高和占用內存的情況:

 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)

結果如下:


image.png

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

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

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

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

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

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

下面是手機 dpi 與 Drawable 目錄的對應關系圖:

DPI 分辨率 系統(tǒng)dpi 基準比例 對應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 目錄的選擇流程

當我們從 Drawable 目錄中加載一張圖片的時候:

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

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

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

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

  5. 如果都沒找到,最終會在默認的drawable文件夾中尋找,如果默認的drawable文件夾中也沒有那就會報錯啦

Drawable 縮放規(guī)則小結

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

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

注意:如果圖片本身就比較大,而又放在了密度較低的文件夾中,
加載時會導致占用內存變得非常大,導致OOM

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

  • 如果圖片所在目錄為 drawable-nodpi,則無論設備 dpi 為多少,保留原圖片大小,不進行縮放

驗證

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

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

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

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

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

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

4. 小結

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

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

1. 質量壓縮

    /**
     * 將圖片 [bitmap] 壓縮到指定大小 [targetSize] 以內 ,單位是 kb
     * 這里的大小指的是 “文件大小”,而不是 “內存大小”
     **/
   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. 質量壓縮不會減少圖片的像素,它是在保持像素的前提下改變圖片的位深及透明度,來達到壓縮圖片的目的
  2. 壓縮后圖片的長,寬,像素都不會改變,那么 bitmap 所占內存大小是不會變的
  3. 由于圖片的質量變低了,所以壓縮后圖片的大小會變小
  4. 質量壓縮 png 格式這種圖片沒有作用,因為 png 是無損壓縮


    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. 壓縮后圖片的 寬度、高度以及占用的內存都會變小,文件大小也會變小(指壓縮后保存到本地的文件)
  3. 采樣率 inSampleSize 代表 寬度、高度變?yōu)樵瓉淼膸追种唬?br> 比如 inSampleSize 為 2,代表 寬度、高度都變?yōu)樵瓉淼?1/2,占用的內存就會變?yōu)樵瓉淼?1/4
  4. 采樣率 inSampleSize 只能為 2 的整次冪,比如:2、4、8、16 ...
  5. 由于 inSampleSize 只能為 2 的整次冪,所以無法精確控制大小
image.png

3. 縮放壓縮

    /**
     * 將圖片 [bitmap] 壓縮到指定寬高范圍內
    **/
    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. 放縮法壓縮使用的是通過矩陣對圖片進行縮放
  2. 縮放后圖片的 寬度、高度以及占用的內存都會變小,文件大小也會變小(指壓縮后保存到本地的文件,原始文件不會改變)
image.png

4. 色彩模式壓縮(RGB565)

    /**
     * 將圖片格式更改為 Bitmap.Config.RGB_565,減少圖片占用的內存大小
    **/
    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. 由于圖片的存儲格式改變,與 ARGB_8888 相比,每個像素的占用的字節(jié)由 8 變?yōu)?4 , 所以圖片占用的內存也為原來的一半
  2. 圖片的寬高不發(fā)生變化
  3. 如果圖片不包含透明信息的話,可以使用此方法進行壓縮
image.png

八、Bitmap 的其他操作

1. 旋轉

    /**
     * 旋轉
     *
     * 注意:如果 [degree] 不是90的倍數(shù)的話,會導致旋轉后圖片變成"斜的",
     * 然而此時計算圖片的寬高時仍然是按照水平和豎直方向計算,所以會導致最終旋轉后的圖片變大
     * 如果進行多次旋轉的話,最終會出現(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. 裁切

  /**
     * 從圖片中間位置裁剪出一個寬高為的 [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)
        }
    }

    /**
     * 從圖片中間位置裁剪出一個半徑為 [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
    }

九、 總結

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

相關代碼

https://github.com/smashinggit/Study

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

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

參考文章:

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

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

友情鏈接更多精彩內容