https://tedaliez.github.io/2019/07/14/CanvasKit%E7%AE%80%E4%BB%8B/
CanvasKit简介
CanvasKit是以WASM为编译目标的Web平台图形绘制接口,其目标是将Skia的图形API导出到Web平台。
从代码提交记录来看,CanvasKit作为了一个Module放置在整个代码仓库中,最早的一次提交记录在2018年9月左右,是一个比较新的codebase
本文简单介绍一下Skia是如何编译为Web平台的,其性能以及未来的可应用场景
编译原理
整个canvaskit模块的代码量非常少:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
.gitignore CHANGELOG.md Makefile WasmAliases.h canvaskit/ 发布代码,canvaskit介绍文档 canvaskit_bindings.cpp compile.sh 编译脚本 cpu.js debug.js externs.js fonts/ 字体资源文件 gpu.js helper.js htmlcanvas/ interface.js karma.bench.conf.js karma.conf.js package.json particles_bindings.cpp perf/ 性能数据 postamble.js preamble.js ready.js release.js serve.py skottie.js skottie_bindings.cpp tests/ 测试代码
|
整个模块我们可以看到其实没有修改包括任何skia的代码文件,只是在编译时指明了skia的源码依赖,同时写了一些胶水代码,从这里可以看出skia迁移至WASM并没有付出很多额外的改造工作。
编译
设置好WASM工具链EmscriptenSDK的环境变量后运行compile.sh就会在out
文件夹中得到canvaskit.js
和canvaskit.wasm
这两个编译产物,这里为了分析选择编译一个debug版本:
debug版本会得到一个未混淆的canvaskit.js,方便我们分析其实现
编译产物浅析
为了快速了解整个模块的情况,直接观察canvaskit.js和canvaskit.wasm文件,先来看下canvaskit.js
js代码量比较大,这里摘取一段最能展示其运行原理的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
|
function makeWebGLContext(canvas, attrs) {
|
代码中出现了大量的WebGL指令和2d的绘制js代码,其实这一块就是EmscriptenSDK对OpenGL的胶水代码(https://emscripten.org/docs/porting/multimedia_and_graphics/OpenGL-support.html), 换言之,canvaskit的绘制代码没有脱离浏览器提供的webgl和context2d的相关接口,毕竟这也是目前在浏览器进行绘制操作的唯一途径
那编译的wasm文件做了啥呢?简单看一下对应wasm的一部分代码, 这也是一个比较庞大的文件,我们只关注一下wasm和js连接的桥梁代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
(import "env" "_eglGetCurrentDisplay" (func $_eglGetCurrentDisplay (result i32))) (import "env" "_eglGetProcAddress" (func $_eglGetProcAddress (param i32) (result i32))) (import "env" "_eglQueryString" (func $_eglQueryString (param i32 i32) (result i32))) (import "env" "_emscripten_glActiveTexture" (func $_emscripten_glActiveTexture (param i32))) (import "env" "_emscripten_glAttachShader" (func $_emscripten_glAttachShader (param i32 i32))) (import "env" "_emscripten_glBeginQueryEXT" (func $_emscripten_glBeginQueryEXT (param i32 i32))) (import "env" "_emscripten_glBindAttribLocation" (func $_emscripten_glBindAttribLocation (param i32 i32 i32))) (import "env" "_emscripten_glBindBuffer" (func $_emscripten_glBindBuffer (param i32 i32))) (import "env" "_emscripten_glBindFramebuffer" (func $_emscripten_glBindFramebuffer (param i32 i32))) (import "env" "_emscripten_glBindRenderbuffer" (func $_emscripten_glBindRenderbuffer (param i32 i32))) (import "env" "_emscripten_glBindTexture" (func $_emscripten_glBindTexture (param i32 i32))) (import "env" "_emscripten_glClear" (func $_emscripten_glClear (param i32))) (import "env" "_emscripten_glClearColor" (func $_emscripten_glClearColor (param f64 f64 f64 f64))) (import "env" "_emscripten_glClearDepthf" (func $_emscripten_glClearDepthf (param f64))) (import "env" "_emscripten_glCompileShader" (func $_emscripten_glCompileShader (param i32))) ...
|
这里省略了一部分,但是仍然可以看出,wasm对绘制的支持全部依赖其运行环境中js注入的函数实现
以这里的_emscripten_glBindTexture
函数为例,对应到js为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
var asmGlobalArg = {}
var asmLibraryArg = { "_emscripten_glBindTexture": _emscripten_glBindTexture
|
GLctx通过代码我们也能找到对应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
createContext:function (canvas, webGLContextAttributes) { var ctx = (canvas.getContext("webgl", webGLContextAttributes) || canvas.getContext("experimental-webgl", webGLContextAttributes)); return ctx && GL.registerContext(ctx, webGLContextAttributes); },registerContext:function (ctx, webGLContextAttributes) { var handle = _malloc(8);
|
所以这里的bindTexture实际上就是WebGL的bindTexture指令(https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bindTexture#Syntax)
分析到这里,我们可以得到一个基本结论: canvaskit中绘制的实现全部在canvaskit.js中调用浏览器绘制API来实现,而计算相关的内容全部放在了wasm中实现
编译脚本解析
通过对编译产物的分析,我们可以发现canvaskit绝大部分的绘制都是借助了Web API中的2d或webgl绘制API来完成的。这里需要分析的是canvaskit如何搭建了skia原生绘制代码和浏览器绘制API的桥梁。
看到compile.sh发现最后一句话涉及到很多canvaskit目录下的文件,因此直接结合编译日志的相关内容分析。
其他的日志都是常规的skia编译命令,只不过执行程序换成了em++而已,em++就是EmscriptenSDK中的编译器命令,可以类比为g++,这些命令会把skia编译为几个静态库
我们略过之前的skia编译命令来到最后一段,这是真正生成WASM产物的地方,其中有大量的逻辑是涉及到canvaskit中的胶水代码的。略去链接, 编译器优化设置, Skia静态库路径的指定, Skia宏定义和头文件路径指定,我们将会得到:
script
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
/Users/JianGuo/VSCodeProject/emsdk/emscripten/1.38.28/em++ \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/debug.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/cpu.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/gpu.js \ --bind \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/preamble.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/helper.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/interface.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/skottie.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/preamble.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/util.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/color.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/font.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/canvas2dcontext.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/htmlcanvas.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/imagedata.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/lineargradient.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/path2d.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/pattern.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/radialgradient.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/htmlcanvas/postamble.js \ --pre-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/postamble.js \ --post-js /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/ready.js \ /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/fonts/NotoMono-Regular.ttf.cpp \ /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/canvaskit_bindings.cpp \ /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/particles_bindings.cpp \ /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/skottie_bindings.cpp \ modules/skottie/utils/SkottieUtils.cpp \ -s ALLOW_MEMORY_GROWTH=1 \ # 允许申请比TOTAL_MEMORY更大的内存 -s EXPORT_NAME=CanvasKitInit \ # js中Module的名字 -s FORCE_FILESYSTEM=0 \ # 开启文件系统支持,用于js中对native的文件系统进行模拟 -s MODULARIZE=1 \ #启用Module的方式生成js,开启后编译的js产物将拥有一个Module作用域,而非全局作用域 -s NO_EXIT_RUNTIME=1 \ # 禁止使用exit函数 -s STRICT=1 \ # 确保编译器不使用弃用语法 -s TOTAL_MEMORY=128MB \ # WASM分配的总内存,如果比此内存更大的场景就需要扩展堆大小 -s USE_FREETYPE=1 \ # 使用emscripten-ports导出的freetype库 -s USE_LIBPNG=1 \ # 使用emscripten-ports导出的libpng库 -s WARN_UNALIGNED=1 \ # 编译时警告未对齐(align) -s USE_WEBGL2=0 \ # 不使用WebGL2 -s WASM=1 \ # 编译为WASM -o out/canvaskit_wasm_debug/canvaskit.js # 指定编译路径
|
其中,pre-js <file>
表示将指定文件的内容插入到生成的js文件前, post-js
表示将指定文件的内容插入到生成的js文件后,我们以skia/modules/canvaskit/htmlcanvas/htmlcanvas.js
为例,看看这些插入的文件都干了啥:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
CanvasKit.MakeCanvas = function(width, height) { var surf = CanvasKit.MakeSurface(width, height); if (surf) { return new HTMLCanvas(surf); } return null; }
function HTMLCanvas(skSurface) { this._surface = skSurface; this._context = new CanvasRenderingContext2D(skSurface.getCanvas()); this._toCleanup = []; this._fontmgr = CanvasKit.SkFontMgr.RefDefault();
|
其实就是对齐了一下浏览器实现,同时对齐了一下Skia内部的接口而已。
最后我们还剩下一段没有分析:
script
1 2 3 4 5 6 7 8 9 10
|
/Users/JianGuo/VSCodeProject/emsdk/emscripten/1.38.28/em++ \ ... --bind \ ... /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/fonts/NotoMono-Regular.ttf.cpp \ /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/canvaskit_bindings.cpp \ /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/particles_bindings.cpp \ /Users/JianGuo/Desktop/skia/skia/modules/canvaskit/skottie_bindings.cpp \ modules/skottie/utils/SkottieUtils.cpp \ ...
|
根据文档,这段命令要求em++以Embind(https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#embind)连接C++代码和JS代码, embind简单来说就是emscriptenSDK提供的将C/C++代码暴露给JavaScript的便捷能力。这里不做重点介绍,我们直接看canvaskit用到的一个代码:
particles_bindings.cpp
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
|
上面代码经过em++编译后会直接将其功能内嵌进wasm文件中。至此,整个编译流程就分析完了
小结
这里用一张图来总结一下整个canvaskit的编译流程, 图中省去了编译器优化和js优化的流程:
![]()
可应用场景
根据官方文档(https://skia.org/user/modules/canvaskit), canvaskit基于skia的API设计向web平台提供了更加方便的图形接口,可以说起到了类似GLWrapper的作用。
得益于Skia本身的其他扩展功能,canvaskit相比于浏览器原生绘制能力,支持了许多更加上层的业务级别功能,例如skia的动画模块skottie(https://skia.org/user/modules/skottie)
Skia中的skottie本身就支持Lottie动画解析和播放,由于Skia良好的跨平台能力,Android和iOS平台现在均可以使用Skia框架来播放Lottie动画,canvaskit则运用WebAssembly的技术来将跨平台的范围扩展到web上,使得web平台可以通过canvaskit的skottie相关接口直接播放lottie动画
对于Web应用而言,canvaskit提供了开发者更加友好的图形接口,并提供了常见的图形概念(例如Bitmap,Path等),减少了上层应用开发者对于绘制接口的理解负担,开发者只需要理解Skia的图形概念即可开发图形界面,有了skia他们也不需要理解复杂的webgl指令。
小结
得益于WASM的理念和EmscriptenSDK的能力,越来越多的native库可以直接导出web上供开发者使用。CanvasKit可以说是C++ Library向Web平台迁移的又一最佳实践。EmscriptenSDK已经做到将Skia这种规模的C++项目以WASM的方式迁移至Web平台,并保证其代码功能的一致性。整个迁移的过程的代价也就是编译工具链的替换和一部分胶水代码。