微信小游戏背后的技术优化,可以让所有小游戏获得更好性能

作者:chrongzhang,腾讯 WXG 客户端开发工程师

这是一篇介绍微信小游戏客户端底层,如果进行优化,可以让所有小游戏获得更好性能的文章 。不是你想像的怎么优化某个小游戏的文章 。来都来了,就了解一下吧:)
小游戏主要分为渲染和逻辑两部分 。渲染优化能让渲染相关的指令(WebGL/GFX)得到更高效的执行,逻辑优化是让除渲染之外的代码也能更高效的执行,本篇主要讲述逻辑相关的优化 。
基础功能优化V8微信小游戏是在 2017 年 12 月 28 日上线的,当时微信Android/ target=_blank class=infotextkey>安卓客户端使用的 V8 版本还是 5.5 。而 google 在 V8 上的迭代速度是很快的,其中一个大的版本变更是从 5.9 版本开始,编译器由原来的 FullCodeGenerator + Crankshaft 变更成更加高效的 Ignition + TurboFan 。
微信小游戏背后的技术优化,可以让所有小游戏获得更好性能

文章插图
mark
V8 引擎之所以性能高,在于其出色的 JIT 执行效率 。JIT 依赖了一个可以在运行时优化代码的动态编译器 。V8 早期的 JIT 编译器是 FullCodegen,后来是 Crankshaft,然后是一直沿用至今的 Turbofan 。
升级 V8,可以获得更高的执行性能(TurboFan)、更快的启动速度(Snapshot + Code Caching)、更低的内存占用(64 位压缩指针) 。小游戏上线至今,客户端使用的 V8 也一直在升级当中,从最初的 5.5,升级到 6.6,然后是 7.6,直到目前的 8.0 。
JSBinding微信小游戏对开发者暴露的是 JS 的接口,开发者调用某些 JS 函数时,最终会调用到客户端底层的原生能力 。而从 JS 到客户端底层之间的桥接能力,就是所谓的 JS 绑定 。JS 绑定又分为两种:裸绑定和非裸绑定 。裸绑定是通过 V8/JAVAScriptCore 提供的原生接口,将某个 JS 函数和原生函数实现绑定到一起,这是最直接,也是最高效的绑定方式 。
非裸绑定是指通过某个 JS 和原生的通信的桥梁(evaluate/prompt/postMessage 等等),在此基础上再封装和转发具体的函数调用 。由于存在中间一层的转发处理,会有额外的消耗 。因此小游戏对外提供的 WebGL 等接口的实现,都采用了裸绑定的方式 。直接用原生裸绑定的 API,又会存在以下问题:
  1. 原生 API 使用较复杂
  2. 不方便实现更高层次的类绑定
  3. V8 和 JavaScriptCore 的 API 差异很大,两个平台需要重复实现绑定
于是,我们实现了一套通用的绑定库: jsbinding,公司内是开源的,未来计划对外也开源 。
具有如下特点:
  1. 简单易用,支持类绑定
  2. 裸绑定,性能高
  3. 同时支持 V8 和 JavaScriptCore
  4. 支持 node addon 绑定实现
未来甚至计划提供 WebAssembly 的绑定实现,是不是还有点小期待呢?
NodeJs/libuv安卓客户端已经全面拥抱 node 。集成 node runtime 后,拥有了如下能力:
  1. node 内置能力(如文件、setTimeout 等)
  2. libuv 异步 IO 处理的能力
node 很多内置能力,是通过原生来实现的(node addon),属于裸绑定,性能较高 。有了 libuv 事件驱动后,可以更加灵活和高效的处理一些异步事件 。比如 WebSocket 的回调,之前的处理流程是,在子线程收到 socket 消息后,将消息内容通过 JNI 调用到 Java 层,Java 层再抛到 JS 线程(也是 JVM 线程),回调到 JS 。而如果使用 libuv,可以在子线程通过 uv_async_send 封装的 ASyncCall 机制,在底层就直接抛到 JS 线程回调到 JS,避免了中间频繁的 JNI 调用和数据传输的开销 。
调用链路优化我们都知道,两点之间,直线最短 。代码也是一样,调用链路越短,越直接,中间的开销就越小 。
JsApi 优化1. JsApi 调用优化首先来看看之前 JsApi 的调用链路:
微信小游戏背后的技术优化,可以让所有小游戏获得更好性能

文章插图
mark
一个 js api 的调用(WeixinJSCore.invokeHandler),首先会调用到 C/C++ 统一的回调函数 voidCallback,然后再通过 JNI 调用到 Java 的统一处理函数 callVoidJavaMethod 。在这个函数里,需要根据 methodID 从 map 中找到对应的 Java Method,然后再通过多次 JNI 调用 J2V8 各种接口将 js api 的参数转换为 Java 类型参数,最后再调用到具体 API 的 Java 实现函数 Invoke 。
这个调用链路显然不是前面提到的裸绑定的实现方法,因为中间还夹了一层 Java 的中转处理层,产生了一些性能消耗 。
针对 invokeHandler,缩短调用链路,减少 JNI 调用优化后,流程如下:
微信小游戏背后的技术优化,可以让所有小游戏获得更好性能

文章插图


推荐阅读