我们的项目中使用到了大量的图片资源,其中绝大部分都是由简单线条组成的 icon。这些素材在 UI 设计初期就是以矢量图的形式构建的,因此非常适合使用 SVG 这样的矢量文件进行储存。目前 iOS 或 Android(配合Google Play)都使用了类 Slicing 技术,能够根据不同设备自动选择合适的图片素材打进包里。而 Flutter 虽然在资源管理上实现了根据不同屏幕分辨率密度自动选择合适的图片资源的功能,但 Flutter 本身还无法实现类似 Apple Slide 这样自动根据不同设备下发不同尺寸资源的能力,因此多套素材无疑会给 App 包大小带来不可小觑的压力。
我们随机选取一个 icon 进行测试:
Resource | Format | x1 | x2 | x3 | Total |
---|---|---|---|---|---|
PNG | 536 bytes | 908 bytes | 1321 KB | 2765 bytes | |
SVG | 1,896 bytes |
我们发现,得益于矢量图像的特性,SVG 格式无需像 PNG 一样为每一种分辨率都生成一套位图,因此只需要计算一个文件的大小,在磁盘占用大小上 SVG 有着绝对优势。这还是在最大分辨率为 x3 的基础上的,随着超高分辨率屏幕和跨平台开发框架(Flutter、SwiftUI)的流行,为每套设备都切一套图的成本只会越来越高。
此外 PNG 还支持有损压缩,因此我们一般会对 PNG 进行进一步压缩以压榨 App 的体积。不过很多人可能不了解,SVG 格式也是可以压缩的。
我们将上图的 SVG 用文本编辑器打开,发现它使用了 XML 作为描述语言,并基于二维坐标系对矢量图形进行描述。他的头部往往会存在一些无用信息,包括转换软件遗留下的冗余信息。同时,同 js 压缩同理,我们也可以通过去处格式化来进一步压缩文件大小。
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24pt" height="24pt" viewBox="0 0 24 24"> <g enable-background="new"> <clipPath id="cp0"> <path transform="matrix(1,0,0,-1,0,24)" d="M 0 24 L 24 24 L 24 0 L 0 0 L 0 24 Z " fill-rule="evenodd"/> </clipPath> <g clip-path="url(#cp0)"> <clipPath id="cp1"> <path transform="matrix(1,0,0,-1,0,24)" d="M -335 -613 L 40 -613 L 40 54 L -335 54 Z "/> </clipPath> <g clip-path="url(#cp1)"> <path transform="matrix(1,0,0,-1,0,24)" d="M -5 29 L 29 29 L 29 -5 L -5 -5 Z " fill="#ffffff" fill-opacity=".01"/> </g> </g> <clipPath id="cp2"> <path transform="matrix(1,0,0,-1,0,24)" d="M -335 -613 L 40 -613 L 40 54 L -335 54 Z "/> </clipPath> <g clip-path="url(#cp2)"> <path transform="matrix(1,0,0,1,3.5,20.75)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke="#1f2329" d="M 0 .25 L 18 .25 "/> </g> <clipPath id="cp3"> <path transform="matrix(1,0,0,-1,0,24)" d="M 5.5 10.64008 L 5.5 7 L 9.158594 7 L 19.5 17.34593 L 15.84753 21 L 5.5 10.64008 Z "/> </clipPath> <g clip-path="url(#cp3)"> <clipPath id="cp4"> <path transform="matrix(1,0,0,-1,0,24)" d="M -335 -613 L 40 -613 L 40 54 L -335 54 Z "/> </clipPath> <g clip-path="url(#cp4)"> <path transform="matrix(1,0,0,-1,0,24)" d="M .5 26 L 24.5 26 L 24.5 2 L .5 2 Z " fill="#ffffff"/> </g> </g> <clipPath id="cp5"> <path transform="matrix(1,0,0,-1,0,24)" d="M -335 -613 L 40 -613 L 40 54 L -335 54 Z "/> </clipPath> <g clip-path="url(#cp5)"> <path transform="matrix(1,0,0,1,5.5,3)" stroke-width="2" stroke-linecap="butt" stroke-linejoin="round" fill="none" stroke="#1f2329" d="M 0 10.35992 L 0 14 L 3.658594 14 L 14 3.654071 L 10.34753 0 L 0 10.35992 Z "/> </g> </g> </svg> 复制代码
我们使用了SVG优化工具,经过压缩,我们可以很明显的看到,压缩后的字符数明显减少,压缩比达到了65%。虽然原理不是很复杂,但实际得到的压缩效果还是很可观的。
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><clipPath id="cp0"><path transform="matrix(1 0 0 -1 0 24)" d="M0 24h24V0H0v24z" fill-rule="evenodd"/></clipPath><g clip-path="url(#cp0)"><clipPath id="cp1"><path transform="matrix(1 0 0 -1 0 24)" d="M-335-613H40V54h-375z"/></clipPath><g clip-path="url(#cp1)"><path d="M-5-5h34v34H-5z" fill="#fff" fill-opacity=".01"/></g></g><clipPath id="cp2"><path transform="matrix(1 0 0 -1 0 24)" d="M-335-613H40V54h-375z"/></clipPath><g clip-path="url(#cp2)"><path stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke="#1f2329" d="M3.5 21h18"/></g><clipPath id="cp3"><path transform="matrix(1 0 0 -1 0 24)" d="M5.5 10.64V7h3.659L19.5 17.346 15.848 21 5.5 10.64z"/></clipPath><g clip-path="url(#cp3)"><clipPath id="cp4"><path transform="matrix(1 0 0 -1 0 24)" d="M-335-613H40V54h-375z"/></clipPath><g clip-path="url(#cp4)"><path d="M.5-2h24v24H.5z" fill="#fff"/></g></g><clipPath id="cp5"><path transform="matrix(1 0 0 -1 0 24)" d="M-335-613H40V54h-375z"/></clipPath><g clip-path="url(#cp5)"><path stroke-width="2" stroke-linejoin="round" fill="none" stroke="#1f2329" d="M5.5 13.36V17h3.659L19.5 6.654 15.848 3 5.5 13.36z"/></g></svg> 复制代码
Resource | Format | x1 | x2 | x3 | Total |
---|---|---|---|---|---|
PNG | 326 bytes | 654 bytes | 567 KB | 1547 bytes | |
SVG | 1234 bytes |
将两种格式都各自进行压缩操作后,会发现最终体积的差别其实不大。尤其是对于 iOS 或 Android(配合Google Play)这种支持 slicing 的原生平台,单个位图体积也要优于矢量图。 此外,位图的解析时间成本要低于矢量图(尤其是在复杂图像上)。而在 iOS 平台还会额外对 PNG 进行优化,因此 Apple 之前一直没有正式支持 SVG 资源。 不过在最新的 Xcode 12 中我们惊讶的发现 Assets 已经支持 SVG 资源了。
不过不要高兴的太早,因为我们知道 Apple 早在 Xcode 6 就支持了矢量格式 PDF,但其实现原理还是在编译过程中将 PDF 动态生成三种尺寸的 PNG,实际展示的时候使用的还是位图资源。 转机出现在了 iOS 13 上。SF Symbols 的出现让矢量图一下子出现在了 Apple 平台的 C 位,连带着欲遮还羞的内部私有库 CoreSVG,让所有人都对原生 SVG 的支持抱有无限遐想。那这次 Xcode 12 对 SVG 的支持,使用的是传说中的 CoreSVG,还是 PDF 的老把戏?接下俩我们实操一下。 通过 Xcode 的 Debug View Hierarchy,我们可以看到 Image 的 data 是 base64 编码的内容,有种一种不祥的气息。 通过对 data 进行解码,不出所料,Xcode 还是将 SVG 转换成了 PNG。而且通过阅读 iOS 14 的 API,CoreSVG 依旧没有公开出来。这个不痛不痒的功能,其实在之前版本的 Xcode 上就能够通过移花接木的方式曲线实现,而且还能够通过使用私有 API 的方式通过 CoreSVG 直接解析 SVG,不过不推荐这么做,会有被拒的风险。 虽然 Apple 已经在去年给出了 Symbol Image 方案,其本质也就是 SVG Path 的集合,也允许设计人员通过 SF Symbols App 设计自定义 icon,但这个学习成本可能不是每一个设计人员能够达到的。
这个结果其实多少还是挺让人失望的,因为隔壁 Android 早就开始支持 SVG 格式的资源了(虽然是阉割版),而我们内部的 Flutter 项目也早早就使用上了 SVG。 我估计 Apple 是对 SVG 还没有充足的把握,毕竟 SVG 作为一个有着20年历史的古老格式,各种历史兼容性包袱、动画、性能等等,都是不能忽视的问题。Apple 对于自家的 SF Symbols 有着超强的控制力,能够保证 100% 的兼容性和性能,但你能保证你们的 UI 在画 icon 的时候是严格按照规范来实现的吗? 实际的情况可能比你想的要糟。
还是拿我们的 Flutter 项目来说,我们发现大量的 icon 并没有严格按照轮廓来勾勒线条,而是使用了路径叠加等方式实现。这些问题不仅会增大 SVG 文件体积,还会增加矢量数据到位图的转换复杂度。不过根据我们的 Flutter 测试,对于简单的 icon ,目前的主流机型都不存在太大的解析压力。
我罗列了一下 SVG 格式目前可能存在的问题:
参考: