幕思城>電商行情>案例>成功案例>節(jié)日獻(xiàn)禮:Flutter圖片庫重磅開源!

    節(jié)日獻(xiàn)禮:Flutter圖片庫重磅開源!

    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)。

    能力特點(diǎn)

    • 支持加載 ui.Image 能力。在基于外接紋理的方案中,使用方無法拿到真正的 ui.Image 去使用,這導(dǎo)致圖片庫在這種特殊的使用場景下無能為力。
    • 支持圖片預(yù)加載能力。正如原生precacheImage一樣。這在某些對圖片展示速度要求較高的場景下非常有用。
    • 新增紋理緩存,與原生圖片庫緩存打通!統(tǒng)一圖片緩存,避免原生圖片混用帶來的內(nèi)存問題。
    • 支持模擬器。在 flutter-1.23.0-18.1.pre之前的版本,模擬器無法展示 Texture Widget。
    • 完善自定義圖片類型通道。解決業(yè)務(wù)自定義圖片獲取訴求。
    • 完善的異常捕獲與收集。
    • 支持動(dòng)圖。(來自淘特的PR)
    Flutter 原生方案

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

    原生 Image Widget 先通過 ImageProvider 得到 ImageStream,通過監(jiān)聽它的狀態(tài),進(jìn)行各種狀態(tài)的展示。比如frameBuilder、loadingBuilder,最終在圖片加載成功后,會 rebuild出 RawImage,RawImage會通過 RenderImage來繪制,整個(gè)繪制的核心是 ImageInfo中的 ui.Image。

    • • Image:負(fù)責(zé)圖片加載的各個(gè)狀態(tài)的展示,如加載中、失敗、加載成功展示圖片等。
    • • ImageProvider:負(fù)責(zé) ImageStream 的獲取,比如系統(tǒng)內(nèi)置的 NetworkImage、AssetImage 等。
    • • ImageStream:圖片資源加載的對象。

    在梳理 flutter 原生圖片方案之后,我們發(fā)現(xiàn)是不是有機(jī)會在某個(gè)環(huán)節(jié)將 flutter 圖片和 native 以原生的方式打通?

    新一代方案

    我們巧妙地將 FFi 方案與外接紋理方案組合,解決了一系列業(yè)務(wù)痛點(diǎn)。

    FFI

    正如開頭說的那些問題,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)化方向:

    1. 1. 解碼前的圖片數(shù)據(jù)給 flutter,由 flutter 提供的解碼器解碼,從而削減內(nèi)存拷貝峰值。
    2. 2. 與 flutter 官方討論,嘗試從內(nèi)部減少這次內(nèi)存拷貝。

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

    Texture

    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ù)

    FFI vs Texture

    機(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)勢。

    滾動(dòng)流暢性分析

    設(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é)論:

    • • UI thread 耗時(shí) texture 方式最好,PowerImage 略好于 IFImage,F(xiàn)FI方式波動(dòng)比較大。
    • • Raster thread 耗時(shí) PowerImage 好于 IFImage。Origin 原生方式好是因?yàn)閷D片 resize了,其他方式加載的是原圖。

    更精簡的代碼

    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ā)中帶來收益。

    Issues

    關(guān)于 issue,我們希望大家在使用 PowerImage 遇到問題與訴求時(shí),積極交流,提出 issue 時(shí)盡可能提供詳細(xì)的信息,以減少溝通成本。在提出 issue 前,請確保已閱讀 readme。

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

    我們每周會花部分時(shí)間統(tǒng)一處理 issues,也期待大家的討論與 PR。

    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。

    難題沒解決?加我微信給你講!【僅限淘寶賣家交流運(yùn)營知識,非賣家不要加我哈】
    >

    推薦閱讀:

    手淘猜你喜歡是什么?影響手淘猜你喜歡流量的因素有哪些?

    更多資訊請關(guān)注幕 思 城。

    發(fā)表評論

    別默默看了 登錄\ 注冊 一起參與討論!

      微信掃碼回復(fù)「666