硬核万字长文:我是如何把Skia的体积“缩小”到1/8的?

作者 | 陈国栋
随着移动互联网的一路高歌 , 越来越多的 App 不满足系统原生的 UI 体系 。开启了各种花式的玩法 。早几年 ReactNative、Weex 等 , 企图尝试让系统组件可以像浏览器一样动态加载 , 从而提高发版本的效率 。更早几年还有一众通过在系统 Webview 基础上面搭建起来的动态化方案 , 包括当下诸多的小程序平台等 。Flutter 的发布仿佛给业界带来一丝新的生机 , 通过 Skia 渲染器完美的保证了在诸多平台渲染的一致性 。但也带来专属于 Flutter 本身的一些问题 。不过多的讨论关于 Flutter 本身 , 这里只谈关于 Skia 和矢量渲染技术中属于我的理解 。
首先要承认我是彻彻底底的标题党 。目前为止我通过官方的编译选项来对 Skia 进行编译裁剪 , 二进制体积依旧很大 。而我的目标就是把 CSS 和排版还有渲染器整体做到 1.5MB 以内 , 如果选用合理小巧的 JS 引擎整体控制在 2MB 到 2.5MB 左右 。
所以如何把 Skia 裁剪到1/8?答案是重写一个(认真严肃)!!!
目前渲染器已经基本完成 , 关键节点的性能测试和 Skia 处于同一水平(甚至还要好一些) 。但是体积只有 Skia 体积(疯狂裁剪后)的 1/8 。
大概是多大?580KB(x86-64 下构建的产物 , Android Armv7a 下还要小许多) 。在这基础上又添加了对复杂文本的排版功能 , 这部分依赖 Freetype(解析字体文件的开源库)和 Harfbuzz(对字模整形的开源库)还有文本的排版引擎 , 带上这部分功能体积会大一些(目前为止 Skia 还不具备复杂文本排版能力) 。
本文希望可以通过简单通俗的语言和大家探讨渲染器背后的核心技术 , 如果你也有类似的需求希望能给到足够的启发 。
关于矢量渲染器
矢量渲染器作为现代 UI 的核心支撑模块 , 常常被作为内嵌在操作系统内的图形子系统的一部分提供给上层开发者 。比如Windows下的GDI/GDI+/Direct2D , Android下的Skia/HWUI(HWUI 对一些复杂多边形的处理依旧依赖 Skia 的软绘制 , 所以不能算完备的矢量渲染器) , MacosX/iOS内置的CoreGraphics , Linux下的Cario 渲染器 。
同样其他的跨平台的库 , 比如 QT 就自己实现了矢量渲染器 , 这样可以在不同平台下拥有统一的渲染效果 。Flutter , Chrome 和 Android 采用同样的 Skia 渲染器来完成跨平台的能力 。
所以要想在不同平台拥有比较好的渲染一致性 , 剥离对系统提供渲染器的依赖是很重要的一步 。
同样行业出现了一些类似于包括NanoVG在内的一些渲染器 , 此类渲染器都采用了模板掩码的一种特殊技法(Opengl 红宝书中提到的)来解决复杂多边形的绘制问题 , 巧妙的规避了复杂的几何运算 。
但是天下没有免费的午餐 , 它同样也会带来相对应的性能问题 。而且天花板很低 , 后续优化几乎无从下手 。对于游戏这类的场景偶尔需要显示一些面板来说无可厚非 , 但是对于传统的界面程序还是显得捉襟见肘 。
前言
在探讨之前我觉得有必要定义一下“渲染”这个词 。这个词在目前互联网技术上面有诸多含义 , 带有一定的迷惑性 。下文所有提及的“渲染”都和计算机图形学中“渲染”拥有同样的含义 , 指的是把特定的像素填充对应的颜色 , 以及围绕这一目的的相关算法 。
鸟瞰渲染器全貌
时至今日Google甚至微软的诸多产品都采用 Skia 作为核心渲染组件 。包括但是不限于 Android、Chrome、Flutter、Xamarin 等等 。不得不说这是一个伟大的技术产品 。
渲染器本身是一个极其复杂的程序 , 就拿 Skia 来说核心侧有超过80w行的代码 。如果算上第三方库甚至达到了惊人的 150w 到 200w 行之巨 。
即使构造的这个轻量的渲染器项目也有超过 25w 行的代码(剔除第三方库 , 比如图片编解码、字体解析、XML 加载库等等 。仍然还有超过 13w 行的核心代码) 。