大數據

如何讓 Flutter 應用更好地使用 SVG?

image.png

例說歷史

在計算機的世界裡,很多空間優化都隱藏著計算消耗,比如下面這張色彩和形狀豐富的 4k 圖片(其實也可以是 8k,屏幕夠大就可以看到),壓縮後只有 5kB 大小。

image.png

如果這個 5kB 用 PNG 來存儲的圖片,是下圖這個樣子。

image.png

表現力天差地別。

為了達到類似的清晰度,一般操作系統會協助應用打包時在 UI 資源中歸集多個分辨率的圖片。

image.png
32x32
image.png
64x64
image.png
256x256
image.png
1024x1024

上面這一個圖標,資源包占用超過 120kB,其中最大的一個版本,運行內存佔用在 4MB。

這麼看來,SVG 圖片應該是絕對首選吧?

並非如此。在給 Flutter 做 SVG 支持分析之前,開發者可能覺得各個移動系統 API 中沒有提供是個很大缺憾。

而經過光柵化代價數據分析後,也能理解了系統對盲目使用 SVG 帶來問題的擔憂。

比如還是上面這個 SVG 圖片,在驍龍 626 的手機上,Flutter 光柵化到 64x64 的區域需要 34ms,一個 SVG 讓應用與 60 幀流暢度徹底無緣。實測 IPhone X 需要 8ms,只能流暢顯示兩個。

另外補充一點,SVG 或者說矢量圖的應用需求是 UI 扁平化趨勢興起後才出現的。在擬物化的時期,拋開光柵化速度不說,矢量圖在顯示寫實風格的圖標時,缺陷是無法容忍的。比如 doggy,用最先進的追蹤矢量化後(右側),已經數碼感十足,存儲佔用也遠超 PNG。

image.png

好在,扁平化的矢量圖在工程推進時,也在有意無意迴避前面說的問題,大部分都走簡約風。所以只要避開陷阱,SVG 還是在很多場景可以做到表現優秀的。

應用現狀

Flutter 項目主線沒有支持

Flutter 的基礎組件 Skia 代碼中有 SVG 目錄,但別誤會了,Skia 只有序列化至 SVG 的功能,沒有解碼繪製 SVG 的能力。

框架開發計劃目前也沒有支持的打算:
https://github.com/flutter/flutter/issues/1831

OS 也沒有支持的意向

這是可以理解的,因為龐大如 Android 和 iOS 也默認不支持:

大家的共識是,全功能的 SVG 支持工作量不小,還有性能隱患(都是拐著彎提到)。

SVG 的鍋,矢量字體方案不用背

前面 SVG 諮詢,在建議解決方案中,都提到用矢量字體解決。矢量字體:

  • 主流 OS 都自帶的支持。
  • 基本只能單色。
  • 不用依賴 xml。
  • 由於單色輸出,很多圖層繪製疊加等等不可控的性能影響要素都被排除。
  • 系統方便做位圖緩存管理(我們開發者工具後續可以再研究)。

雖然在 SVG 投入不少研究,也不得不承認,字體矢量圖輸出是目前很務實高效的方案。

配合工具流程改進 SVG 應用

SVG 作為一個強大的矢量圖標準格式,還是可以找到合適的應用的。比如多彩圖標,方便熱更新,生產工具對此格式的廣泛支持。

讓 SVG 再次偉大

在 OS 和 runtime 都拋棄 SVG 的情況下,flutter_svg 包毅然然扛起大旗,簡單快捷的給 Flutter 提供了 SVG 渲染解碼的能力,顯示出 Flutter/Dart 不俗的擴展潛能。

flutter_svg 的使用非常簡單,提供和 flutter framework 中 image_provider 類似的接口。下面兩段代碼就是分別顯示來自 asset 和網絡的 SVG 圖片:

SvgPicture.asset(
    'assets/adsmall.svg',
    placeholderBuilder: (BuildContext context) => Container(
        child: const CircularProgressIndicator()),
),

SvgPicture.network(
    'https://raw.githubusercontent.com/dnfield/flutter_svg/master/example/assets/deborah_ufw/new-camera.svg',
    placeholderBuilder: (BuildContext context) => Container(
        child: const CircularProgressIndicator()),
),

用工具避坑

不能對 SVG 的性能隱患坐視不理。

UC 瀏覽器內核技術團隊開發了一個【資源面板】工具,可以方便地連接 Flutter 應用,實時顯示資源分配的內存,對其中的 SVG 圖片,資源面板提供了預覽和獲取光柵化損耗的功能。

image.png

通過記錄和對比 SVG 在實際移動設備上的光柵化損耗,我們可以方便地識別出有隱患的 SVG 文件,將 SVG 的應用安排妥當。

image.png

通過實際 Rasterization Cost 的對比可以看到,簡約風格的圖標,時間消耗到 16.66ms 來說在驍龍 626 上也還是可以接受的。

實現原理

flutter_svg

flutter_svg 是一個 dart package,提供解析來自 network、asset、memory 等 SVG 的能力。

由於解析結果並不是 ui.Image 這樣的位圖,所以 flutter_svg 並沒有和 ImageCache 協作,而是自己實現了一套 PictureCache , PictureCache 中緩存的是 ui.Picture ,這個類實際是 skia 引擎的 SkPicture Wrapper,二進制方式記錄具體的 SVG 繪製指令。

ui.Picture 類佔用的內存不會很大,緩存基本上是為了避免反覆 parse xml。

比如 SvgPicture.asset 的構造接口如下:

SvgPicture.asset(
    String assetName, {
    Key key,
    this.matchTextDirection = false,
    AssetBundle bundle,
    String package,
    this.width,
    this.height,
    this.fit = BoxFit.contain,
    this.alignment = Alignment.center,
    this.allowDrawingOutsideViewBox = false,
    this.placeholderBuilder,
    Color color,
    BlendMode colorBlendMode = BlendMode.srcIn,
    this.semanticsLabel,
    this.excludeFromSemantics = false,
  })  : pictureProvider = ExactAssetPicture(
            allowDrawingOutsideViewBox == true
                ? svgStringDecoderOutsideViewBox
                : svgStringDecoder,
            assetName,
            bundle: bundle,
            package: package,
            colorFilter: _getColorFilter(color, colorBlendMode)),
        super(key: key);

SvgPicture 的 _picture,由 pictureProvider 的 stream 通知更新:

void _resolveImage() {
   final PictureStream newStream = widget.pictureProvider
       .resolve(createLocalPictureConfiguration(context));
   assert(newStream != null);
   _updateSourceStream(newStream);
 }

pictureProvider 的 stream 由 來自 pictureCache 的 completer 填充 ui.Picture 。

// in PictureProvider<T>.resolve
      stream.setCompleter(
        _cache.putIfAbsent(
          key,
          () => load(key, onError: onError),
        ),
      );

Debug 和 Profile 模式下,通過添加配合代碼,開發者工具可以在 PictureCache 中查詢所有現存的 SvgPicture 。

光柵化時間獲取

光柵化的發起接口是 ui.Picutre.toImage 方法,具體的計時在 rasterizer 線程。

image.png

補充說明

Android VectorDrawable

Android 提供了一套 VectorDrawable 方案,是一個簡化版的 SVG , 格式和特性不完全兼容,提供轉換工具。從文檔來看,確實是擔心過度複雜的 SVG 影響性能。參考文檔:
https://developer.android.com/studio/write/vector-asset-studio

單獨 SVG 位圖緩存優化

目前 Flutter 用的是一次性光柵化輸出每幀的模式,和 chromium 的 cc 按區域構建位圖再合成不同,如果在光柵化輸出時標記 SVG 的 Picture,緩存這部分位圖可以提升幀數,代價當然是內存損耗。

這個功能目前純用 Dart 無法方便實現,因為在 dart.ui 線程中,RenderPicture 無法預見具體的光柵化分辨率。

最後

目前,【資源面板】可在阿里內部使用,團隊正在爭取讓 Flutter 主線接受這一改動。歡迎大家探討交流。

Leave a Reply

Your email address will not be published. Required fields are marked *