compressor 是一個(gè) Android 平臺(tái)上的開(kāi)源圖片壓縮庫(kù),使用它,可以方便的對(duì)本地圖片進(jìn)行壓縮,與此同時(shí),該庫(kù)還提供了各種壓縮參數(shù)的設(shè)置選項(xiàng)。
使用
val compressedImageFile = Compressor.compress(context, actualImageFile) {
resolution(1280, 720)
quality(80)
format(Bitmap.CompressFormat.WEBP)
size(2_097_152) // 2 MB
}
輸入:
- 一個(gè)圖片文件
- 壓縮質(zhì)量,以及格式化類型、最大壓縮質(zhì)量。
輸出:
- 壓縮后的圖片文件(該文件默認(rèn)存儲(chǔ)應(yīng)用的沙盒目錄下)
核心
下面分析一下這個(gè)倉(cāng)庫(kù)的核心點(diǎn)。
壓縮實(shí)現(xiàn)
該庫(kù)的功能為壓縮圖片,具體壓縮是通過(guò) Bitmap 自身提供的 compress 方法進(jìn)行壓縮。
bitmap.compress(format, quality, fileOutputStream)
壓縮參數(shù)組合
壓縮操作是通過(guò)單例類 Compressor
的 compress
方法入口來(lái)完成,具體需要先指定目標(biāo)壓縮文件,然后指定壓縮參數(shù)。
而壓縮參數(shù)控制是通過(guò)第四個(gè)參數(shù) compressionPatch
控制,它有一個(gè)默認(rèn)的實(shí)現(xiàn) DefaultConstraint.default()
,所以如果不指定其他設(shè)置,默認(rèn)設(shè)置就會(huì)生效。
此外,當(dāng)設(shè)置了自定義的壓縮參數(shù)設(shè)置,這些參數(shù)設(shè)置項(xiàng)都會(huì)保存在 Compression
的 constraints
集合中,這是一個(gè)圖片壓縮參數(shù)的抽象接口集合,然后遍歷參數(shù)集合,并調(diào)用不同的壓縮參數(shù)實(shí)現(xiàn),如下所示,這里是一種鏈?zhǔn)秸{(diào)用效果:
compression.constraints.forEach { constraint ->
//該策略是否滿足條件
while (constraint.isSatisfied(result).not()) {
//如果不滿足,就進(jìn)行處理
result = constraint.satisfy(result)
}
}
這樣每一個(gè)壓縮參數(shù)的實(shí)現(xiàn)結(jié)果,都會(huì)作為接下來(lái)壓縮參數(shù)的輸入,從而達(dá)到鏈?zhǔn)秸{(diào)用的效果,一步一步,讓所有的參數(shù)設(shè)置在一個(gè)圖片源文件上生效。
壓縮參數(shù)接口
Constraint
接口是該庫(kù)的核心,也是一個(gè)很巧妙的設(shè)計(jì)。
通常來(lái)講,對(duì)于圖片壓縮,我們可以按照面向過(guò)程的思想,只需要定義一個(gè)方法,然后在方法中對(duì)圖片壓縮質(zhì)量、壓縮格式、輸出位置等按個(gè)進(jìn)行處理,最終進(jìn)行壓縮,這樣代碼邏輯就會(huì)集中在一塊里,這樣的設(shè)計(jì)對(duì)后續(xù)代碼的維護(hù)并不好,而且不具備模塊性,整個(gè)是一個(gè)大塊,看著也不是很優(yōu)雅。
該庫(kù)通過(guò) Constraint
的接口很優(yōu)雅的解決了這個(gè)問(wèn)題。
不同的壓縮參數(shù),自己去實(shí)現(xiàn)自己的壓縮方案,這個(gè)接口提供了兩個(gè)方法:
interface Constraint {
fun isSatisfied(imageFile: File): Boolean
fun satisfy(imageFile: File): File
}
第一個(gè)方法是 isSatisfied
,它用于判斷當(dāng)前圖片文件是否已經(jīng)滿足參數(shù)設(shè)置條件,如果已經(jīng)滿足,就不執(zhí)行 satisfy
方法,否則就執(zhí)行 satisfy
方法,該方法完成具體的壓縮設(shè)置操作。
比如 FormatConstraint
的實(shí)現(xiàn),這是指定壓縮格式的實(shí)現(xiàn)類,如果當(dāng)前圖片已經(jīng)是指定的格式,就進(jìn)行處理,否則不處理。
class FormatConstraint(private val format: Bitmap.CompressFormat) : Constraint {
override fun isSatisfied(imageFile: File): Boolean {
return format == imageFile.compressFormat()
}
override fun satisfy(imageFile: File): File {
return overWrite(imageFile, loadBitmap(imageFile), format)
}
}
這里當(dāng)檢測(cè)到當(dāng)前圖片的格式不是指定的格式,就會(huì)執(zhí)行 satisfy 方法,satisfy 方法中執(zhí)行具體的壓縮,縱觀其他幾個(gè)參數(shù)策略的實(shí)現(xiàn),它們大都是通過(guò) overWriter 去進(jìn)行具體的圖片參數(shù)設(shè)置。
overWrite 的實(shí)現(xiàn)
- 檢查圖片格式是否跟指定格式一致,否則更改圖片名稱后綴
- 刪除臨時(shí)的本地圖片文件
- 使用新參數(shù)對(duì) Bitmap 進(jìn)行壓縮、處理,并保存到新的臨時(shí)文件并返回
代碼如下所示:
fun overWrite(imageFile: File, bitmap: Bitmap, format: Bitmap.CompressFormat = imageFile.compressFormat(), quality: Int = 100): File {
val result = if (format == imageFile.compressFormat()) {
imageFile
} else {
File("${imageFile.absolutePath.substringBeforeLast(".")}.${format.extension()}")
}
imageFile.delete()
saveBitmap(bitmap, result, format, quality)
return result
}
saveBitmap
方法具體就是調(diào)用 Bitmap 的 compress 方法進(jìn)行壓縮。
拆分
- Constraint 壓縮參數(shù)設(shè)置的抽象接口,每一種壓縮策略必須實(shí)現(xiàn)該接口
- Compressor 壓縮門(mén)面類,入口類,只提供一個(gè) 方法,用于讓調(diào)用者設(shè)置不同的壓縮選項(xiàng),并啟動(dòng)壓縮。
- Compression 一個(gè)用于盛放不同 Constraint 的集合
- 不同壓縮策略的實(shí)現(xiàn)類
- DefaultConstraint 默認(rèn)壓縮參數(shù)的實(shí)現(xiàn)
- DestinationConstraint 指定壓縮文件輸出的文件位置
- FormatConstraint 指定文件最終輸出的壓縮格式
- QualityConstraint 指定壓縮質(zhì)量
- ResolutionConstraint 指定圖片寬高值
- SizeConstraint 指定圖片最終的壓縮大小
細(xì)節(jié)
- 壓縮質(zhì)量設(shè)置對(duì) PNG圖片無(wú)效。
這是由于 Bitmap 自身的壓縮限制,它提供的 compress 方法,即使設(shè)置了壓縮質(zhì)量,但是對(duì) PNG 格式無(wú)效。
from Bitmap#compress 參數(shù)介紹
- 如何實(shí)現(xiàn)指定大小的壓縮 #SizeConstraint
設(shè)置文件最大質(zhì)量,如果當(dāng)前文件大小大于最大質(zhì)量,則繼續(xù)進(jìn)行壓縮,具體通過(guò)設(shè)置圖片采樣率進(jìn)行壓縮,并設(shè)置最低采樣率為10,另外設(shè)置了壓縮次數(shù),如果超過(guò)了指定的壓縮次數(shù),還沒(méi)有達(dá)到大小,則不再壓縮,技即使圖片質(zhì)量還沒(méi)有達(dá)到目標(biāo)值。
不足
從上面 overWrite 方法的實(shí)現(xiàn)可以看到,每一次壓縮參數(shù)的生效,都會(huì)伴隨上一個(gè)緩存文件的刪除,以及下一個(gè)臨時(shí)文件的生成,這樣可能導(dǎo)致壓縮會(huì)產(chǎn)生比較多的 IO 開(kāi)銷(xiāo)。
但這是一種博弈,這樣的好處,是把不同的壓縮參數(shù)實(shí)現(xiàn)拆分到不同的模塊類,讓代碼結(jié)構(gòu)更清晰,而且在我開(kāi)發(fā)咕咚云圖(一個(gè)手機(jī)圖床)的過(guò)程中,并沒(méi)有發(fā)現(xiàn) IO 開(kāi)銷(xiāo)導(dǎo)致什么問(wèn)題,所以,相比這樣設(shè)計(jì)為代碼帶來(lái)的簡(jiǎn)潔以及可維護(hù)性,這樣的 IO 開(kāi)銷(xiāo)可以忽略。
咕咚 DeepSource 2020/12/03