本文已授權(quán)微信公眾號 : code小生
(codexiaosheng) 在微信公眾平臺原創(chuàng)首發(fā)
前言
在平時的 Android 開發(fā)中嘉赎,與 Bitmap 打交道可以說是再常見不過的事了。我在寫這篇文章之前吐辙,對于 Bitmap 相關(guān)的一些東西總是模模糊糊晴弃,比如 Bitmap 的文件大小還有占用內(nèi)存大小的區(qū)別,還有對 Bitmap 壓縮的幾種方法各自的區(qū)別和用途是什么,等等
在這篇文章中,我將會把在 Bitmap 中相關(guān)的知識點都一一介紹,如果你也是對 Bitmap 總是感覺模模糊糊的話娄涩, 相信你看完這篇文章后一定會有所收獲
目錄
一、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)建一個 Bitamap 對象呢?Google 給我們提供了兩種方式:
-
Bitmap 的靜態(tài)方法 createBitmap(XX)
-
-
BitmapFactory 的 decodeXX 系列靜態(tài)方法
-
二躬存、Bitmap 的顏色配置信息與壓縮方式信息
Bitmap 中有兩個內(nèi)部枚舉類:Config 和 CompressFormat张惹,Config 是用來設(shè)置顏色配置信息的,CompressFormat 是用來設(shè)置壓縮方式的
Config
Config 類描述了一個 Bitmap 是如何存儲像素信息的岭洲,它影響了圖片的質(zhì)量(顏色深度)以及顯示透明/不透明顏色的能力
顏色格式 | 描述 | 每個像素占用內(nèi)存大小 |
---|---|---|
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默認(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)于圖片的顏色格式绍坝,有幾點需要注意:
- Bitmap 默認(rèn)的圖片格式是 ARGB_8888
- 圖片占用內(nèi)存的大小與圖片的顏色格式相關(guān)徘意, 占用內(nèi)存的大小 = 圖片的寬度 × 圖片的高度 × 每個像素占用的內(nèi)存大小
- 當(dāng)我們需要做性能優(yōu)化或者防止 OOM 的時候,可以將 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個值:
壓縮方式 | 描述 |
---|---|
Bitmap.CompressFormat.JPEG | 表示以JPEG壓縮算法進行圖像壓縮勤讽,壓縮后的格式可以是".jpg"或者".jpeg",是一種有損壓縮 |
Bitmap.CompressFormat.PNG | 表示以PNG壓縮算法進行圖像壓縮拗踢,壓縮后的格式可以是".png"脚牍,是一種無損壓縮 |
Bitmap.CompressFormat.WEBP | 表示以WebP壓縮算法進行圖像壓縮,壓縮后的格式可以是".webp"巢墅,是一種有損壓縮诸狭,質(zhì)量相同的情況下,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 的保存和轉(zhuǎn)換
前面介紹了如何創(chuàng)建一個 Bitmap蓄髓,當(dāng)我們拿到一個 Bitmap 對象后叉庐,通常還有有以下操作:
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 的文件大小
說到 Bitmap 大小這一塊的時候会喝,我們一定要先搞清楚幾個概念:
Bitmap 原始的文件大小
把一個 Bitamp 通過壓縮保存到本地的文件大小
Bitmap 加載到內(nèi)存中占用的內(nèi)存大小
注意:通常情況下眨唬,這三個值不相等!
我們以一張 寬高為 1080 * 1920 好乐,圖片原始大小為 705 kb 的圖片為例(本文均以此圖片為例)匾竿,逐個解釋和驗證這三個數(shù)據(jù):
1. Bitmap 原始的文件大小
這個很好理解,就是圖片的自身的大小嘛蔚万,沒有經(jīng)過任何處理岭妖,通過下圖我們可以看到,這張圖片的原始大小是 705 kb:
注意反璃,如果我們直接在 Android Studio 中打開這張圖片的話昵慌,上面顯示的圖片大小是 721.96 kB,而在 Windows 中屬性顯示的是705 KB淮蜈,這兩者為什么不同呢斋攀?
如果仔細觀察的話,會發(fā)現(xiàn)梧田,這兩個數(shù)值的單位不一樣淳蔼!一個是 kB侧蘸,一個是 KB。
kB(Kilobyte)鹉梨,是“千字節(jié)”(" kilobyte")的一種廣泛運用的縮寫讳癌,其意義是1000字節(jié)。根據(jù)國際單位制標(biāo)準(zhǔn)存皂,1kB = 1000B(字節(jié), Byte)
由于計算機學(xué)家長期使用二進制系統(tǒng)晌坤,一個千字節(jié)是基于2的冪次的,事實上一千字節(jié)是2或者說是1024個字節(jié)旦袋。
千字節(jié)也常指1024 (2^10)字節(jié)骤菠,因為1000約等于1024。Microsoft Windows 系統(tǒng)中仍在大量使用公制前綴的二進制寫法(即 1千字節(jié) = 1024 B)
所以上面兩張圖片的大小顯示不一致的情況疤孕。
下面我們通過代碼驗證一下:
- 把圖片放到工程的 assets 目錄下
- 通過下面代碼加載圖片商乎,然后打印出圖片的大小:
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
然后我們再驗證一下保存的圖片信息:
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
再查看一下保存的圖片信息:
到這里,我們就會有疑問了蔗牡,為什么通過 Bitmap 轉(zhuǎn)換之后圖片大小就不一樣了呢颖系?
關(guān)鍵就在這一句,
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos) //壓縮圖片并將數(shù)據(jù)存儲到 ByteArrayOutputStream 中
當(dāng)我們需要把一個 Bitmap 對象保存到本地時辩越,需要先將其轉(zhuǎn)換成 byte 數(shù)組嘁扼,這個過程是通過 Bitmap 的 compress 方法完成的。
這個方法中第一個參數(shù)代表保存的圖片類型黔攒,第二個參數(shù)代表圖片的質(zhì)量趁啸。這個值的范圍是 0~100,數(shù)值越大圖片質(zhì)量越高督惰,同時保存后的圖片大小也越大不傅。
也就是說,當(dāng)通過這個方法把一張 Bitmap 保存到本地時赏胚,第二參數(shù)控制了保存的圖片質(zhì)量访娶,同時也就影響了保存圖片的大小
3. Bitmap 加載到內(nèi)存中占用的內(nèi)存大小
請看第五部分
小結(jié)
Bitmap 原始的文件大小 、Bitamp 壓縮保存到本地的大小觉阅、Bitmap 加載到內(nèi)存中占用的內(nèi)存大小崖疤,這三者是三個不同的概念秘车,且通常這三者并不相等
將 Bitmap 保存到本地時,可以通過 compress 方法的第二個參數(shù)控制圖片的質(zhì)量戳晌,從而達到控制圖片大小的目的鲫尊。(用于圖片壓縮痴柔,后面會介紹)
五沦偎、Bitmap 占用內(nèi)存的大小
終于到了大家最最關(guān)心的點,Bitmap 占用內(nèi)存的大锌任怠豪嚎!很多時候,我們只是朦朦朧朧的知道谈火,加載大的圖片要注意侈询,防止OOM。
但是糯耍,加載一張圖片到底占用多少內(nèi)存呢扔字?
如何計算加載一張圖片到底占用多少內(nèi)存
來人,上公式:
總內(nèi)存 = 寬 × 高 × 色彩空間
把上面的公式再詳細描述一下就是:
總內(nèi)存 = 寬的像素數(shù) × 高的像素數(shù) × 每個像素點占用的大小
這個公式也很好理解温技,寬 × 高 即圖片總共有多少像素點革为,然后乘 每個像素點占用的大小 就得出了總內(nèi)存。
Bitmap 中直接提供了相關(guān)方法得到圖片所占用的內(nèi)存大卸媪邸:
- getAllocationByteCount() // API 19 以后使用
- getByteCount()
除了系統(tǒng)提供的方法震檩,我們也可以根據(jù)上面的公式自己計算。
接下來我們就通過系統(tǒng)提供的方法和我們自己計算來驗證一下:
1). 從 assets 目錄中加載圖片蜓堕,并計算占用的內(nèi)存大信茁病:
// 加載圖片
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("計算占用內(nèi)存大刑撞拧: ${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). 計算結(jié)果如下:
我們可以看到背伴,利用系統(tǒng)提供的 api 與 我們自己用公式計算得出的占用內(nèi)存大小是一樣的沸毁。
對于這張圖片來說,寬高為 1080 * 1920挂据,圖片的顏色格式是 ARGB_8888以清,證明每個像素占用 4 個字節(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)存 = 寬的像素數(shù) × 高的像素數(shù) × 每個像素點占用的大小
可以得出掷倔,影響占用內(nèi)存的大小因素有:
- 寬高
- 色彩空間
所以,當(dāng)我們需要對 Bitamp 加載進行優(yōu)化的時候个绍,就可以從這兩個方面進行著手:
- 減少 Bitmap 的寬高
- 使用占用更少內(nèi)存的色彩模式
除了上面兩點勒葱,還有一個因素也會影響到 Bitamp 占用的內(nèi)存大小浪汪,它就是 縮放
縮放
1. 什么是縮放
根據(jù)前面幾部分的介紹,我們知道凛虽,加載一張 1080 * 1920 的圖片死遭,然后通過 bitmap.getWidth() 和 bitmap.getHeight() 得到的也是 1080 * 1920
如果圖片的原始大小是 1080 * 1920,那邊加載出來的 Bitmap 對象也一定是 1080 * 1920 么凯旋?
答案是否定的呀潭。在加載 Bitamp 對象時可以手動設(shè)置 inSampleSize 來進行縮放。另外至非,如果是從 Drawable 目錄下加載圖片的話钠署,系統(tǒng)會默認(rèn)地根據(jù)圖片所在的 Drawable 目錄以及手機的 DPI 對加載的圖片進行縮放
2. 縮放是如何影響影響占用內(nèi)存的
當(dāng)我們對圖片進行縮放時,實際上造成的結(jié)果是圖片寬高的改變荒椭,通過上面的公式我們可以知道谐鼎,寬高改變了,占用的內(nèi)存也就改變了趣惠。
所以圖片的縮放對內(nèi)存的影響本質(zhì)上還是寬高對占用內(nèi)存的影響
3. Bitmap 中如何對圖片進行縮放
1)狸棍、 手動設(shè)置縮放參數(shù)
當(dāng)我們創(chuàng)建一個 Bitmap 對象的時候,會有一個可選的 Options 對象味悄,其中的 inSampleSize 參數(shù)可以控制縮放的比例草戈,inSampleSize 的值代表 圖片的寬度、高度分別變?yōu)樵瓉淼?1/inSampleSize
比如一張 1080 * 1920 的圖片傍菇,如果加載時設(shè)置了 inSampleSize = 2猾瘸,證明圖片的寬度變?yōu)樵瓉淼?1/2,高度也變?yōu)樵瓉淼?1/2丢习,所以得到的 Bitmap 對象的寬高是 540 * 860
根據(jù)上面的占用內(nèi)存的計算公式牵触,它占用的內(nèi)存大小就變?yōu)樵瓉淼?1/2 * 1/2 = 1/4
下面我們來驗證一下,還是那張圖片咐低,在加載的時候設(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é)果如下:
我們可以看到,圖片的寬高由原來的 1080 * 1920 變成了 540 * 960见擦,寬高分別變?yōu)樵瓉淼?1/2钉汗,占用內(nèi)存的大小由原來的 8100 變成了 2025,內(nèi)存大小變?yōu)榱嗽瓉淼?1/2 * 1/2 = 1/4
根據(jù)這個特性鲤屡,我們可以在加載大圖的時候進行縮放處理损痰,防止OOM的發(fā)生
注意,inSampleSize 的值要求必須大于1酒来,且只能是2的整數(shù)倍
2)卢未、 從 Drawable 目錄中加載圖片的自動縮放
當(dāng)我們從 assets 目錄中或者網(wǎng)絡(luò)上加載一張圖片的時候,默認(rèn)情況下得到的 Bitmap 對象的寬高是與原圖片的寬高一致的。比如前面的我們舉的例子辽社,寬高都是 1080 * 1920
如果我們從 Drawable 目錄下加載圖片的話伟墙,系統(tǒng)會根據(jù)圖片所在的目錄以及手機的DPI對圖片進行縮放
下面是手機 dpi 與 Drawable 目錄的對應(yīng)關(guān)系圖:
DPI | 分辨率 | 系統(tǒng)dpi | 基準(zhǔn)比例 | 對應(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 目錄中加載一張圖片的時候:
比如在一個中等分辨率的手機上,Android 就會選擇d rawable-mdpi 文件夾下的圖片滴铅,文件夾下有這張圖就會優(yōu)先被使用戳葵,在這種情況下,圖片是不會被縮放的
但是如果沒有在 drawable-mdpi 的文件夾下找到相應(yīng)圖片的話汉匙,
Android 系統(tǒng)會首先從更高一級的 drawable-hdpi 文件夾中查找拱烁,
如果找到圖片資源就進行縮放處理(縮小),顯示在屏幕上如果 drawable-hdpi 文件夾下也沒有的話盹兢,就依次往 drawable-xhdp i文件夾邻梆、drawable-xxhdpi 文件夾守伸、
drawable-xxxhdpi 文件夾绎秒、drawable-nodpi 文件夾中尋找如果更高密度的文件夾里都沒有找到,就往更低密度的文件夾 drawable-ldpi 文件夾下查找尼摹。如果找到圖片資源就進行縮放處理(放大)见芹,顯示在屏幕上
如果都沒找到,最終會在默認(rèn)的drawable文件夾中尋找蠢涝,如果默認(rèn)的drawable文件夾中也沒有那就會報錯啦
Drawable 縮放規(guī)則小結(jié)
如果圖片所在的文件夾 dpi 剛好是手機屏幕密度所對應(yīng)的文件夾(比如:手機 dpi 為 xxhdpi玄呛,圖片在 drawable-xxhdpi 文件夾中),
則該圖片不會被壓縮如果圖片所在目錄 dpi 低于匹配目錄和二,那么該圖片被認(rèn)為是為低密度設(shè)備需要的徘铝,現(xiàn)在要顯示在高密度設(shè)備上,圖片會被放大惯吕,寬和高惕它,以及占用的內(nèi)存都會變大
注意:如果圖片本身就比較大,而又放在了密度較低的文件夾中废登,
加載時會導(dǎo)致占用內(nèi)存變得非常大淹魄,導(dǎo)致OOM
如果圖片所在目錄 dpi 高于匹配目錄,那么該圖片被認(rèn)為是為高密度設(shè)備需要的堡距,現(xiàn)在要顯示在低密度設(shè)備上甲锡,圖片會被縮小,寬和高羽戒,以及占用的內(nèi)存都會變小
如果圖片所在目錄為 drawable-nodpi缤沦,則無論設(shè)備 dpi 為多少,保留原圖片大小易稠,不進行縮放
驗證
以我的手機為例缸废,屏幕分辨率是 1080 * 1920,DPI 是 480缩多,對應(yīng)的 Drawable 目錄是 drawable-xxhdpi(超超高密度)
- 把圖片拷貝到 drawable-xxhdpi 目錄下呆奕,然后加載圖片并顯示其信息
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.pic)
ivPic.setImageBitmap(bitmap)
showInfo(bitmap) //顯示圖片信息
根據(jù)上面的介紹的規(guī)則养晋,我們加載圖片所對應(yīng)的 Drawable 與我們的手機 DPI 相匹配,所以圖片不會進行縮放
- 把圖片放拷貝到 drawable-xxxhdpi 目錄下(高于手機DPI)梁钾,然后加載圖片并顯示其信息绳泉,此時圖片會被縮小
- 把圖片放拷貝到 drawable-xhdpi 目錄下(低于手機DPI),然后加載圖片并顯示其信息姆泻,此時圖片會被放大
注意:
在做測試的時候零酪,要保證同時只有一個 drawable 文件夾中存在需要加載的那張圖片
4. 小結(jié)
- 總內(nèi)存 = 寬的像素數(shù) × 高的像素數(shù) × 每個像素點占用的大小
- 由以上公式可以知道影響內(nèi)存占用大小的因素是 寬高和色彩空間
- 加載 一個 Bitamap 可以通過設(shè)置 inSampleSize 的值控制加載得到的圖片的大小
- 從 Drawable 目錄中加載圖片時,系統(tǒng)會根據(jù)手機 DPI 和 Drawable 目錄對圖片進行縮放
七拇勃、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()
}
- 質(zhì)量壓縮不會減少圖片的像素月腋,它是在保持像素的前提下改變圖片的位深及透明度,來達到壓縮圖片的目的
- 壓縮后圖片的長瓣赂,寬榆骚,像素都不會改變,那么 bitmap 所占內(nèi)存大小是不會變的
- 由于圖片的質(zhì)量變低了煌集,所以壓縮后圖片的大小會變小
-
質(zhì)量壓縮 png 格式這種圖片沒有作用妓肢,因為 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
}
- 采樣率壓縮其原理是縮放 bitmap 的尺寸
- 壓縮后圖片的 寬度苫纤、高度以及占用的內(nèi)存都會變小碉钠,文件大小也會變小(指壓縮后保存到本地的文件)
- 采樣率 inSampleSize 代表 寬度、高度變?yōu)樵瓉淼膸追种唬?br> 比如 inSampleSize 為 2卷拘,代表 寬度喊废、高度都變?yōu)樵瓉淼?1/2,占用的內(nèi)存就會變?yōu)樵瓉淼?1/4
- 采樣率 inSampleSize 只能為 2 的整次冪恭金,比如:2操禀、4、8横腿、16 ...
- 由于 inSampleSize 只能為 2 的整次冪颓屑,所以無法精確控制大小
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
}
}
- 放縮法壓縮使用的是通過矩陣對圖片進行縮放
- 縮放后圖片的 寬度、高度以及占用的內(nèi)存都會變小耿焊,文件大小也會變小(指壓縮后保存到本地的文件揪惦,原始文件不會改變)
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)
}
}
- 由于圖片的存儲格式改變罗侯,與 ARGB_8888 相比器腋,每個像素的占用的字節(jié)由 8 變?yōu)?4 , 所以圖片占用的內(nèi)存也為原來的一半
- 圖片的寬高不發(fā)生變化
- 如果圖片不包含透明信息的話,可以使用此方法進行壓縮
八纫塌、Bitmap 的其他操作
1. 旋轉(zhuǎn)
/**
* 旋轉(zhuǎn)
*
* 注意:如果 [degree] 不是90的倍數(shù)的話诊县,會導(dǎo)致旋轉(zhuǎn)后圖片變成"斜的",
* 然而此時計算圖片的寬高時仍然是按照水平和豎直方向計算措左,所以會導(dǎo)致最終旋轉(zhuǎn)后的圖片變大
* 如果進行多次旋轉(zhuǎn)的話依痊,最終會出現(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
}
九、 總結(jié)
- Bitmap 的顏色配置怎披,以及不同格式占用內(nèi)存的大小
- 注意區(qū)分 原始圖片大小胸嘁、Bitmap 對象的大小(寬、高)凉逛、Bitmap 占用內(nèi)存的大小性宏、將 Bitmap 保存成文件的大小
- Bitmap 占用內(nèi)存:總內(nèi)存 = 寬的像素數(shù) × 高的像素數(shù) × 每個像素點占用的大小
- Bitmap 的縮放和從 Drawable 目錄中加載圖片的規(guī)則
- Bitmap 的幾種壓縮方法和各自的特點
相關(guān)代碼
https://github.com/smashinggit/Study
注:此工程包含多個module,本文所用代碼均在 bitmap module 下
注:由于本人水平有限状飞,所以難免會有理解偏差或者使用不正確的問題毫胜。如果小伙伴們有更好的理解或者發(fā)現(xiàn)有什么問題,歡迎留言批評指正~
參考文章:
玩轉(zhuǎn)Android Bitmap
怎樣計算Bitmap的內(nèi)存占用和Bitmap加載優(yōu)化
Android 適配(drawable文件夾)圖片適配