WebAssembly进阶系列三:微信小程序支持webP的WebAssembly方案

6,400 阅读6分钟

导语:相信不少人听说过 WebAssembly,它是由 Google、Microsoft、Mozilla、Apple 等几家大公司合作发起的一个关于面向Web的通用二进制和文本格式的项目。现在就让我们一步步揭开WebAssembly的神秘面纱,并亲自动手将WebAssembly应用在实际业务中。

1. 引言

通过《WebAssembly进阶系列一:WebAssembly是什么》《WebAssembly进阶系列二:WebAssembly处于编译阶段哪个环节》这两篇文章深入了解WebAssembly的由来、优势及适用场景后,接下来便是实践检验真理的时候,让我们一起动手将WebAssembly应用在微信小程序场景中,让微信小程序环境支持解码webP格式(不了解或没听过webP的各位同仁,请先移步到“探究WebP的一些事儿”)。

2. WebAssembly工作流程

动手之前,让我们先来了解下如何加载和运行WebAssembly的代码:C / C++ / Rust / Java等高级语言开发的代码或功能库 -> Emscripten编译 -> wasm文件 -> 结合WebAssembly JS API -> 浏览器环境中运行,如下图所示:

简单来说,编译前端LLVM / Emscripten流程可以获得wasm文件和胶水js。然后,通过胶水js来加载wasm并转为arrayBuffer格式。紧接着进行编译和实例化后,即可用JavaScript与WebAssembly通信。

详细过程以及每个过程调用的API如下图所示:

3. 浏览器环境支持webP

了解完WebAssembly的工作流程后,是不是还不清楚要从哪开始搞起?你可以去github官网上看一下libwebp开源项目,Google已经完全支持把libwebp源码编译为wasm和asm.js两个版本了。针对不支持WebAssembly的系统或不兼容WebAssembly的浏览器,可以在损失一点性能的情况下降级为使用asm.js。具体编译步骤如下图所示:

待编译完之后,我们便可获得wasm文件和胶水JS。然后,我们可以用“python -m SimpleHTTPServer 8080”启动一个本地服务,在浏览器地址栏输入 http://localhost:8080 后就能看到webP解码后的图片。

最后,让我们来总结下整个流程。

(1)用LLVM / Emscripten / CMake工具对libwebp解码库进行编译,获得wasm文件和胶水JS。

(2)胶水JS申请内存,对wasm文件进行编译、加载和实例化后,导出Module对象。

(3)利用Module对象上的WebpToSDL方法对webP进行解码,并转成Canvas在浏览器渲染显示出来,呈现最终的图片。

4. 微信小程序环境支持webP

微信小程序在Android / iOS上用于执行脚本以及渲染组件的环境都不尽相同。

在Android上,微信小程序逻辑层的JavaScript代码运行在V8中,视图层是由自研XWeb引擎基于Mobile Chrome 67内核来渲染,天然支持webP格式;在iOS上,微信小程序逻辑层的JavaScript代码运行在JavaScriptCore中,视图层是由WKWebView来渲染,宿主Safari浏览器内核不支持webP格式。

通过第3节内容,我们知道浏览器环境已经能够支持webP了,那直接把之前编译好的wasm文件和胶水JS扔进微信小程序的运行环境,然后跑起来不就搞定了?Too young too simple!

浏览器环境支持webP的思路是libwebp解码webP -> jpg / png / gif的canvas图片渲染显示,这已经改变了原来image组件的结构。

而微信小程序提供给开发者的组件不允许去改变它原来的结构,因此换种思路是libwebp解码webP -> jpg / png / gif的rgb data -> jpg / png / gif base64 -> 回传给JS并赋值给image src进行渲染显示。

下面我罗列下从libwebp编译wasm文件和胶水JS开始,直到在微信小程序环境跑通为止,整个过程中遇到的一些坑点和优化点:

(1)编译CMakeLists.txt时需加上“-O3”选项,大大提升编译速度。

(2)编译CMakeLists.txt时需加上“-s USE_PTHREADS=0”选项,因为iOS Safari浏览器不兼容ShareArrayBuffer共享缓冲区。

(3)编译CMakeLists.txt时需加上“-s ALLOW_MEMORY_GROWTH=1”选项,目的是为了解决解码超大分辨率的webP图片时出现的OOM问题。

(4)由于微信小程序环境的兼容性问题,去除胶水JS代码中libwebp编译时加上的SDL相关代码,能节省100KB左右的空间。

(5)去除胶水JS中ENVIRONMENT_IS_NODE / ENVIRONMENT_IS_SHELL相关的代码,因为微信小程序环境并未使用到。

(6)由于iOS Safari浏览器的兼容性问题,将胶水JS中流式编译和实例化的方法去掉,替换成非流式编译和实例化的方法。

(7)由于WebAssembly还没有和<script type='module'>或ES6的import语句集成,因此将wasm文件先转成base64字符串。等胶水JS运行加载逻辑时,再将base64转成ArrayBuffer并编译和实例化后导出Module对象,节省从服务器下载wasm文件的时间。

(8)编译CMakeLists.txt时需加上“-s USE_LIBPNG=1”选项编译libpng.a库,然后将webP解码获得的rgb数据,通过png解码库转成png内存数据,紧接着转成base64回传给JS,最后赋值给image src进行渲染显示。难点是rgb转成png内存数据这一步出了点问题,但是wasm无法调试代码,只能通过搭建libpng的VS工程进行断点调试,最终定位到是rgb转png data时传入的data_size为0导致。

(9)胶水JS里的new WebAssembly.Memory代码在微信小程序环境运行时,会报“refused to create a webassembly object without 'unsafe-eval'”的错误,必须在page-frame.html里的CSP设置里加上unsafe-eval才能解决。

踩了这么多坑之后,终于能在微信小程序环境里支持webP了。实测WebAssembly在解码不同格式不同分辨率的webP时,性能都完胜JavaScript。

5. 写在最后

虽然WebAssembly的解码性能比JavaScript快不少,但遇到超大分辨率(如1920 x 1080等)的webP时,却远远落后于客户端的解码性能。综合对比各种方案的性能和兼容性之后,我们还是采用了基于iOS客户端自定义协议webphttps的方案,大致步骤如下:

(1)首先,微信小程序基础库判断开发者在image组件使用的是webP格式时,则在image src里加上webp头部如webpexample.png

(2)然后,客户端通过NSURLProtocol协议挟持webphttps的请求,并下载相应的webP数据进行解码。

(3)最后,再把解码后的image数据回吐给浏览器进行渲染显示。

到最后,我们完成了微信小程序环境支持webP的方案落地,敬请期待

参考资料

  1. webassembly介绍
  2. 加载和运行WebAssembly代码
  3. WebAssembly在企业邮箱中的一次实践
  4. Download and install — Emscripten 1.38.38 documentation
  5. 探究WebP的一些事儿
  6. libwebp开源项目

个人公众号:前端开发升值记