節(jié)日獻(xiàn)禮:Flutter圖片庫重磅開源!
2022-11-13|14:42|發(fā)布在分類 / 成功案例| 閱讀:92
2022-11-13|14:42|發(fā)布在分類 / 成功案例| 閱讀:92
背景
去年,閑魚技術(shù)團(tuán)隊(duì)新一代圖片庫 PowerImage 在經(jīng)過一系列灰度、問題修復(fù)、代碼調(diào)優(yōu)后,已全量穩(wěn)定應(yīng)用于閑魚。相對于上一代 IFImage,PowerImage 經(jīng)過進(jìn)一步的演進(jìn),適應(yīng)了更多的業(yè)務(wù)場景與最新的 flutter 特性,解決了一系列痛點(diǎn):比如,因?yàn)橥耆珤仐壛嗽?ImageCache,在與原生圖片混用的場景下,會讓一些低頻的圖片反而占用了緩存;比如,我們在模擬器上無法展示圖片;比如我們在相冊中,需要在圖片庫之外再搭建圖片通道。
簡介
PowerImage 是一個(gè)充分利用 native 原生圖片庫能力、高擴(kuò)展性的flutter圖片庫。我們巧妙地將外接紋理與 ffi 方案組合,以更貼近原生的設(shè)計(jì),解決了一系列業(yè)務(wù)痛點(diǎn)。
在介紹新方案開始之前,先簡單回憶一下 flutter 原生圖片方案。
原生 Image Widget 先通過 ImageProvider 得到 ImageStream,通過監(jiān)聽它的狀態(tài),進(jìn)行各種狀態(tài)的展示。比如frameBuilder、loadingBuilder,最終在圖片加載成功后,會 rebuild出 RawImage,RawImage會通過 RenderImage來繪制,整個(gè)繪制的核心是 ImageInfo中的 ui.Image。
在梳理 flutter 原生圖片方案之后,我們發(fā)現(xiàn)是不是有機(jī)會在某個(gè)環(huán)節(jié)將 flutter 圖片和 native 以原生的方式打通?
新一代方案
我們巧妙地將 FFi 方案與外接紋理方案組合,解決了一系列業(yè)務(wù)痛點(diǎn)。
正如開頭說的那些問題,Texture 方案有些做不到的事情,這需要其他方案來互補(bǔ),這其中核心需要的就是 ui.Image。我們把 native 內(nèi)存地址、長度等信息傳遞給 flutter 側(cè),用于生成 ui.Image。
首先 native 側(cè)先獲取必要的參數(shù)(以 iOS 為例):
_rowBytes = CGImageGetBytesPerRow(cgImage);
CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);
CFDataRef rawDataRef = CGDataProviderCopyData(dataProvider);
_handle = (long)CFDataGetBytePtr(rawDataRef);
NSData *data = CFBridgingRelease(rawDataRef);
self.data = data;
_length = data.length;
dart 側(cè)拿到后
@override
FutureOrcreateImageInfo(Map map) {
Completercompleter = Completer();
int handle = map['handle'];
int length = map['length'];
int width = map['width'];
int height = map['height'];
int rowBytes = map['rowBytes'];
ui.PixelFormat pixelFormat =
ui.PixelFormat.values[map['flutterPixelFormat'] ?? 0];
Pointerpointer = Pointer.fromAddress(handle);
Uint8List pixels = pointer.asTypedList(length);
ui.decodeImageFromPixels(pixels, width, height, pixelFormat,
(ui.Image image) {
ImageInfo imageInfo = ImageInfo(image: image);
completer.complete(imageInfo);
//釋放 native 內(nèi)存
PowerImageLoader.instance.releaseImageRequest(options);
}, rowBytes: rowBytes);
return completer.future;
}
我們可以通過 ffi 拿到 native 內(nèi)存,從而生成 ui.Image。這里有個(gè)問題,雖然通過 ffi 能直接獲取 native 內(nèi)存,但是由于 decodeImageFromPixels會有內(nèi)存拷貝,在拷貝解碼后的圖片數(shù)據(jù)時(shí),內(nèi)存峰值會更加嚴(yán)重。
這里有兩個(gè)優(yōu)化方向:
FFI 這種方式適合輕度使用、特殊場景使用,支持這種方式可以解決無法獲取 ui.Image 的問題,也可以在模擬器上展示圖片(flutter <= 1.23.0-18.1.pre),并且圖片緩存將完全交給 ImageCache 管理。
Texture 方案與原生結(jié)合有一些難度,這里涉及到?jīng)]有 ui.Image只有 textureId。這里有幾個(gè)問題需要解決:
問題一:Image Widget 需要 ui.Image去 build RawImage從而繪制,這在本文前面的Flutter 原生方案介紹中也提到了;問題二:ImageCache 依賴 ImageInfo 中 ui.Image的寬高進(jìn)行 cache 大小計(jì)算以及緩存前的校驗(yàn);問題三:native 側(cè) texture 生命周期管理。分別都有解決方案:
問題一:通過自定義 Image 解決,透出 imageBuilder 來讓外部自定義圖片 widget
問題二:為 Texture 自定義 ui.image,如下:
import'dart:typed_data';
import'dart:ui'as ui show Image;
import'dart:ui';
classTextureImageimplementsui.Image{
int_width;
int_height;
int textureId;
TextureImage(this.textureId, int width, int height)
: _width = width,
_height = height;
@override
void dispose() {
// TODO: implement dispose
}
@override
intget height =>_height;
@override
FuturetoByteData(
{ImageByteFormat format = ImageByteFormat.rawRgba}) {
// TODO: implement toByteData
throw UnimplementedError();
}
@override
intget width =>_width;
}
這樣的話,TextureImage 實(shí)際上就是個(gè)殼,僅僅用來計(jì)算 cache 大小。實(shí)際上,ImageCache 計(jì)算大小,完全沒必要直接接觸到 ui.Image,可以直接找 ImageInfo 取,這樣的話就沒有這個(gè)問題了。
問題三:關(guān)于 native 側(cè)感知 flutter image 釋放時(shí)機(jī)的問題。
修改的 ImageCache 釋放如下(部分代碼):
typedefvoid HasRemovedCallback(dynamic key, dynamic value);
classRemoveAwareMap<K, V>implementsMap<K, V>{
HasRemovedCallback hasRemovedCallback;
...
}
//------
final RemoveAwareMap<Object, _PendingImage>_pendingImages = RemoveAwareMap<Object, _PendingImage>();
//------
void hasImageRemovedCallback(dynamic key, dynamic value) {
if(key is ImageProviderExt) {
waitingToBeCheckedKeys.add(key);
}
if(isScheduledImageStatusCheck) return;
isScheduledImageStatusCheck = true;
//We should do check in MicroTask to avoid if image is remove and add right away
scheduleMicrotask(() {
waitingToBeCheckedKeys.forEach((key) {
if(!_pendingImages.containsKey(key) &&
!_cache.containsKey(key) &&
!_liveImages.containsKey(key)) {
if(key is ImageProviderExt) {
key.dispose();
}
}
});
waitingToBeCheckedKeys.clear();
isScheduledImageStatusCheck = false;
});
}整體架構(gòu)
我們將兩種解決方案非常優(yōu)雅地結(jié)合在了一起:
我們抽象出了 PowerImageProvider ,對于 external(ffi)、texture,分別生產(chǎn)自己的 ImageInfo 即可。它將通過對 PowerImageLoader 的調(diào)用,提供統(tǒng)一的加載與釋放能力。
藍(lán)色實(shí)線的 ImageExt 即為自定義的 Image Widget,為 texture 方式透出了 imageBuilder。
藍(lán)色虛線 ImageCacheExt 即為 ImageCache 的擴(kuò)展,僅在 flutter < 2.2.0 版本才需要,它將提供 ImageCache 釋放時(shí)機(jī)的回調(diào)。
這次,我們也設(shè)計(jì)了超強(qiáng)的擴(kuò)展能力。除了支持網(wǎng)絡(luò)圖、本地圖、flutter 資源、native 資源外,我們提供了自定義圖片類型的通道,flutter 可以傳遞任何自定義的參數(shù)組合給 native,只要 native 注冊對應(yīng)類型 loader,比如「相冊」這種場景,使用方可以自定義 imageType 為 album ,native 使用自己的邏輯進(jìn)行加載圖片。有了這個(gè)自定義通道,甚至圖片濾鏡都可以使用 PowerImage 進(jìn)行展示刷新。
除了圖片類型的擴(kuò)展,渲染類型也可進(jìn)行自定義。比如在上面 ffi 中說的,為了降低內(nèi)存拷貝帶來的峰值問題,使用方可以在 flutter 側(cè)進(jìn)行解碼,當(dāng)然這需要 native 圖片庫提供解碼前的數(shù)據(jù)。
數(shù)據(jù)
機(jī)型:iPhone 11 Pro;圖片:300 張網(wǎng)絡(luò)圖;行為:在listView中手動(dòng)滾動(dòng)到底部再滾動(dòng)到頂部;
native Cache:20 maxMemoryCount; flutter Cache:30MB
flutter version 2.5.3; release 模式下
這里有兩個(gè)現(xiàn)象:
FFI: 186MB波動(dòng)
Texture:194MB波動(dòng)
在 2.5.3 版本中,Texture 方案與 FFI,在內(nèi)存水位上差異不大,內(nèi)存波動(dòng)上面與 flutter 1.22 結(jié)論相反。
圖中棋格圖,為打開 checkerboardRasterCacheImages后所展示,可以看出,ffi方案會緩存整個(gè)cell,而texture方案,只有cell中的文字被緩存,RasterCache 會使得 ffi 在流暢度方面會有一定優(yōu)勢。
設(shè)備: Android OnePlus 8t,CPU和GPU進(jìn)行了鎖頻。
case: GridView每行4張圖片,300張圖片,從上往下,再從下往上,滑動(dòng)幅度從500,1000,1500,2000,2500,5輪滑動(dòng)。重復(fù)20次。
方式: for i in {1..20}; do flutter drive --target=test_driver/app.dart --profile; done 跑數(shù)據(jù),獲取TimeLine數(shù)據(jù)并分析。
結(jié)論:
dart 側(cè)代碼有較大幅度的減少,這歸功于技術(shù)方案貼合 flutter 原生設(shè)計(jì),我們與原生圖片共用較多代碼。
FFI 方案補(bǔ)全了外接紋理的不足,遵循原生 Image 的設(shè)計(jì)規(guī)范,不僅讓我們享受到 ImageCache 帶來的統(tǒng)一管理,也帶來了更精簡的代碼。
為了保證核心代碼的穩(wěn)定性,我們有著較為完善的單測,行覆蓋率接近95%。
關(guān)于開源
我們期待通過社區(qū)的力量讓 PowerImage 更加完善與強(qiáng)大,也希望 PowerImage 能為大家在工程研發(fā)中帶來收益。
關(guān)于 issue,我們希望大家在使用 PowerImage 遇到問題與訴求時(shí),積極交流,提出 issue 時(shí)盡可能提供詳細(xì)的信息,以減少溝通成本。在提出 issue 前,請確保已閱讀 readme。
對于 bug 的 issue,我們自定義了模板(Bug report),可以方便地填一些必要的信息。其他類型則可以選擇 Open a blank issue。
我們每周會花部分時(shí)間統(tǒng)一處理 issues,也期待大家的討論與 PR。
為了保持 PowerImage 核心功能的穩(wěn)定性,我們有著完善的單測,行覆蓋率達(dá)到了 95%(power_image庫)。
在提交PR時(shí),請確保所提交的代碼被單測覆蓋到,并且涉及到的單測代碼請同時(shí)提交。
得益于 Github 的 Actions 能力,我們在主分支 push 代碼、對主分支進(jìn)行 PR 操作時(shí),都會觸發(fā) flutter test任務(wù),只有單測通過才可合入。
未來
開源是 PowerImage 的開始,而不是結(jié)束,PowerImage 可做的事情還有很多,有趣而豐富。比如第一個(gè) issue 中描述的 loadingBuilder如何實(shí)現(xiàn)?比如 ffi 方案如何支持動(dòng)圖?再比如Kotlin和Swift···
PowerImage 未來將持續(xù)演進(jìn),在當(dāng)前 texture 方案與 ffi 方案共存的情況下,伴隨著 flutter 本身的迭代,我們將更傾向于向 ffi 發(fā)展,正如在上文的對比中, ffi 方案可以天然享用 raster cache 所帶來的流暢度的優(yōu)勢。
PowerImage 也會持續(xù)追隨 flutter 的腳步,以始終貼合原生的設(shè)計(jì)理念,不斷進(jìn)步,我們希望更多的同學(xué)加入進(jìn)來,共同成長。
這個(gè)問題還有疑問的話,可以加幕.思.城火星老師免費(fèi)咨詢,微.信號是為: msc496。
推薦閱讀:
更多資訊請關(guān)注幕 思 城。
微信掃碼回復(fù)「666」
別默默看了 登錄\ 注冊 一起參與討論!