在稿定科技,我们使用 QuickJS 与 Skia 搭建并落地了自研的 App 端编辑器渲染能力。去年北京的 QCon+ 上,笔者为此做了「基于 QuickJS + Skia 的 GUI 框架[1]」分享。下面是一些基于该能力渲染的实际应用截图:
但在短短几个月后,我们就再次升级了这项 QuickJS + Skia 的工程设计,将 Skia 的渲染能力切换到与 Flutter 中的 Dart VM 相集成。本文会介绍这背后的技术演进,共有这么几个部分:
稿定的跨端工程最早始于笔者一项出于业余兴趣的个人实验,即尝试用 QuickJS 结合 libuv 来接入平台 IO 能力,并在此基础上绑定 Skia 来实现 Canvas 渲染。这相当于实现了一套 HTML5 Canvas 标准的子集,效果如下:
skia-quickjs-poc
我们在这一设计的基础上搭建了编辑器的原型,但并未最终落地。其问题主要在于性能,具体可参见这张图:
js-canvas-arch
上图显示了在将 JS 引擎嵌入原生环境后,从点击事件到执行 UI 更新之间的主要环节。其中,JS 的 Canvas 绘制会直接操作 Skia 的 SkBitmap。这一操作虽然已没有线程通信开销,但一旦每帧进行数百次绘制 API 调用(这对命令式的 Canvas 绘制而言很常见),仍然很容易超出 16ms 的限制。这种高频操作时的性能问题,应当也是 React Native 始终不考虑 Canvas 支持的主要原因之一,在其换用无 JIT 的 Hermes 引擎后更是如此。
但是,解释器的性能是足够支撑 DOM 式的 API 的。为此我们直接借用了 Flutter Engine 中的部分源码,不再将 drawImage
这种绘制 API 开放到 JS 层,改为用 C++ Layer 来建模编辑器中的各类元素对象。也可以认为,这是将命令模式 GUI 封装为了保留模式 GUI[2]。每种 Layer 都具备自己的 paint
方法,每帧更新时,只需递归遍历 Layer 执行其 paint
方法即可:
layer-tree
这种 API 设计,使我们较为容易地实现了渲染线程拆分改造。执行交互逻辑的 QuickJS 线程和执行渲染的 Skia 线程独立运作,QuickJS 每次事件回调中提交的更新不再需要被全部绘制,而是只在渲染线程空闲时绘制最新的任务,同时清空任务队列,从而实现避免卡顿的跳帧能力。可以认为这属于经典生产者 - 消费者模式的变体,如下所示:
最终的 JS 版本架构可以分三层概括如下:
虽然上述架构成功支持了业务的初期落地,但它在此过程中也暴露出了一些问题,主要有这么几点:
为此我们需要继续探索解决方案,比如换 Flutter 重写(不是)。
我们首先想到的一条折中路线,是单独抽离 Dart VM,在现有代码库中替代 QuickJS,属于对 VM 的嵌入式集成(embedding)。基于一些工程实验,我们确实搭建出了这一方案的 MVP 原型,具体可参见笔者「自己动手嵌入 Dart VM[4]」这篇专栏。
然而,如果单纯将 QuickJS 换成 Dart VM,并不能解决业务层开发技术栈分歧的问题。而如果引入 Flutter 的 Widget 体系来实现跨平台 UI,这时由于 Flutter 中的 Dart VM 没有对外开放(符号被隐藏),又会存在两份 Dart VM,影响性能和体积。并且,Dart 和 Flutter Engine 存在相当深度的绑定,这种绑定甚至已经深到了「不依赖 Flutter Engine 就无法编译出 Dart VM 的 iOS 和安卓版」的程度。因此抽离 VM 单独使用的工程量相当大,得不偿失。
但还有另一条更彻底的路线,那就是直接在标准 Flutter 环境中接入现有的 C++ 渲染体系,并用同一个 Dart VM 环境控制它。如果基于表层的 Flutter API,这条路线是不可行的。因为 Flutter 默认的 MethodChannel[5] 性质属于 RPC 异步通信,其延迟完全无法达到实时逐帧渲染的需求。但基于 Dart 的 FFI 能力,这一路线最终被证明是可行的,也是我们现在使用的方案。
FFI(Foreign Function Interface[6])意为外部函数接口,它允许我们在一门语言中调用另一门语言中的函数。Dart FFI[7] 为我们提供了直通原生动态库函数符号的能力,可以极大优化调用原生 API 时的性能。它此前长期处于 beta 状态,并在前不久正式随 Flutter 2.0 进入稳定。如果基于该能力来复用 Flutter 中的 Dart VM,那么就可以获得相当简单而统一的应用层技术栈:
上述两者都可以在同一个 Dart Isolate 中完成,从而也省下了 Bridge 通信的开销。为此有这么两项主要的工作需要完成:
首先对于 Skia 离屏上下文的建立过程,其重点可概述如下:
0
作为 ID 即可。makeCurrent
之后,才能开始 Skia 的 GPU 渲染端初始化。总之,Skia 的离屏渲染虽然有跨平台一致的使用层 API,但其上下文创建过程是平台独立的。这具体还可参考 Flutter Engine 中的源码,在此不再赘述。
在具备支持离屏绘制的 Skia 实例后,就可以用 C++ 的 Layer 来绘制它,进而为 Layer 绑定 Dart 对象了。这里实现 Dart 绑定的核心能力,是 Dart FFI 中的 GC Finalizer[10]。它允许为 Dart 对象外挂一个由 void*
指针指向的任意 C++ 对象,并在 Dart 对象被 GC 时,执行用于销毁(析构)该 C++ 对象的回调函数(Finalizer)。其简单示例如下所示:
// 在 Dart 对象被 GC 时执行的回调,可在此销毁附带的 C++ 对象
static void RunFinalizer(void* isolate_callback_data,
Dart_WeakPersistentHandle handle,
void* peer) {
// 将 void* 指针强转为我们需要的类型,然后释放它
auto foo = reinterpret_cast<Foo*>(peer);
delete foo;
}
// 每个 Dart 对象会被表示为一个 handle,在此为其绑定 C++ 对象
DART_EXPORT void PassObjectToCUseDynamicLinking(Dart_Handle h) {
// 在堆上 new 出 C++ 对象
auto foo = new Foo();
// 指定其体积以便垃圾回收器参考,可后续更新该体积
intptr_t size = 2 * 1024 * 1024;
// 用原始 handle 建立可持久存在的 weak persistent handle
// 并关联上析构回调
Dart_NewWeakPersistentHandle_DL(h, foo, size, RunFinalizer);
}
上面的 C++ 可以按这种方式在 Dart 中使用:
// 根据平台加载动态库
final DynamicLibrary nativeLib = Platform.isAndroid
? DynamicLibrary.open('libdemo.so')
: DynamicLibrary.process();
// 在动态库中查找原始函数符号
// 这里的 void Function(Object) 是该函数从 Dart 侧所见的类型
// Void Function(Handle, Pointer<Void>) 是为 FFI 库声明的类型
// FFI 侧的 Handle 类型对应 Dart 侧的 Object 类型
final void Function(Object) _passObjectToC = nativeLib
?.lookup<NativeFunction<Void Function(Handle, Pointer<Void>)>>(
'PassObjectToCUseDynamicLinking')
?.asFunction();
// 对所有需绑定 C++ 对象的 Dart 对象,该基类可供其继承
class BaseObject {
BaseObject() {
// 将 C++ 对象隐式绑定到 Dart 对象实例上
// 从而该 Dart 对象销毁时,也会销毁 C++ 对象
_passObjectToC(this);
}
}
通过这种形式,就可以形成 Dart 对象到 C++ 对象的一对一绑定了。但是,业务中还有可能需要动态获取到这个 C++ 对象。比如在 C++ 中,经常需要将绑定在 Dart Layer 对象上的 C++ 对象拿来 walk 遍历绘制。这时候 void*
指针并不能直接可见,需要在 Dart 对象上显式添加一个指向 C++ 对象的属性,其用 Dart FFI 定义出的类型为 Pointer<Void>
。这个类型对应于 void*
,就像 Dart 中的 Pointer<Int>
对应于 int*
一样。它在 Dart 中不能做任何修改,只能用 C++ 创建并返回。因此我们在实际业务中的方案是这样的:
BaseObject
上,添加一个名为 ptr
的 Pointer<Void>
类型属性。BaseObject
的构造器中,先通过 FFI 调用一个返回 Pointer<Void>
类型指针的 C++ 函数,赋值给 ptr
属性。ptr
属性后,将这个 ptr
和 this
(handle 类型)一起传入上面的 _passObjectToC
,并让其中建立的 C++ 对象持有该 handle。ptr
并强转类型即可。Dart FFI 中
Pointer<Void>
类型和 C++void*
类型的这种一对一映射关系,可以非常有效地帮助我们理解指针。在笔者「写给前端的手动内存管理基础入门(一)[11]」中,也重度应用了这种从类型出发的视角,来帮助前端同学理解原生语言。如果你对 C 系语言还不熟悉,这里推荐一读。
以上代码示例中还有一个值得注意的地方,那就是名为 Dart_NewWeakPersistentHandle_DL
的函数。这是 Dart VM 特别开放的 DL(动态链接)API,只需引入头文件即可使用,无需显式依赖 Dart VM。这类 API 具有 _DL
后缀,可以用来在 C++ 中将普通的 Dart_Handle
转换为具备长生命周期的 Dart_PersistentHandle
、Dart_WeakPersistentHandle
和 Dart_FinalizableHandle
。具体可参见 dart_api_dl.h[12]。
在完成 Dart 对象与 C++ 对象的互通后,还需要实现一些常见的平台 API。这部分内容和 QuickJS 等其他引擎很接近,其实也没有什么别的,大概三件事:
为什么没有 Dart 到 C++ 的异步调用呢?因为这可以通过 1 和 3 的组合来解决,亦即先进行一次 Dart 到 C++ 的同步调用,然后 C++ 异步调用回 Dart。对于 3 的异步调用,需要使用 Port 机制进行异步通信。通过建立 Dart_CObject
的方式,可以从任意线程向 Dart Isolate 发送消息。其具体示例可参见 GitHub Issue[13] 讨论。
对于 Dart FFI 的接入应用,这里列出一些令人印象较为深刻的注意事项:
typedef
缓解。Dart_Handle
可能在连续传递给 C++ 接收时存在重复,需要将它们转为 Dart_WeakPersistentHandle
。JS_Call
),否则应用会立刻崩溃。这里必须使用 Port。textureId
,使其能各自渲染到正确的 Skia 实例中。在完成 Dart FFI 的改造后,还有一项工作是重写已有的 TS 框架到 Dart。这主要是件体力活,只需按照原有代码的字面意义,将 TS 中的逻辑搬运到 Dart 中即可。由于 Dart 不支持 JSON 式的对象字面量语法,因此对于一些形如 {a:{b:{c:1}}}
这样存在嵌套的状态结构,需要将它们逐层拆分为 class,这一点较为繁琐。另外 Dart 的 int
和 double
区分较严格,JSON 转换时应注意相应的类型。除此之外,这部分改造并没有遇到太多值得一提的麻烦。
完成这项迁移后,最后还有一条灵魂的拷问,那就是这样开发技术栈的搭建和切换,是否有「劳民伤财」的折腾之嫌呢?
首先需要明确的是,我们确实需要自己控制 Skia,因为 Flutter 默认缺乏竖排等一些必要的排版能力。如果没有对特殊渲染能力的需求,直接使用 Flutter 自带的 Widget 与 Canvas 是最方便的选择。但只要走通了 Dart FFI,不论是特殊的竖排文字还是更底层的 GL 操作,这些依赖 C++ 库的能力,原理上都已经可以无缝地接入 Dart 了。伴随着 Flutter 2.0 中 Dart FFI 的稳定,我们应当有望见到更多这类「深度嵌入」的混合渲染技术栈。
另外整套方案中,Dart VM 关键的 GC Finalizer 能力,在我们选择 QuickJS 的时间点还没有推出。并且 QuickJS 的 API 非常友好易懂,它的集成为我们培养了从 0 到 1 的入门经验,在项目早期发挥了很大作用。回头看来,这仍然是一条选择从头自研时的必经之路。如果把 Dart VM 比喻成我们吃饱的第四个包子,那么 QuickJS 就是前三个——没有办法只靠吃最后一个就吃饱。但一旦发现更优的路线,个人仍然认为应当(在有条件的前提下)做到尽早切换,避免因技术债而积重难返。
最后在开发成本方面,从最早引入 QuickJS 到现在接入 Dart VM,从 C++ 渲染层到 TS 和 Dart 的编辑器框架,我们对整套基础设施的搭建实际上只有两个人全职投入,再加上一位帮助实现业务层需求的校招同学就足够了。这并不需要大型的 infra 团队,最后搭建出的方案也仍然处于对 Flutter 无侵入性的轻量级。对于有同类场景的中小团队,个人认为本文分享的这套实践应当是务实且具备参考价值的。
在未来,我们希望使原有的 TS 代码库继续在服务端发挥价值。为此赋能的重点之一是笔者正在与 @太狼[14] 合作开发的 @napi-rs/canvas[15] 库。这是一个用 Rust 将 Skia 实现为 Node 扩展的服务端 Canvas 实现,大家不妨期待其后续的进展与分享。至于本文所介绍的框架本身则尚处于内部演化中,暂时尚不开源。另外特别感谢同为国人研发的 Dart Native[16] 项目,它在我们遇到 FFI 问题时提供了重要的帮助。
[1]基于 QuickJS + Skia 的 GUI 框架: https://qconplus.infoq.cn/2020/beijing/presentation/2763
[2]这是将命令模式 GUI 封装为了保留模式 GUI: https://www.zhihu.com/question/39093254/answer/1351958747
[3]My first disappointment with Flutter: https://suragch.medium.com/my-first-disappointment-with-flutter-5f6967ba78bf
[4]自己动手嵌入 Dart VM: https://zhuanlan.zhihu.com/p/296388598
[5]MethodChannel: https://flutter.dev/docs/development/platform-integration/platform-channels
[6]Foreign Function Interface: https://en.wikipedia.org/wiki/Foreign_function_interface
[7]Dart FFI: https://dart.dev/guides/libraries/c-interop
[8]TextureWidget: https://api.flutter.dev/flutter/widgets/Texture-class.html
[9]SkCanvas Creation: https://skia.org/user/api/skcanvas_creation
[10]GC Finalizer: https://github.com/dart-lang/sdk/issues/35770
[11]写给前端的手动内存管理基础入门(一): https://zhuanlan.zhihu.com/p/356214452
[12]dart_api_dl.h: https://github.com/dart-lang/sdk/blob/master/runtime/include/dart_api_dl.h
[13]GitHub Issue: https://github.com/dart-lang/sdk/issues/37022
[14]@太狼: https://www.zhihu.com/people/Broooooklyn
[15]@napi-rs/canvas: https://github.com/Brooooooklyn/canvas
[16]Dart Native: https://github.com/dart-native/dart_native
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/XpgsgRlfQqzJkSP7LvgHUA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。