在我們的業(yè)務(wù)場景中,需要使用客戶端采集圖片,上傳服務(wù)器,然后對圖片信息進(jìn)行識別。為了提升程序的性能,我們需要保證圖片上傳服務(wù)器的速度的同時,保證用于識別圖片的質(zhì)量。整個優(yōu)化包括兩個方面的內(nèi)容:
- 相機(jī)拍照的優(yōu)化:包括相機(jī)參數(shù)的選擇、預(yù)覽、啟動速度和照片質(zhì)量等;
- 圖片壓縮的優(yōu)化:基于拍攝的圖片和從相冊中選擇的圖片進(jìn)行壓縮,控制圖片大小和尺寸。
在本文中,我們主要介紹圖片壓縮優(yōu)化,后續(xù)我們會介紹如何對 Android 的相機(jī)進(jìn)行封裝和優(yōu)化。本項目主要基于 Android 自帶的圖片壓縮 API 進(jìn)行封裝,結(jié)合了 Luban 和 Compressor 的優(yōu)點,同時提供了用戶自定義壓縮策略的接口。該項目的主要目的在于,統(tǒng)一圖片壓縮框庫的實現(xiàn),集成常用的兩種圖片壓縮算法,讓你以更低的成本集成圖片壓縮功能到自己的項目中。
1、圖片壓縮的基礎(chǔ)知識
對于一般業(yè)務(wù)場景,當(dāng)我們展示圖片的時候,Glide 會幫我們處理加載的圖片的尺寸問題。但在把采集來的圖片上傳到服務(wù)器之前,為了節(jié)省流量,我們需要對圖片進(jìn)行壓縮。
在 Android 平臺上,默認(rèn)提供的壓縮有三種方式:質(zhì)量壓縮和兩種尺寸壓縮,鄰近采樣以及雙線性采樣。下面我們簡單介紹下者三種壓縮方式都是如何使用的:
1.1 質(zhì)量壓縮
所謂的質(zhì)量壓縮就是下面的這行代碼,它是 Bitmap 的方法。當(dāng)我們得到了 Bitmap 的時候,即可使用這個方法來實現(xiàn)質(zhì)量壓縮。它一般位于我們所有壓縮方法的最后一步。
// android.graphics。Bitmap
compress(CompressFormat format, int quality, OutputStream stream)
該方法接受三個參數(shù),其含義分別如下:
- format:枚舉,有三個選項 JPEG, PNG 和 WEBP,表示圖片的格式;
- quality:圖片的質(zhì)量,取值在 [0,100] 之間,表示圖片質(zhì)量,越大,圖片的質(zhì)量越高;
- stream:一個輸出流,通常是我們壓縮結(jié)果輸出的文件的流
1.2 鄰近采樣
鄰近采樣基于臨近點插值算法,用像素代替周圍的像素。鄰近采樣的核心代碼只有下面三行,
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 1;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red, options);
復(fù)制代碼鄰近采樣核心的地方在于 inSampleSize 的計算。它通常是我們使用的壓縮算法的第一步。我們可以通過設(shè)置 inSampleSize 來得到原始圖片采樣之后的結(jié)果,而不是將原始的圖片全部加載到內(nèi)存中,以防止 OOM。標(biāo)準(zhǔn)使用姿勢如下:
// 獲取原始圖片的尺寸
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;
// 進(jìn)行圖片加載,此時會將圖片加載到內(nèi)存中
options.inJustDecodeBounds = false;
options.inSampleSize = calInSampleSize();
Bitmap bitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);
這里主要分成兩個步驟,它們各自的含義是:
- 先通過設(shè)置 Options 的 inJustDecodeBounds 為 true,來加載圖片,以得到圖片的尺寸信息。此時圖片不會被加載到內(nèi)存中,所以不會造成 OOM,同時我們可以通過 Options 得到原圖的尺寸信息。
- 根據(jù)上一步中得到的圖片的尺寸信息,計算一個 inSampleSize,然后將 inJustDecodeBounds 設(shè)置為 false,以加載采樣之后的圖片到內(nèi)存中。
關(guān)于 inSampleSize 需要簡單說明一下:inSampleSize 代表壓縮后的圖像一個像素點代表了原來的幾個像素點,例如 inSampleSize 為 4,則壓縮后的圖像的寬高是原來的 1/4,像素點數(shù)是原來的 1/16,inSampleSize 一般會選擇 2 的指數(shù),如果不是 2 的指數(shù),內(nèi)部計算的時候也會向 2 的指數(shù)靠近。所以,實際使用過程中,我們會通過明確指定 inSampleSize 為 2 的指數(shù),來避免內(nèi)部計算導(dǎo)致的不確定性。
1.3 雙線性采樣
鄰近采樣可以對圖片的尺寸進(jìn)行有效的控制,但是它存在幾個問題。比如,當(dāng)我需要把圖片的寬度壓縮到 1200 左右的時候,如果原始的圖片的寬度壓是 3200,那么我只能通過設(shè)置 inSampleSize 將采樣率設(shè)置為 2 來將其壓縮到 1600. 此時圖片的尺寸比我們的要求要大。就是說,鄰近采樣無法對圖片的尺寸進(jìn)行更加精準(zhǔn)的控制。如果需要對圖片尺寸進(jìn)行更加精準(zhǔn)的控制,那么就需要使用雙線性壓縮了。
雙線性采樣采用雙線性插值算法,相比鄰近采樣簡單粗暴的選擇一個像素點代替其他像素點,雙線性采樣參考源像素相應(yīng)位置周圍 2x2 個點的值,根據(jù)相對位置取對應(yīng)的權(quán)重,經(jīng)過計算得到目標(biāo)圖像。
它在 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 應(yīng)用 createBitmap() 進(jìn)行處理,并傳入 Matrix 指定圖片尺寸放縮的比例。該方法返回的 Bitmap 就是雙線性壓縮之后的結(jié)果。
1.4 圖片壓縮算法總結(jié)
在實際使用過程中,我們通常會結(jié)合三種壓縮方式使用,一般使用的步驟如下,
- 使用鄰近采樣對原始的圖片進(jìn)行采樣,將圖片控制到比目標(biāo)尺寸稍大的大小,防止 OOM;
- 使用雙線性采樣對圖片的尺寸進(jìn)行壓縮,控制圖片的尺寸為目標(biāo)的大??;
- 對上述兩個步驟之后得到的圖片 Bitmap 進(jìn)行質(zhì)量壓縮,并將其輸出到磁盤上。
當(dāng)然,本質(zhì)上 Android 圖片的編碼是由 Skia 庫來完成的,所以,除了使用 Android 自帶的庫進(jìn)行壓縮,我們還可以調(diào)用外部的庫進(jìn)行壓縮。為了追求更高的壓縮效率,通常我們會在 Native 層對圖片進(jìn)行處理,這將涉及 JNI 的知識。筆者曾在之前的文章 《在 Android 中使用 JNI 的總結(jié)》 中介紹過 Android 平臺上 JNI 的調(diào)用的常規(guī)思路,感興趣的同學(xué)可以參考下。
2、Github 上的開源的圖片壓縮庫
現(xiàn)在 Github 上的圖片壓縮框架主要有 Luban 和 Compressor 兩個。Star 的數(shù)量也比較高,一個 9K,另一個 4K. 但是,這兩個圖片壓縮的庫有各自的優(yōu)點和缺點。下面我們通過一個圖片總結(jié)一下:
上面的圖表已經(jīng)總結(jié)得很詳細(xì)了。所以,根據(jù)上面的兩個庫各自的優(yōu)缺點,我們打算開發(fā)一個新的圖片壓縮框架。它滿足下面的功能:
- 支持 RxJava:我們可以像使用 Compressor 的時候那樣,指定圖片壓縮的線程和結(jié)果監(jiān)聽的線程;
- 支持 Luban 壓縮算法:Luban 壓縮算法核心的部分只在于 inSampleSize 的計算,因此,我們可以很容易得將其集成到我們的新的庫中。之所以加入 Luban,是為了讓我們的庫可以適用于一般圖片展示的場景。用戶無需指定圖片的尺寸,用起來省心省力。
- 支持 Compressor 壓縮算法同時指定更多的參數(shù):Compressor 壓縮算法就是我們上述提到的三種壓縮算法的總和。不過,當(dāng)要壓縮的寬高比與原始圖片的寬高比不一致的時候,它只提供了一種情景。下文中介紹我們框架的時候會說明進(jìn)行更詳細(xì)的說明。當(dāng)然,你可以在調(diào)用框架的方法之前主動去計算出一個寬高比,但是你需要把圖片壓縮的第一個階段主動走一遍,費心費力。
- 提供用戶自定義壓縮算法的接口:我們希望設(shè)計的庫可以允許用戶自定義壓縮策略。在想要替換圖片壓縮算法的時候,通過鏈?zhǔn)秸{(diào)用的一個方法直接更換策略即可。即,我們希望能夠讓用戶以最低的成本替換項目中的圖片壓縮算法。
3、項目整體架構(gòu)
以下是我們的圖片壓縮框架的整體架構(gòu),這里我們只列舉除了其中核心的部分代碼。這里的 Compress 是我們的鏈?zhǔn)秸{(diào)用的起點,我們可以用它來指定圖片壓縮的基本參數(shù)。然后,當(dāng)我們使用它的 strategy() 方法之后,方法將進(jìn)入到圖片壓縮策略中,此時,我們繼續(xù)鏈?zhǔn)秸{(diào)用壓縮策略的自定義方法,個性化地設(shè)置各壓縮策略自己的參數(shù):
這里的所有的壓縮策略都繼承自抽線的基類 AbstractStrategy,它提供了兩個默認(rèn)的實現(xiàn) Luban 和 Compressor. 接口 CompressListener 和 CacheNameFactory 分別用來監(jiān)聽圖片壓縮進(jìn)度和自定義壓縮的圖片的名稱。下面的三個是圖片相關(guān)的工具類,用戶可以調(diào)用它們來實現(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 的庫的基礎(chǔ)上,我們增加了一個 copy 的選項,用來表示當(dāng)圖片因為小于指定的大小而沒有被壓縮之后,是否將原始的圖片拷貝到指定的目錄。因為,比如當(dāng)你使用回調(diào)獲取圖片壓縮結(jié)果的時候,如果按照 Luban 庫的邏輯,你得到的是原始的圖片,所以,此時你需要額外進(jìn)行判斷。因此,我們增加了這個布爾類型的參數(shù),你可以通過它指定將原始文件進(jìn)行拷貝,這樣你就不需要在回調(diào)中對是否是原始圖片進(jìn)行判斷了。
// 在 Compress 的 with() 方法中指定 Context 和 要壓縮文件 File
val luban = Compress.with(this, file)
// 這里添加一個回調(diào),如果你不使用 RxJava,那么可以用它來處理壓縮的結(jié)果
.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()
}
})
// 壓縮圖片的名稱工廠方法,用來指定壓縮結(jié)果的文件名
.setCacheNameFactory { System.currentTimeMillis().toString() }
// 圖片的質(zhì)量
.setQuality(80)
// 上面基本的配置完了,下面指定圖片的壓縮策略為 Luban
.strategy(Strategies.luban())
// 指定如果圖片小于等于 100K 就不壓縮了,這里的參數(shù) copy 表示,如果不壓縮的話要不要拷貝文件
.setIgnoreSize(100, copy)
// 按上面那樣得到了 Luban 實例之后有下面兩種方式啟動圖片壓縮
// 啟動方式 1:使用 RxJava 進(jìn)行處理
val d = luban.asFlowable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { displayResult(it.absolutePath) }
// 啟動方式 2:直接啟動,此時使用內(nèi)部封裝的 AsyncTask 進(jìn)行壓縮,壓縮結(jié)果只能在上面的回調(diào)中進(jìn)行處理了
luban.launch()
4.2 Compressor 的使用
下面是 Compressor 壓縮策略的基本的使用,在調(diào)用 strategy() 方法指定壓縮策略之前,你的任務(wù)與 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) 用來表示圖片壓縮的目標(biāo)大小。具體的大小是如何計算的呢?在 Compressor 庫中你是無法確定的,但是在我們的庫中,你可以通過 setScaleMode() 方法來指定。這個方法接收一個整數(shù)類型的枚舉,它的取值范圍有 4 個,即 SCALE_LARGER, SCALE_SMALLER, SCALE_WIDTH 和 SCALE_HEIGHT,它們具體的含義我們會進(jìn)行詳細(xì)說明。這里我們默認(rèn)的壓縮方式是 SCALE_LARGER,也就是 Compressor 庫的壓縮方式。那么這四個參數(shù)分別是什么含義呢?
這里我們以一個例子來說明,假設(shè)有一個圖片的寬度是 1000,高度是 500,簡寫作 (W:1000, H:500),通過 setMaxHeight() 和 setMaxWidth() 指定的參數(shù)均為 100,那么,就稱目標(biāo)圖片的尺寸,寬度是 100,高度是 100,簡寫作 (W:100, H:100)。那么按照上面的四種壓縮方式,最終的結(jié)果將是:
- SCALE_LARGER:對高度和長度中較大的一個進(jìn)行壓縮,另一個自適應(yīng),因此壓縮結(jié)果是 (W:100, H:50). 也就是說,因為原始圖片寬高比 2:1,我們需要保持這個寬高比之后再壓縮。而目標(biāo)寬高比是 1:1. 而原圖的寬度比較大,所以,我們選擇將寬度作為壓縮的基準(zhǔn),寬度縮小 10 倍,高度也縮小 10 倍。這是 Compressor 庫的默認(rèn)壓縮策略,顯然它只是優(yōu)先使得到的圖片更小。這在一般情景中沒有問題,但是當(dāng)你想把短邊控制在 100 就無計可施了(需要計算之后再傳參),此時可以使用 SCALE_SMALLER。
- SCALE_SMALLER:對高度和長度中較大的一個進(jìn)行壓縮,另一個自適應(yīng),因此壓縮結(jié)果是 (W:200, H:100). 也就是,高度縮小 5 倍之后,達(dá)到目標(biāo) 100,然后寬度縮小 5 倍,達(dá)到 200.
- SCALE_WIDTH:對寬度進(jìn)行壓縮,高度自適應(yīng)。因此得到的結(jié)果與 SCALE_LARGER 一致。
- SCALE_HEIGHT:對高度進(jìn)行壓縮,寬度自適應(yīng),因此得到的結(jié)果與 SCALE_HEIGHT 一致。
4.3 自定義策略
自定義一個圖片壓縮策略也是很簡單的,你可以通過繼承 SimpleStrategy 或者直接繼承 AbstractStrategy 來實現(xiàn):
class MySimpleStrategy: SimpleStrategy() {
override fun calInSampleSize(): Int {
return 2
}
fun myLogic(): MySimpleStrategy {
return this
}
}
復(fù)制代碼注意下,如果想要實現(xiàn)鏈?zhǔn)降恼{(diào)用,自定義壓縮策略的方法需要返回自身。
喜歡請點擊+關(guān)注哦
