例说历史
在计算机的世界里,很多空间优化都隐藏着计算消耗,比如下面这张色彩和形状丰富的 4k 图片(其实也可以是 8k,屏幕够大就可以看到),压缩后只有 5kB 大小。
如果这个 5kB 用 PNG 来存储的图片,是下图这个样子。
表现力天差地别。
为了达到类似的清晰度,一般操作系统会协助应用打包时在 UI 资源中归集多个分辨率的图片。
32x32
64x64
256x256
1024x1024
上面这一个图标,资源包占用超过 120kB,其中最大的一个版本,运行内存占用在 4MB。
这么看来,SVG 图片应该是绝对首选吧?
并非如此。在给 Flutter 做 SVG 支持分析之前,开发者可能觉得各个移动系统 API 中没有提供是个很大缺憾。
而经过光栅化代价数据分析后,也能理解了系统对盲目使用 SVG 带来问题的担忧。
比如还是上面这个 SVG 图片,在骁龙 626 的手机上,Flutter 光栅化到 64x64 的区域需要 34ms,一个 SVG 让应用与 60 帧流畅度彻底无缘。实测 IPhone X 需要 8ms,只能流畅显示两个。
另外补充一点,SVG 或者说矢量图的应用需求是 UI 扁平化趋势兴起后才出现的。在拟物化的时期,抛开光栅化速度不说,矢量图在显示写实风格的图标时,缺陷是无法容忍的。比如 doggy,用最先进的追踪矢量化后(右侧),已经数码感十足,存储占用也远超 PNG。
好在,扁平化的矢量图在工程推进时,也在有意无意回避前面说的问题,大部分都走简约风。所以只要避开陷阱,SVG 还是在很多场景可以做到表现优秀的。
应用现状
Flutter 项目主线没有支持
Flutter 的基础组件 Skia 代码中有 SVG 目录,但别误会了,Skia 只有序列化至 SVG 的功能,没有解码绘制 SVG 的能力。
框架开发计划目前也没有支持的打算:
https://github.com/flutter/flutter/issues/1831
OS 也没有支持的意向
这是可以理解的,因为庞大如 Android 和 iOS 也默认不支持:
- https://*.com/questions/34990236/how-to-use-svg-image-in-imageview
- https://*.com/questions/35691839/how-to-display-svg-image-using-swift
大家的共识是,全功能的 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 图片,资源面板提供了预览和获取光栅化损耗的功能。
通过记录和对比 SVG 在实际移动设备上的光栅化损耗,我们可以方便地识别出有隐患的 SVG 文件,将 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 线程。
补充说明
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 主线接受这一改动。欢迎大家探讨交流。