在我們的業(yè)務場景中昏苏,需要使用客戶端采集圖片,上傳服務器威沫,然后對圖片信息進行識別贤惯。為了提升程序的性能,我們需要保證圖片上傳服務器的速度的同時棒掠,保證用于識別圖片的質量孵构。整個優(yōu)化包括兩個方面的內容:
- 相機拍照的優(yōu)化:包括相機參數(shù)的選擇、預覽句柠、啟動速度和照片質量等浦译;
- 圖片壓縮的優(yōu)化:基于拍攝的圖片和從相冊中選擇的圖片進行壓縮,控制圖片大小和尺寸溯职。
在本文中精盅,我們主要介紹圖片壓縮優(yōu)化,后續(xù)我們會介紹如何對 Android 的相機進行封裝和優(yōu)化谜酒。本項目主要基于 Android 自帶的圖片壓縮 API 進行封裝叹俏,結合了 Luban 和 Compressor 的優(yōu)點,同時提供了用戶自定義壓縮策略的接口僻族。該項目的主要目的在于粘驰,統(tǒng)一圖片壓縮框庫的實現(xiàn),集成常用的兩種圖片壓縮算法述么,讓你以更低的成本集成圖片壓縮功能到自己的項目中蝌数。
1、圖片壓縮的基礎知識
對于一般業(yè)務場景度秘,當我們展示圖片的時候顶伞,Glide 會幫我們處理加載的圖片的尺寸問題。但在把采集來的圖片上傳到服務器之前,為了節(jié)省流量唆貌,我們需要對圖片進行壓縮滑潘。
在 Android 平臺上,默認提供的壓縮有三種方式:質量壓縮和兩種尺寸壓縮锨咙,鄰近采樣以及雙線性采樣语卤。下面我們簡單介紹下者三種壓縮方式都是如何使用的:
1.1 質量壓縮
所謂的質量壓縮就是下面的這行代碼,它是 Bitmap 的方法酪刀。當我們得到了 Bitmap 的時候粹舵,即可使用這個方法來實現(xiàn)質量壓縮。它一般位于我們所有壓縮方法的最后一步蓖宦。
// android.graphics齐婴。Bitmap
compress(CompressFormat format, int quality, OutputStream stream)
該方法接受三個參數(shù)油猫,其含義分別如下:
- format:枚舉稠茂,有三個選項
JPEG
,PNG
和WEBP
,表示圖片的格式情妖; - quality:圖片的質量睬关,取值在
[0,100]
之間,表示圖片質量毡证,越大电爹,圖片的質量越高; - stream:一個輸出流料睛,通常是我們壓縮結果輸出的文件的流
1.2 鄰近采樣
鄰近采樣基于臨近點插值算法丐箩,用像素代替周圍的像素。鄰近采樣的核心代碼只有下面三行恤煞,
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 1;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red, options);
鄰近采樣核心的地方在于 inSampleSize
的計算屎勘。它通常是我們使用的壓縮算法的第一步。我們可以通過設置 inSampleSize 來得到原始圖片采樣之后的結果居扒,而不是將原始的圖片全部加載到內存中概漱,以防止 OOM。標準使用姿勢如下:
// 獲取原始圖片的尺寸
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
options.inSampleSize = 1;
BitmapFactory.decodeStream(srcImg.open(), null, options);
this.srcWidth = options.outWidth;
this.srcHeight = options.outHeight;
// 進行圖片加載喜喂,此時會將圖片加載到內存中
options.inJustDecodeBounds = false;
options.inSampleSize = calInSampleSize();
Bitmap bitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);
這里主要分成兩個步驟瓤摧,它們各自的含義是:
- 先通過設置 Options 的
inJustDecodeBounds
為 true,來加載圖片玉吁,以得到圖片的尺寸信息照弥。此時圖片不會被加載到內存中,所以不會造成 OOM进副,同時我們可以通過 Options 得到原圖的尺寸信息这揣。 - 根據(jù)上一步中得到的圖片的尺寸信息,計算一個 inSampleSize,然后將 inJustDecodeBounds 設置為 false曾沈,以加載采樣之后的圖片到內存中这嚣。
關于 inSampleSize 需要簡單說明一下:inSampleSize 代表壓縮后的圖像一個像素點代表了原來的幾個像素點,例如 inSampleSize 為 4塞俱,則壓縮后的圖像的寬高是原來的 1/4姐帚,像素點數(shù)是原來的 1/16,inSampleSize 一般會選擇 2 的指數(shù)障涯,如果不是 2 的指數(shù)罐旗,內部計算的時候也會向 2 的指數(shù)靠近。所以唯蝶,實際使用過程中九秀,我們會通過明確指定 inSampleSize 為 2 的指數(shù),來避免內部計算導致的不確定性粘我。
1.3 雙線性采樣
鄰近采樣可以對圖片的尺寸進行有效的控制鼓蜒,但是它存在幾個問題。比如征字,當我需要把圖片的寬度壓縮到 1200 左右的時候都弹,如果原始的圖片的寬度壓是 3200,那么我只能通過設置 inSampleSize 將采樣率設置為 2 來將其壓縮到 1600. 此時圖片的尺寸比我們的要求要大匙姜。就是說畅厢,鄰近采樣無法對圖片的尺寸進行更加精準的控制。如果需要對圖片尺寸進行更加精準的控制氮昧,那么就需要使用雙線性壓縮了框杜。
雙線性采樣采用雙線性插值算法,相比鄰近采樣簡單粗暴的選擇一個像素點代替其他像素點袖肥,雙線性采樣參考源像素相應位置周圍 2x2 個點的值咪辱,根據(jù)相對位置取對應的權重,經(jīng)過計算得到目標圖像昭伸。
它在 Android 中的使用也比較簡單梧乘,
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red);
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
Bitmap sclaedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2, matrix, true);
也就是對得到的 Bitmap 應用 createBitmap()
進行處理,并傳入 Matrix 指定圖片尺寸放縮的比例庐杨。該方法返回的 Bitmap 就是雙線性壓縮之后的結果选调。
1.4 圖片壓縮算法總結
在實際使用過程中,我們通常會結合三種壓縮方式使用灵份,一般使用的步驟如下仁堪,
- 使用鄰近采樣對原始的圖片進行采樣,將圖片控制到比目標尺寸稍大的大小填渠,防止 OOM弦聂;
- 使用雙線性采樣對圖片的尺寸進行壓縮鸟辅,控制圖片的尺寸為目標的大小莺葫;
- 對上述兩個步驟之后得到的圖片 Bitmap 進行質量壓縮匪凉,并將其輸出到磁盤上。
當然捺檬,本質上 Android 圖片的編碼是由 Skia 庫來完成的再层,所以,除了使用 Android 自帶的庫進行壓縮堡纬,我們還可以調用外部的庫進行壓縮聂受。為了追求更高的壓縮效率,通常我們會在 Native 層對圖片進行處理烤镐,這將涉及 JNI 的知識蛋济。筆者曾在之前的文章 《在 Android 中使用 JNI 的總結》 中介紹過 Android 平臺上 JNI 的調用的常規(guī)思路,感興趣的同學可以參考下炮叶。
2碗旅、Github 上的開源的圖片壓縮庫
現(xiàn)在 Github 上的圖片壓縮框架主要有 Luban 和 Compressor 兩個。Star 的數(shù)量也比較高悴灵,一個 9K扛芽,另一個 4K. 但是,這兩個圖片壓縮的庫有各自的優(yōu)點和缺點积瞒。下面我們通過一個表格總結一下:
框架 | 優(yōu)點 | 缺點 |
---|---|---|
Luban | 據(jù)說是根據(jù)微信圖片壓縮逆推的算法 | 1.只適用于一般的圖片展示的場景,無法對圖片的尺寸進行精準壓縮登下;2.內部封裝 AsyncTaks 來進行異步的圖片壓縮茫孔,對于 RxJava 支持不好。 |
Compressor | 1.可以對圖片的尺寸進行壓縮被芳;2.支持 RxJava缰贝。 | 1.尺寸壓縮的場景有限,如果有特別的需求畔濒,則需要手動修改源代碼剩晴;2.圖片壓縮采樣的時候計算有問題,導致采樣后的圖片尺寸總是小于我們指定的尺寸 |
上面的圖表已經(jīng)總結得很詳細了侵状。所以赞弥,根據(jù)上面的兩個庫各自的優(yōu)缺點,我們打算開發(fā)一個新的圖片壓縮框架趣兄。它滿足下面的功能:
- 支持 RxJava:我們可以像使用 Compressor 的時候那樣绽左,指定圖片壓縮的線程和結果監(jiān)聽的線程;
- 支持 Luban 壓縮算法:Luban 壓縮算法核心的部分只在于 inSampleSize 的計算艇潭,因此拼窥,我們可以很容易得將其集成到我們的新的庫中戏蔑。之所以加入 Luban,是為了讓我們的庫可以適用于一般圖片展示的場景鲁纠。用戶無需指定圖片的尺寸总棵,用起來省心省力。
- 支持 Compressor 壓縮算法同時指定更多的參數(shù):Compressor 壓縮算法就是我們上述提到的三種壓縮算法的總和改含。不過彻舰,當要壓縮的寬高比與原始圖片的寬高比不一致的時候,它只提供了一種情景候味。下文中介紹我們框架的時候會說明進行更詳細的說明刃唤。當然,你可以在調用框架的方法之前主動去計算出一個寬高比白群,但是你需要把圖片壓縮的第一個階段主動走一遍尚胞,費心費力。
- 提供用戶自定義壓縮算法的接口:我們希望設計的庫可以允許用戶自定義壓縮策略帜慢。在想要替換圖片壓縮算法的時候笼裳,通過鏈式調用的一個方法直接更換策略即可。即粱玲,我們希望能夠讓用戶以最低的成本替換項目中的圖片壓縮算法躬柬。
3、項目整體架構
以下是我們的圖片壓縮框架的整體架構抽减,這里我們只列舉除了其中核心的部分代碼允青。這里的 Compress 是我們的鏈式調用的起點,我們可以用它來指定圖片壓縮的基本參數(shù)卵沉。然后颠锉,當我們使用它的 strategy()
方法之后,方法將進入到圖片壓縮策略中史汗,此時琼掠,我們繼續(xù)鏈式調用壓縮策略的自定義方法,個性化地設置各壓縮策略自己的參數(shù):
這里的所有的壓縮策略都繼承自抽線的基類 AbstractStrategy停撞,它提供了兩個默認的實現(xiàn) Luban 和 Compressor. 接口 CompressListener 和 CacheNameFactory 分別用來監(jiān)聽圖片壓縮進度和自定義壓縮的圖片的名稱瓷蛙。下面的三個是圖片相關的工具類,用戶可以調用它們來實現(xiàn)自己壓縮策略戈毒。
4艰猬、使用
首先,在項目的 Gradle 中加入我的 Maven 倉庫的地址:
maven { url "https://dl.bintray.com/easymark/Android" }
然后副硅,在你的項目的依賴中姥宝,添加該庫的依賴:
implementation 'me.shouheng.compressor:compressor:0.0.1'
然后,就可以在項目中使用了恐疲。你可以參考 Sample 項目的使用方式腊满。不過套么,下面我們還是對它的一些 API 做簡單的說明。
4.1 Luban 的使用
下面是 Luban 壓縮策略的使用示例碳蛋,它與 Luban 庫的使用類似胚泌。只是在 Luban 的庫的基礎上,我們增加了一個 copy 的選項肃弟,用來表示當圖片因為小于指定的大小而沒有被壓縮之后玷室,是否將原始的圖片拷貝到指定的目錄。因為笤受,比如當你使用回調獲取圖片壓縮結果的時候穷缤,如果按照 Luban 庫的邏輯,你得到的是原始的圖片箩兽,所以津肛,此時你需要額外進行判斷。因此汗贫,我們增加了這個布爾類型的參數(shù)身坐,你可以通過它指定將原始文件進行拷貝,這樣你就不需要在回調中對是否是原始圖片進行判斷了落包。
// 在 Compress 的 with() 方法中指定 Context 和 要壓縮文件 File
val luban = Compress.with(this, file)
// 這里添加一個回調部蛇,如果你不使用 RxJava,那么可以用它來處理壓縮的結果
.setCompressListener(object : CompressListener{
override fun onStart() {
LogUtils.d(Thread.currentThread().toString())
Toast.makeText(this@MainActivity, "Compress Start", Toast.LENGTH_SHORT).show()
}
override fun onSuccess(result: File?) {
LogUtils.d(Thread.currentThread().toString())
displayResult(result?.absolutePath)
Toast.makeText(this@MainActivity, "Compress Success : $result", Toast.LENGTH_SHORT).show()
}
override fun onError(throwable: Throwable?) {
LogUtils.d(Thread.currentThread().toString())
Toast.makeText(this@MainActivity, "Compress Error :$throwable", Toast.LENGTH_SHORT).show()
}
})
// 壓縮圖片的名稱工廠方法咐蝇,用來指定壓縮結果的文件名
.setCacheNameFactory { System.currentTimeMillis().toString() }
// 圖片的質量
.setQuality(80)
// 上面基本的配置完了涯鲁,下面指定圖片的壓縮策略為 Luban
.strategy(Strategies.luban())
// 指定如果圖片小于等于 100K 就不壓縮了,這里的參數(shù) copy 表示嘹害,如果不壓縮的話要不要拷貝文件
.setIgnoreSize(100, copy)
// 按上面那樣得到了 Luban 實例之后有下面兩種方式啟動圖片壓縮
// 啟動方式 1:使用 RxJava 進行處理
val d = luban.asFlowable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { displayResult(it.absolutePath) }
// 啟動方式 2:直接啟動撮竿,此時使用內部封裝的 AsyncTask 進行壓縮,壓縮結果只能在上面的回調中進行處理了
luban.launch()
4.2 Compressor 的使用
下面是 Compressor 壓縮策略的基本的使用笔呀,在調用 strategy()
方法指定壓縮策略之前,你的任務與 Luban 一致髓需。所以许师,如果你需要更換圖片壓縮算法的時候,直接使用 strategy()
方法更換策略即可僚匆,前面部分的邏輯無需改動微渠,因此,可以降低你更換壓縮策略的成本咧擂。
val compressor = Compress.with(this, file)
.setQuality(60)
.setTargetDir("")
.setCompressListener(object : CompressListener {
override fun onStart() {
LogUtils.d(Thread.currentThread().toString())
Toast.makeText(this@MainActivity, "Compress Start", Toast.LENGTH_SHORT).show()
}
override fun onSuccess(result: File?) {
LogUtils.d(Thread.currentThread().toString())
displayResult(result?.absolutePath)
Toast.makeText(this@MainActivity, "Compress Success : $result", Toast.LENGTH_SHORT).show()
}
override fun onError(throwable: Throwable?) {
LogUtils.d(Thread.currentThread().toString())
Toast.makeText(this@MainActivity, "Compress Error :$throwable", Toast.LENGTH_SHORT).show()
}
})
.strategy(Strategies.compressor())
.setMaxHeight(100f)
.setMaxWidth(100f)
.setScaleMode(Configuration.SCALE_SMALLER)
.launch()
這里的 setMaxHeight(100f)
和 setMaxWidth(100f)
用來表示圖片壓縮的目標大小逞盆。具體的大小是如何計算的呢?在 Compressor 庫中你是無法確定的松申,但是在我們的庫中云芦,你可以通過 setScaleMode()
方法來指定俯逾。這個方法接收一個整數(shù)類型的枚舉,它的取值范圍有 4 個舅逸,即 SCALE_LARGER
, SCALE_SMALLER
, SCALE_WIDTH
和 SCALE_HEIGHT
桌肴,它們具體的含義我們會進行詳細說明。這里我們默認的壓縮方式是 SCALE_LARGER琉历,也就是 Compressor 庫的壓縮方式坠七。那么這四個參數(shù)分別是什么含義呢?
這里我們以一個例子來說明旗笔,假設有一個圖片的寬度是 1000彪置,高度是 500,簡寫作 (W:1000, H:500)蝇恶,通過 setMaxHeight()
和 setMaxWidth()
指定的參數(shù)均為 100拳魁,那么,就稱目標圖片的尺寸艘包,寬度是 100的猛,高度是 100,簡寫作 (W:100, H:100)想虎。那么按照上面的四種壓縮方式卦尊,最終的結果將是:
- SCALE_LARGER:對高度和長度中較大的一個進行壓縮,另一個自適應舌厨,因此壓縮結果是 (W:100, H:50). 也就是說岂却,因為原始圖片寬高比 2:1,我們需要保持這個寬高比之后再壓縮裙椭。而目標寬高比是 1:1. 而原圖的寬度比較大躏哩,所以,我們選擇將寬度作為壓縮的基準揉燃,寬度縮小 10 倍扫尺,高度也縮小 10 倍。這是 Compressor 庫的默認壓縮策略炊汤,顯然它只是優(yōu)先使得到的圖片更小正驻。這在一般情景中沒有問題,但是當你想把短邊控制在 100 就無計可施了(需要計算之后再傳參)抢腐,此時可以使用 SCALE_SMALLER姑曙。
- SCALE_SMALLER:對高度和長度中較大的一個進行壓縮,另一個自適應迈倍,因此壓縮結果是 (W:200, H:100). 也就是伤靠,高度縮小 5 倍之后,達到目標 100啼染,然后寬度縮小 5 倍宴合,達到 200.
- SCALE_WIDTH:對寬度進行壓縮焕梅,高度自適應。因此得到的結果與 SCALE_LARGER 一致形纺。
- SCALE_HEIGHT:對高度進行壓縮丘侠,寬度自適應,因此得到的結果與 SCALE_HEIGHT 一致逐样。
4.3 自定義策略
自定義一個圖片壓縮策略也是很簡單的蜗字,你可以通過繼承 SimpleStrategy 或者直接繼承 AbstractStrategy 來實現(xiàn):
class MySimpleStrategy: SimpleStrategy() {
override fun calInSampleSize(): Int {
return 2
}
fun myLogic(): MySimpleStrategy {
return this
}
}
注意下,如果想要實現(xiàn)鏈式的調用脂新,自定義壓縮策略的方法需要返回自身挪捕。
5、最后
因為我們的項目中争便,需要把圖片的短邊控制到 1200级零,長變只適應,只通過改變 Luban 來改變采樣率只能把邊長控制到一個范圍中滞乙,無法精準壓縮奏纪。所以,我們想到了 Compressor斩启,并提出了 SCALE_SMALLER 的壓縮模式. 但是 Luban 也不是用不到序调,一般用來展示的圖片的壓縮,它用起來更加方便兔簇。因此发绢,我們在庫中綜合了兩個框架,其實代碼量并不大垄琐。當然边酒,為了讓我們的庫功能更加豐富,因此我們提出了自定義壓縮策略的接口狸窘,也是用來降低壓縮策略的更換成本吧墩朦。
最后項目開源在 Github,地址是:https://github.com/Shouheng88/Compressor. 歡迎 Star 和 Fork翻擒,為該項目貢獻代碼或者提出 issue :)
后續(xù)介杆,筆者會對 Android 端的相機優(yōu)化和 JNI 操作 OpenCV 進行圖片處理進行講解,感興趣的關注作者呦 :)
獲取更多技術文章可以直接關注我的公眾號「Hello 開發(fā)者」韭寸,另外感興趣的可以加入技術 QQ 交流群:1018235573.
以上,感謝閱讀~