本篇文章已授權微信公眾號 hongyangAndroid (鴻洋)獨家發布
最近基於 Android StackBlur 開源庫,根據自己碰到的需求場景,封裝了個高斯模糊組件,順便記錄一下。
為什么要自己重復造輪子?
其實也談不上重頭自己造輪子,畢竟是基於大神的開源庫,做了二次封裝。封裝的目的在於,方便外部使用。畢竟有着自己的編程習慣,大神的開源庫也只是提供了基礎的功能,現實編程中,產品的需求是各種各樣的。
導致每次使用時,都蠻麻煩的,需要額外自己處理蠻多東西。而一旦新的項目又需要接入高斯模糊了,又得重新寫一些代碼,復制粘貼也麻煩,經常由於各種業務耦合報錯。
既然如此,干脆花時間抽個基礎、公用的高斯模糊組件,需要時直接依賴即可。
基礎理論
高斯模糊
高斯模糊的原理和算法就不介紹了,我也不懂,沒深入,這里就大概講講我的粗坯理解:
我們知道,一張圖片,本質上其實是一個個像素點構成的,雖然經過計算機處理后,呈現在我們眼前的是具體的圖像。但在計算機中,其實就是一堆數組數據。
數組中每個單位就是一個個像素點,那么每個像素點是存儲什么內容呢,其實也就是 RGB 或者 ARGB 之類格式的數據。
高斯模糊,大體上就是對這張圖片中的每個像素點都重新進行計算,每個像素點的新值等於以它為中心,半徑為 r 的周圍區域內所有像素點各自按照不同權重相加之和的平均值。
可以粗坯的理解為,本來這個像素點是要呈現它自己本身的內容,但經過高斯模糊計算后,摻雜進它周圍區域像素點的內容了。就像加水稀釋類似的道理,既然都摻雜進周圍的內容了,那么它呈現的內容相比最初,肯定就不那么清晰了。
而如果摻雜的半徑越大,混合進的內容也就越多,那么它本身的內容就越淡了,是不是這個理。所以,這就是為什么每個開源的高斯模糊組件庫,使用時基本都需要我們傳入一個 radius 半徑的參數。而且,半徑越大,越模糊。
這么一粗坯的解釋,就理解多了,是吧。
為什么需要大概掌握這個理論基礎呢?
想想,高斯模糊是遍歷所有像素點,對每個像素點都重新計算。那么,這自然是一個耗時的工作,掌握了理論基礎,我們要優化時也才有方向去優化。
性能對比
大神的開源庫中提供了三種高斯模糊方式,而我在 Blankj 的 AndroidUtilCode 開源庫中發現了另外一種,所以我將他們都整合起來,一共有四種:
- Google 官方提供的 RenderScript 方式 (RSBlur)
- C 編寫的高斯算法 blur.c 方式 (NativeBlur)
- Java 編寫的高斯算法方式1(JavaBlur)
- Java 編寫的高斯算法方式2(StackBlur)
其實,大體上就三種:Google 官方提供的,大神用 C 寫的高斯模糊算法,大神用 Java 寫的高斯模糊算法。至於后面兩種,看了下,算法的代碼不一樣,我就把它們當做是兩種不同的算法實現了。也許是一樣,但我沒深入去看,反正代碼不一樣,我就這么認為了。
下面我們來做個實驗,在如下相同條件下,不同高斯模糊方案的耗時比較:
- radius=10, scale=1, bitmap=200*200(11.69KB)
ps: radius 表示高斯算法計算過程中的半徑取值,scale=1表示對 bitmap 原圖進行高斯模糊。這些前提條件需要了解一下,不然你在看網上其他類似性能比對的文章時,發現它們動不動就優化到幾 ms 級別的,然而你自己嘗試卻始終達不到。這是因為也許所使用的這些前提都不一致,不一致的前提下,耗時根本無從比較。
private void testBlur() {
int sum = 0;
for (int i = 0; i < 100; i++) {
long time = SystemClock.uptimeMillis();
DBlur.source(this, R.drawable.image).modeRs().radius(10).sampling(1).build().doBlurSync();
long end = SystemClock.uptimeMillis();
sum += (end - time);
}
Log.e("DBlur", "RSBlur cast " + (sum/100) + "ms");
}
代碼模板如上,分別運行 100 次后取平均值,四種不同方式的耗時如下表:
前提條件 | RSBlur | NativeBlur | JavaBlur | StackBlur |
---|---|---|---|---|
radius=10, scale=1, bitmap=200*200 | 51ms | 13ms | 162ms | 384ms |
radius=20, scale=1, bitmap=200*200 | 56ms | 12ms | 164ms | 435ms |
radius=10, scale=2, bitmap=200*200 | 48ms | 11ms | 75ms | 110ms |
radius=10, scale=8, bitmap=200*200 | 45ms | 7ms | 14ms | 19ms |
radius=10, scale=8, bitmap=1920*1180 | 183ms | 143ms | 346ms | 460ms |
radius=20, scale=8, bitmap=1920*1180 | 204ms | 145ms | 353ms | 510ms |
radius=20, scale=1, bitmap=1920*1180 | 474ms | 444ms | 8663ms | 內存溢出 |
100 次樣本可能不多,但大體上我們也能比較出不同類型的高斯模糊之前的區別,及其適用場景:
- 總體上,NativeBlur 和 RSBlur 的耗時會少於 JavaBlur 和 StackBlur
- JavaBlur 和 StackBlur 方式,如果先對 Bitmap 進行縮小,再高斯模糊,最后再放大,耗時會大大縮短
- radius 增大會增加耗時,但影響不大,但視圖呈現效果會越模糊
- scale 對原圖縮小倍數越多,耗時越短,但視圖呈現效果同樣會越模糊
- 分辨率越高的圖片,高斯模糊的就越耗時
- 對於大圖而言,如果要使用 JavaBlur 或 StackBlur,最好設置 scale 先縮小再模糊,否則將非常耗時且容易內存溢出
- 如果已經通過 scale 方式進行優化,那么最好 radius 值可以相對小一點,否則兩者的值都大會對圖片的模糊效果特別強烈,也許會過了頭
性能優化
高斯模糊的優化考慮點,其實就三個:
- 選擇不同的高斯模糊方式
- 通過 scale 對原圖先縮小,再模糊,最后再放大方式
- 優化高斯模糊算法
最后一點就不考慮了,畢竟難度太大。那么,其實就剩下兩種,要么是從高斯模糊的方案上選擇,要么從待模糊的圖片上做手腳。
雖然有四種高斯模糊方案,但每種都有各自優缺點:
- RSBlur 在低端機上可能無法兼容
- NativeBlur 需要生成對應 CPU 架構的 so 文件支持
- JavaBlur 和 StackBlur 耗時會較長
優化的考慮點大體上這幾種:
- 大體上,使用 NativeBlur 或者 RSBlur 即可,如果出現一些問題,那么此時可考慮切到 JavaBlur 或 StackBlur 方案,但記得結合 scale 方式優化處理。
- 如果高斯模糊的圖片有實時性要求,要求模糊得同步進行處理,主線程后續的工作需要等待高斯模糊后才能夠處理的話,那么盡量選擇 scale 方式進行優化,減少耗時。
- 如果對實時性沒要求,但對圖片模糊程度有要求,那其實,只要后台異步去進行高斯模糊即可,此時 scale 可不用縮小太多,而利用 radius 來控制模糊效果,以達到理想的要求。
- 如果兩者都有要求,那就自行嘗試尋找折中點吧。
最后說一點,因為已經封裝成組件庫了,RSBlur 也是引入的 support 包,so 文件也打包好了,那么使用這兩種方案足夠滿足絕大部分場景了,所以,沒有特意指定,組件默認的方案為 RSBlur。
二次封裝
需求場景
為什么要二次封裝?那肯定是因為有自己的各種需求場景的,我的需求如下:
- 要能夠對當前頁面(Activity)截圖后,進行模糊
- 要能夠對 drawable 資源圖片進行模糊、或者對指定 View 的視圖進行模糊
- 模糊完成后,要能夠自動以淡入的動畫方式顯示在指定的控件上
- 存在這種需求場景:對當前界面截圖、並且模糊,模糊后的圖片展示的時機可能在其他界面,因此需要支持緩存功能,可以根據指定 cacheKey 值獲取緩存
- 當然,可以根據各種配置使用高斯模糊,當不指定配置時,有默認配置
總結一下,其實封裝要做的事也就是要實現:
- 截圖、緩存、淡入動畫、默認配置
- 可以的話,組件最好可以達到,其他人在不看文檔,不看源碼前提下,以最少的成本接入直接上手使用
實現
截圖、緩存、動畫這些都屬於純功能代碼的封裝了,具體就不說了。
這里想來講講,如何設計,可以讓其他人以最少的成本接入直接上手使用。
我的想法是,利用 AndroidStudio 的代碼提示功能,具體的說就是,你只需要了解組件的入口是 DBlur 即可,至於后續怎么使用,全靠 AndroidStudio 來提示,跟着 AndroidStudio 走就行。
例如:
當敲完 DBlur. 時,會彈出代碼提示框,入口很少,是吧,就兩個,看命名也能猜到作用:getCacheBitmap()
明顯是用來取緩存的,那么要高斯模糊自然是另外一個入口 source()
,這個方法有多個重載函數,看參數,其實也能知道,這就對應着要模糊的圖片的不同來源類型,如:
- 直接傳入 Bitmap 對其進行模糊
- 傳入 Activity/View,內部會對這個界面/控件進行截圖后再模糊
- 傳入 resId,對 drawable 資源圖片進行模糊
那么,可能想問了,哪里進行高斯模糊配置,哪里設置同步或異步,哪里注冊回調等等。別急,既然只給你開了一個入口,那么就跟着入口走下去,自然會一步步引導你走到最后。如:
第一步、第二步該做什么,我都給你規定好了,你也只能按照步驟一步步來。想要設置高斯模糊配置,你得先指定圖片來源,才能進入第二步,在這里,可以進行的配置也都給你列出來了,想要哪個,直接設置即可。如:
mode()
,modeRs()
,modeNative()
等等類似 mode 開頭的方法,用於指定要使用哪種高斯模糊方案,一共四種,每種內部都有提供對應的常量標志,但如果你不知道哪里找,那么直接調用 modeXXX 方法即可。radius()
用於設置高斯模糊計算的半徑,內部默認為 4。sampling()
用於設置對原圖的縮小比例,內部默認為 8,即默認先縮小 8 倍,再模糊,最后再放大。cache()
用於設置緩存此次模糊后的圖片,沒有調用默認不緩存。animAlpha()
用於設置使用淡入動畫,需要結合intoTarget()
使用,否則不生效。intoTarget()
用於設置模糊完成后,自動顯示到指定控件上。
另外,看每個方法返回的類名,其實這個過程都是在設置配置項,如果有對 Builder 模式了解的話,應該清楚,這個大多用來解決構造函數參數過多的場景,最后一般都會有一個 build()
或者 create()
類型的方法。參考的是 Android 源碼中 AlertDialog。
也就是說,要進入下一個步驟,需要調用 build()
方法,如:
顯然,已經到最后一個步驟了,這里就是發起高斯模糊工作的地方了。
doBlur()
會指定此次高斯模糊工作異步進行,所以需要注冊回調的在這里傳入。doBlurSync()
指定高斯模糊同步進行,模糊后的 Bitmap 直接返回。
至此,接入結束。
使用這個高斯模糊的組件,只需要知道 DBlur 入口,其他都跟隨着 AndroidStudio 代碼提示一步步往下走即可,當然你也可以直接看源碼,注釋里也寫得蠻清楚的了。
這個就是我的想法,能力不足,只能想出這種方案,如果有哪里需要改進,哪里不合理,或者有其他思路,歡迎指點一下。
使用示例
compile 'com.dasu.image:blur:0.0.4'
//使用默認配置,最短調用鏈
Bitmap bitmap = DBlur.source(MainActivity.this).build().doBlurSync();
//同步模糊,將imageView控制的視圖進行模糊,完成后自動顯示到 imageView1 控件上,以淡入動畫方式
DBlur.source(imageView).intoTarget(imageView1).animAlpha().build().doBlurSync();
//異步模糊,將drawable資源文件中的圖片以 NATIVE 方式進行模糊,注冊回調,完成時手動顯示到 imageView1 控件上
DBlur.source(this, R.drawable.background).mode(BlurConfig.MODE_NATIVE).build()
.doBlur(new OnBlurListener() {
@Override
public void onBlurSuccess(Bitmap bitmap) {
imageView1.setImageBitmap(bitmap);
}
@Override
public void onBlurFailed() {
//do something
}});
Github
DBlur 的 Github 鏈接:https://github.com/woshidasusu/base-module/tree/master/blur
大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支持~