如何让 Flutter 应用更好地使用 SVG?

如何让 Flutter 应用更好地使用 SVG?

例说历史

在计算机的世界里,很多空间优化都隐藏着计算消耗,比如下面这张色彩和形状丰富的 4k 图片(其实也可以是 8k,屏幕够大就可以看到),压缩后只有 5kB 大小。

如何让 Flutter 应用更好地使用 SVG?

如果这个 5kB 用 PNG 来存储的图片,是下图这个样子。

如何让 Flutter 应用更好地使用 SVG?

表现力天差地别。

为了达到类似的清晰度,一般操作系统会协助应用打包时在 UI 资源中归集多个分辨率的图片。

如何让 Flutter 应用更好地使用 SVG?
32x32
如何让 Flutter 应用更好地使用 SVG?
64x64
如何让 Flutter 应用更好地使用 SVG?
256x256
如何让 Flutter 应用更好地使用 SVG?
1024x1024

上面这一个图标,资源包占用超过 120kB,其中最大的一个版本,运行内存占用在 4MB。

这么看来,SVG 图片应该是绝对首选吧?

并非如此。在给 Flutter 做 SVG 支持分析之前,开发者可能觉得各个移动系统 API 中没有提供是个很大缺憾。

而经过光栅化代价数据分析后,也能理解了系统对盲目使用 SVG 带来问题的担忧。

比如还是上面这个 SVG 图片,在骁龙 626 的手机上,Flutter 光栅化到 64x64 的区域需要 34ms,一个 SVG 让应用与 60 帧流畅度彻底无缘。实测 IPhone X 需要 8ms,只能流畅显示两个。

另外补充一点,SVG 或者说矢量图的应用需求是 UI 扁平化趋势兴起后才出现的。在拟物化的时期,抛开光栅化速度不说,矢量图在显示写实风格的图标时,缺陷是无法容忍的。比如 doggy,用最先进的追踪矢量化后(右侧),已经数码感十足,存储占用也远超 PNG。

如何让 Flutter 应用更好地使用 SVG?

好在,扁平化的矢量图在工程推进时,也在有意无意回避前面说的问题,大部分都走简约风。所以只要避开陷阱,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 图片,资源面板提供了预览和获取光栅化损耗的功能。

如何让 Flutter 应用更好地使用 SVG?

通过记录和对比 SVG 在实际移动设备上的光栅化损耗,我们可以方便地识别出有隐患的 SVG 文件,将 SVG 的应用安排妥当。

如何让 Flutter 应用更好地使用 SVG?

通过实际 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 线程。

如何让 Flutter 应用更好地使用 SVG?

补充说明

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 主线接受这一改动。欢迎大家探讨交流。

上一篇:前端开发:工具和流程


下一篇:详解bootstrap-fileinput文件上传控件的亲身实践