Flutter 圖片庫重磅開源!

語言: CN / TW / HK

作者:新宿

背景

去年,閒魚技術團隊新一代圖片庫 PowerImage 在經過一系列灰度、問題修復、程式碼調優後,已全量穩定應用於閒魚。

相對於上一代 IFImage,PowerImage 經過進一步的演進,適應了更多的業務場景與最新的 flutter 特性,解決了一系列痛點:比如,因為完全拋棄了原生的 ImageCache,在與原生圖片混用的場景下,會讓一些低頻的圖片反而佔用了快取;比如,我們在模擬器上無法展示圖片;比如我們在相簿中,需要在圖片庫之外再搭建圖片通道。

詳情可閱讀:《Flutter 圖片庫高燃新登場》

PowerImage相關連結:

GitHub:(✅star🌟)

https://github.com/alibaba/power_image

Flutter pub:(✅like👍)

https://pub.dev/packages/power_image

簡介

PowerImage 是一個充分利用 native 原生圖片庫能力、高擴充套件性的flutter圖片庫。我們巧妙地將外接紋理與 ffi 方案組合,以更貼近原生的設計,解決了一系列業務痛點。

能力特點

  • 支援載入 ui.Image 能力。在基於外接紋理的方案中,使用方無法拿到真正的 ui.Image 去使用,這導致圖片庫在這種特殊的使用場景下無能為力。
  • 支援圖片預載入能力。正如原生precacheImage一樣。這在某些對圖片展示速度要求較高的場景下非常有用。
  • 新增紋理快取,與原生圖片庫快取打通!統一圖片快取,避免原生圖片混用帶來的記憶體問題。
  • 支援模擬器。在 flutter-1.23.0-18.1.pre之前的版本,模擬器無法展示 Texture Widget。
  • 完善自定義圖片型別通道。解決業務自定義圖片獲取訴求。
  • 完善的異常捕獲與收集。
  • 支援動圖。(來自淘特的PR)

Flutter 原生方案

在介紹新方案開始之前,先簡單回憶一下 flutter 原生圖片方案。

原生 Image Widget 先通過 ImageProvider 得到 ImageStream,通過監聽它的狀態,進行各種狀態的展示。比如frameBuilderloadingBuilder,最終在圖片載入成功後,會rebuild 出 RawImageRawImage 會通過 RenderImage 來繪製,整個繪製的核心是 ImageInfo 中的 ui.Image

  • Image:負責圖片載入的各個狀態的展示,如載入中、失敗、載入成功展示圖片等。\

  • ImageProvider:負責 ImageStream 的獲取,比如系統內建的 NetworkImage、AssetImage 等。

  • ImageStream:圖片資源載入的物件。

在梳理 flutter 原生圖片方案之後,我們發現是不是有機會在某個環節將 flutter 圖片和 native 以原生的方式打通?

新一代方案

我們巧妙地將 FFi 方案與外接紋理方案組合,解決了一系列業務痛點。

FFI

正如開頭說的那些問題,Texture 方案有些做不到的事情,這需要其他方案來互補,這其中核心需要的就是ui.Image。我們把 native 記憶體地址、長度等資訊傳遞給 flutter 側,用於生成ui.Image。

首先 native 側先獲取必要的引數(以 iOS 為例):

dart 側拿到後

我們可以通過 ffi 拿到 native 記憶體,從而生成 ui.Image。這裡有個問題,雖然通過 ffi 能直接獲取 native 記憶體,但是由於 decodeImageFromPixels 會有記憶體拷貝,在拷貝解碼後的圖片資料時,記憶體峰值會更加嚴重。

這裡有兩個優化方向:

  1. 解碼前的圖片資料給 flutter,由 flutter 提供的解碼器解碼,從而削減記憶體拷貝峰值;
  2. 與 flutter 官方討論,嘗試從內部減少這次記憶體拷貝。

FFI 這種方式適合輕度使用、特殊場景使用,支援這種方式可以解決無法獲取 ui.Image 的問題,也可以在模擬器上展示圖片(flutter <= 1.23.0-18.1.pre),並且圖片快取將完全交給 ImageCache 管理。

Texture

Texture 方案與原生結合有一些難度,這裡涉及到沒有 ui.Image 只有 textureId。這裡有幾個問題需要解決:

  1. 問題一:Image Widget 需要 ui.Image 去 build RawImage 從而繪製,這在本文前面的Flutter 原生方案介紹中也提到了;
  2. 問題二:ImageCache 依賴 ImageInfo 中 ui.Image 的寬高進行 cache 大小計算以及快取前的校驗;
  3. 問題三:native 側 texture 生命週期管理。

分別都有解決方案:

問題一: 通過自定義 Image 解決,透出 imageBuilder 來讓外部自定義圖片 widget

問題二: 為 Texture 自定義 ui.Image,如下:

這樣的話,TextureImage 實際上就是個殼,僅僅用來計算 cache 大小。實際上,ImageCache 計算大小,完全沒必要直接接觸到 ui.Image,可以直接找 ImageInfo 取,這樣的話就沒有這個問題了。\

問題三: 關於 native 側感知 flutter image 釋放時機的問題。

修改的 ImageCache 釋放如下(部分程式碼):

整體架構

我們將兩種解決方案非常優雅地結合在了一起:

我們抽象出了 PowerImageProvider ,對於 external(ffi)、texture,分別生產自己的 ImageInfo 即可。它將通過對 PowerImageLoader 的呼叫,提供統一的載入與釋放能力。

藍色實線的 ImageExt 即為自定義的 Image Widget,為 texture 方式透出了 imageBuilder。

藍色虛線 ImageCacheExt 即為 ImageCache 的擴充套件,僅在 flutter < 2.2.0 版本才需要,它將提供 ImageCache 釋放時機的回撥。

這次,我們也設計了超強的擴充套件能力。除了支援網路圖、本地圖、flutter 資源、native 資源外,我們提供了自定義圖片型別的通道,flutter 可以傳遞任何自定義的引數組合給 native,只要 native 註冊對應型別 loader,比如「相簿」這種場景,使用方可以自定義 imageType 為 album ,native 使用自己的邏輯進行載入圖片。有了這個自定義通道,甚至圖片濾鏡都可以使用 PowerImage 進行展示重新整理。

除了圖片型別的擴充套件,渲染型別也可進行自定義。比如在上面 ffi 中說的,為了降低記憶體拷貝帶來的峰值問題,使用方可以在 flutter 側進行解碼,當然這需要 native 圖片庫提供解碼前的資料。

資料

FFI vs Texture

機型:iPhone 11 Pro;圖片:300 張網路圖;行為:在listView中手動滾動到底部再滾動到頂部;
native Cache:20 maxMemoryCount; flutter Cache:30MB

flutter version 2.5.3; release 模式下

這裡有兩個現象:

FFI:   186MB波動 Texture:194MB波動

在 2.5.3 版本中,Texture 方案與 FFI,在記憶體水位上差異不大,記憶體波動上面與 flutter 1.22 結論相反。

圖中棋格圖,為開啟 checkerboardRasterCacheImages  後所展示,可以看出,ffi方案會快取整個cell,而texture方案,只有cell中的文字被快取,RasterCache 會使得 ffi 在流暢度方面會有一定優勢。

滾動流暢性分析

裝置: Android OnePlus 8t,CPU和GPU進行了鎖頻。
case: GridView每行4張圖片,300張圖片,從上往下,再從下往上,滑動幅度從500,1000,1500,2000,2500,5輪滑動。重複20次。

方式: for i in {1..20}; do flutter drive --target=test_driver/app.dart --profile; done 跑資料,獲取TimeLine資料並分析。

結論:

  • UI thread 耗時 texture 方式最好,PowerImage 略好於 IFImage,FFI方式波動比較大。
  • Raster thread 耗時 PowerImage 好於 IFImage。Origin 原生方式好是因為對圖片 resize了,其他方式載入的是原圖。

更精簡的程式碼

dart 側程式碼有較大幅度的減少,這歸功於技術方案貼合 flutter 原生設計,我們與原生圖片共用較多程式碼。

FFI 方案補全了外接紋理的不足,遵循原生 Image 的設計規範,不僅讓我們享受到 ImageCache 帶來的統一管理,也帶來了更精簡的程式碼。

單測

為了保證核心程式碼的穩定性,我們有著較為完善的單測,行覆蓋率接近95%。

關於開源

我們期待通過社群的力量讓 PowerImage 更加完善與強大,也希望 PowerImage 能為大家在工程研發中帶來收益。

Issues

關於 issue,我們希望大家在使用 PowerImage 遇到問題與訴求時,積極交流,提出 issue 時儘可能提供詳細的資訊,以減少溝通成本。在提出 issue 前,請確保已閱讀 readme。

對於 bug 的 issue,我們自定義了模板(Bug report),可以方便地填一些必要的資訊。其他型別則可以選擇 Open a blank issue。

我們每週會花部分時間統一處理 issues,也期待大家的討論與 PR。

PR

為了保持 PowerImage 核心功能的穩定性,我們有著完善的單測,行覆蓋率達到了 95%(power_image庫)。

在提交PR時,請確保所提交的程式碼被單測覆蓋到,並且涉及到的單測程式碼請同時提交。

得益於 Github 的 Actions 能力,我們在主分支 push 程式碼、對主分支進行 PR 操作時,都會觸發 flutter test 任務,只有單測通過才可合入。

未來

開源是 PowerImage 的開始,而不是結束,PowerImage 可做的事情還有很多,有趣而豐富。比如第一個 issue 中描述的 loadingBuilder 如何實現?比如 ffi 方案如何支援動圖?再比如Kotlin和Swift···

PowerImage 未來將持續演進,在當前 texture 方案與 ffi 方案共存的情況下,伴隨著 flutter 本身的迭代,我們將更傾向於向 ffi 發展,正如在上文的對比中, ffi 方案可以天然享用 raster cache 所帶來的流暢度的優勢。

PowerImage 也會持續追隨 flutter 的腳步,以始終貼合原生的設計理念,不斷進步,我們希望更多的同學加入進來,共同成長。

PowerImage相關連結:

GitHub:(✅star🌟)

https://github.com/alibaba/power_image

Flutter pub:(✅like👍)

https://pub.dev/packages/power_image

關注【阿里巴巴移動技術】,阿里前沿移動乾貨&實踐給你思考!