一個優秀的可定製化Flutter相簿元件,看這一篇就夠了

閒魚技術發表於2019-06-26

背景

開發圖片、視訊相關功能時,相簿是一個繞不開的話題,因為大家基本都有從相簿獲取圖片或者視訊的需求。最直接的方式是呼叫系統相簿介面,雖然基本功能是滿足的,卻無法滿足一些高階功能,例如自定義UI、多選圖片等。

設計思路

閒魚這套相簿元件API使用簡單,功能豐富靈活,具有較高的訂製性。業務方可以選擇完全接入元件,也可以選擇在元件上面進行UI定製。

Flutter做UI展現層,具體的資料由各Native平臺提供。這種模式,天然從工程上把UI程式碼和資料程式碼進行了隔離。我們在開發一個native元件的時候常常會使用MVC架構。Flutter元件的開發的思路也基本類似。整體架構如下:

一個優秀的可定製化Flutter相簿元件,看這一篇就夠了

可以看出,在Flutter側是一個典型的MVC架構,這裡Widget就是View,View和Model繫結,在Model改變的時候View會重新build反映出Model的變化。View的事件會觸發Controller去Native獲取資料然後更新Model。Native和Flutter通過Method Channel進行通訊,兩層之間沒有強依賴關係,只需要按約定的協議進行通訊即可。

Native側的組成部分,UIAdapter主要是負責機型的適配、劉海屏、全面屏之類的識別。Permission負責媒體讀寫許可權的申請處理。Cache主要負責快取GPU紋理,在大圖預覽的時候提高響應速度。Decoder負責解析Bitmap,OpenGL負責Bitmap轉紋理。

需要說明的是:我們的這一套實現依賴於flutter外接紋理。在整個相簿元件看到的大多數圖片都是一個GPU紋理,這樣給java堆記憶體的佔用相對於以前的相簿實現有大幅的降低。在低端機上面如果使用原生的系統相簿,由於記憶體的原因,app有被系統殺掉的風險。現象就是,從系統相簿返回,app重新啟動了。使用Flutter相簿元件,在低端機上面體驗會有所改觀。

一些難點

分頁載入

相簿列表需要載入大量圖片,Flutter的GridView元件有好幾個建構函式,比較容易犯的錯誤是使用了第一個函式,這需要在一開始就提供大量的widget。應該選擇第二個建構函式,GridView在滑動的時候會回撥IndexedWidgetBuilder來獲取widget,相當於一種懶載入。

GridView.builder({...List<Widget> children = const <Widget>[],...})

GridView.builder({...@required IndexedWidgetBuilder itemBuilder,int itemCount,...})
複製程式碼

滑動過程中,圖片滑過後,也就是不可見的時候要進行資源的回收,我們這裡這裡對應的就是紋理的刪除。不斷的滑動GridView,記憶體在上升後會處於穩定,不會一直增長。如果快速的來回滑動紋理會反覆的建立和刪除,這樣會有記憶體的抖動,體驗不是很好。

於是,我們維護了一個圖片的狀態機,狀態包括None,Loading,Loaded,Wait_Dispose,Disposed。開始載入的時候,狀態從None進入Loading,這個時候使用者看到的是空白或者是佔點陣圖,當資料回撥回來會把狀態設定為Loaded的這時候會重新build widget樹來顯示圖片icon,當使用者滑走的時候狀態進入 Wait_Dispose,這時候並不會馬上Dispose,如果使用者又滑回來則會從Wait_Dispose進入Loaded狀態,不會繼續Dispose。如果使用者沒有往回滑則會從Wait_Dispose進入Disposed狀態。當進入Disposed狀態後,再需要顯示該圖片的時候就需要重新走載入流程了。

相簿大圖展示

當點選GridView的某張圖片的時候會進行這張圖片的大圖展示,方便使用者檢視的更清楚。我們知道相機拍攝的圖片解析度都是很高的,如果完全載入,記憶體會有很大的開銷,所以我們在Decode Bitmap的時候進行了縮放,最高只到1080p。Android原生的Bitmap Decode經驗同樣適用,先Decode出Bitmap的寬高,然後根據要展示的大小計算出縮放倍數, 然後Decode出需要的Bitmap。

Android相簿的圖片大多是有旋轉角度的,如果不處理直接顯示,會出現照片旋轉90度的問題,所以需要對Bitmap進行旋轉,採用Matrix旋轉一張1080p的圖片在我的測試機器上面大概需要200ms,如果使用OpenGL的紋理座標進行旋轉,大於只需要10ms左右,所以採用OpenGl進行紋理的旋轉是一個較好的選擇。

在進行大圖預覽的時候會進入一個水平滑動的PageView,Flutter的PageView一般來說是不會去主動載入相鄰的page的。這裡有一個取巧的辦法,對於PageController的viewportFraction引數我們可以設定成為0.9999,如下所示:

PageController(viewportFraction=0.9999)
複製程式碼

還有另外一種辦法,就是在Native側做預載入。例如:在載入第5張圖片的時候,相鄰的4,6的圖片紋理提前進行載入,當滑動到4,6的時候直接使用快取的紋理。

記憶體

相簿圖片使用GPU紋理,會大幅減少Java堆記憶體的佔用,對整個app的效能有一定的提升。需要注意的是,GPU的記憶體是有限的需要在使用完畢後及時刪除,不然會有記憶體的洩漏的風險。另外,在Android平臺刪除紋理的時候需要保證在GPU執行緒進行,不然刪除是沒有效果的。

在華為P8,Android5.0上面進行了對比測試,Flutter相簿和原native相簿總記憶體佔用基本一致,在GridView列表頁面,新增最大記憶體13M左右。它們的區別在於原native相簿使用的是Java堆記憶體,Flutter相簿使用的是Native記憶體。

總結

這套相簿元件API簡單、易用,高度可定製。Flutter側層次分明,有UI訂製需求的可以重寫Widget來達到目的。另外這是一個不依賴於系統相簿的相簿元件,自身是完備的,能夠和現有的app保持UI、互動的一致性。同時為後面支援更多和相簿相關的玩法打好基礎。

後續計劃

由於我們使用的是GPU紋理,可以考慮支援顯示高清4K圖片,而且客戶端記憶體不會有太大的壓力。但是4k圖片的Bitmap轉紋理需消耗更多的時間,UI互動上面未來考慮做loading狀態的支援。

元件功能豐富,線上穩定執行一段時間後將會開源回饋給社群。大家敬請關注閒魚技術公眾號。

相關文章