XiaoboTalk

对比 RN 和 JS,深入聊聊 Flutter 和 Dart,以及未来的 AI 跨端编程

最近几年我个人的编码领域,好像被有意无意的框进跨端开发中;最早我做 iOS 开发,后来接触了前端开发和 Android 开发,再后来做低码开发、AI D2C 开发,接触和研究了很多和底层 VDOM 相关的开发,有些较为复杂的项目,还涉及到自定义视图树指令集、微型语法解释器相关的开发工作,这些项目都让我学到了很多开发框架上的知识。
最近一年,我工作的主要内容,也是跨端开发:基于 Hippy-Vue 的跨端开发 ,Hippy 是腾讯开源的跨端开发框架,几乎就是对 RN 的复刻,由于 Hippy 的社区不活跃,官方维护的也积极;又迫于工作需要,我几乎阅读了 Hippy 的整个源码。这让我对跨端开发有了很多思考和展望。
这些经历给我一个明显的感受,跨端开发会越来越流行,也越来越成熟了,除了特定领域的开发:例如 Apple Pencil 手写笔记,相机,图形开发等和平台硬件强相关的业务;日常的普通业务开发,使用跨端开发一定是能节省成本的。
另外,随着 AI 热,我个人认为,AI 跨端开发 Runtime 也会慢慢出现。

RN 是如何工作的

RN 主要基于 JS 语言 + JS 引擎,还有一个 RN 实现的宿主环境引擎。RN 自身来自 React 框架,天然有 VDOM,所以 RN 只需额外把这个 VDOM 结构翻译成 Native 真实的视图树即可,最终的 UI 还是被 Native 原生渲染,RN 引擎只负责翻译工作。

编译阶段

将 RN 编译为 JS 包,包是经过压缩的 JS 代码。JS 代码的主要功能,有两部分:
  1. RN runtime core,用于运行时操作 VDOM
  1. Native 包装器和宿主环境相关的全局变量。

运行时的 JS 引擎

RN 的运行时,就是执行编译阶段打包好的 JS 代码文件,这需要一个 JS 引擎,好在 JS 引擎有很多成熟的框架:
  1. V8 引擎,常见于 Chrome 和 Android 系统,都会被系统内置。
  1. JSC 引擎 (JavaScript Core),iOS 系统默认的 JS 引擎。
  1. Hermes,Facebook 开源的独立 JS 引擎。
RN 集成的时候,可以选择使用 Hemers 这个 JS 引擎,从而模型 JS 引擎的环境差异。引擎除了负责执行 JS,还会作为 JSI Bridge 的基座。
JSI Bridge 是 JS 和 C++ 零拷贝通信的桥梁,相比传统的 Bridge (使用 json 序列化和反序列化的通信机制),JSI,可以非常高效让 C++ 和 JS 通信,然后 C++ 再和 Native 通信。总结:
  • JS C++ 引擎 (V8/JSC/Hermes) 和宿主 App 工作在同一进程,所以共享内存空间,彼此通信有一次线程上下文切换开销。
  • JS 引擎在自己的独立线程中,不会干扰到 App 的主 UI 线程。
  • JS 的执行在 JS 引擎线程,JS 调用栈,是 C++ 程序的模拟栈,工作在同一个栈区,可以零成本交互。

Yoga 或者 Titans 负责 Flex 布局求解

前端开发的布局引擎,主流是 Flex 布局,但 UI 最终渲染的时候,需要确定每个元素的绝对定位。将 Flex 布局求解为绝对定位,由引擎内的 Yoga (RN,Facebook开源) 负责,Titans 也是一个 Flex 求解工具库,C++ 实现,由字节开源。

Dom Manager 翻译 VDOM 为客户端视图

这一层也是 C++ 实现,主要将 JS 虚拟 DOM 树,翻译为 Native 视图树,最终的 UI 被 Native 渲染。

其他的宿主 API 实现

这一部分也是依赖 JS引擎,使用 C++ 注入 JS的全局对象,API 等,例如实现 Global 对象,fetch 函数,console.log 等等。主要是为了对齐 Web API,让前端开发的时候,无需关心平台是否支持。

RN 是如何集成到 Native 的

由于历史原因,JS 最初是解释执行,只支持 JIT 编译,虽然后来 JS 引擎接入了 AOT 对热点函数进行字节码编译,这个编译过程也发生在运行时。所以 RN 代码的打包产物,还是一个压缩 JS 源代码文件。把这个文件放如 App 沙盒,App 运行的时候,先启动 RN 引擎 (内部保证 JS 引擎),然后加载沙盒里的 JS 源代码文件,开始 JIT + AOT 混合模式执行。由于这个特性,JS 相较于 Webview 性能较好,但对比 Flutter 和原生性能较差。

Flutter 和 Dart

来到 Flutter 这边,其实整体的原理和 RN 类似,但额外多了一个渲染引擎(Skia impeller),另外就是 Dart 语言,和 Dart VM 天生既支持 JIT,也支持 AOT;这让 Flutter 的集成和工作机制在 Debug 和 Release 有了明显区分。可以说 Dart 语言就是为 Flutter 而设计的。

编译阶段和集成

编译阶段,Flutter 需要支持 hot reload,会将代码编译为 Dart Kenal Snapshot 中间字节码,Debug 阶段的 Flutter 引擎内的 Dart VM 包含 JIT 模块,可以直接解释执行这种中间字节码,实现 hot reload。由于 Flutter 采用的是 Dart VM;不像 JS VM,iOS / Android 并没有内置 Dart VM,所以无论是开发阶段,还是发布阶段,都需要将 Dart VM 一并打包到 App 程序中。

iOS 端为例,Flutter 是如何集成的

最早接触和开发 Flutter 的时候,Flutter 的集成是比较繁琐的,当时工具链还不完善,很多集成工作都需要开发者自己完成,其中两个核心动态库:
  1. Flutter.framework,包含了 Dart VM、渲染引擎、Platform Channel、渲染管线、Dom manager 等。
    1. Debug 模式,Dart VM 需要同时包含 JIT 模块和 AOT 模块
    2. Release 模式,Dart VM 只需要包含 AOT 模块
  1. App.framework,Flutter 引用程序,Dart 编译的产物
    1. Debug 模式:既 Dart Kenal Snapshot 中间字节码 (通过 Flutter 引擎访问本地端口提供),也包含 AOT 执行所需的机器码(App.framework 本身)
    2. Release 模式,只是一个 AOT 机器码,高效执行。
由于需要严格区分 Debug 和 Release,集成开发的时候需要做很多工作,以 iOS / Cocoapods 为例,需要写一个 pod 插件,区分环境引入不同的模块到 pod 子工程。
不过,随着 Flutter 的不断发展,这一问题得到了有效解决,Flutter 提供了很多配套设施,以的Flutter 3.35.7 为例,Flutter 脚手架会直接将上边两个产物打包进应用程序的 app 压缩包:
$ tree Build/Products/Debug-iphonesimulator -L 1 . ├── App.framework ├── Flutter ├── Flutter.framework ├── FlutterIntegrate.app ├── FlutterIntegrate.swiftmodule ├── FlutterPluginRegistrant └── Pods_FlutterIntegrate.framework
然后,继续看 FlutterIntegrate.app 结构内部:
. ├── __preview.dylib ├── _CodeSignature │   └── CodeResources ├── Base.lproj │   ├── LaunchScreen.storyboardc │   └── Main.storyboardc ├── FlutterIntegrate ├── FlutterIntegrate.debug.dylib ├── Frameworks │   ├── App.framework │   └── Flutter.framework ├── Info.plist └── PkgInfo
app bundle 内会直接打进去 Frameworks 动态库,而 pods 工程只是个壳工程,用来告诉 pods 工程,这些路径下是存在动态库的。
$ file Flutter.framework/Flutter Flutter.framework/Flutter: Mach-O universal binary with 1 architecture: [arm64:Mach-O 64-bit dynamically linked shared library arm64] Flutter.framework/Flutter (for architecture arm64): Mach-O 64-bit dynamically linked shared library arm64
此外,Debug 模式下,也可以看到 Dart VM 中是包含 Snapshot 这个 JIT 模块的:
$ nm -gU Flutter.framework/Flutter | grep Dart 0000000001e52e00 S _OBJC_CLASS_$_FlutterDartProject 0000000001e53120 S _OBJC_CLASS_$_FlutterHeadlessDartRunner 0000000001e52e28 S _OBJC_METACLASS_$_FlutterDartProject 0000000001e53148 S _OBJC_METACLASS_$_FlutterHeadlessDartRunner 0000000000a0ce80 S _kDartIsolateSnapshotData 0000000000856220 T _kDartIsolateSnapshotInstructions 0000000001472240 S _kDartVmSnapshotData 0000000000856220 T _kDartVmSnapshotInstructions
另外 Flutter 在开发模式下,也方便了很多,每个 Flutter module 自带了一个壳工程 Runner 工程,可以直接在在 Flutter 工程下,运行:
$ flutter run
就会自动启动 Runner 程序,并保持 Flutter 热更新能力。一个 Flutter Module 工程的目录:
$ tree -L 2 -a . ├── .android │ ├── app │ ├── build.gradle │ ├── Flutter │ ├── gradle │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── include_flutter.groovy │ ├── local.properties │ ├── settings.gradle │ └── src ├── .dart_tool │ ├── flutter_build │ ├── package_config.json │ ├── package_graph.json │ └── version ├── .DS_Store ├── .gitignore ├── .idea │ ├── libraries │ ├── modules.xml │ └── workspace.xml ├── .ios │ ├── .symlinks │ ├── Config │ ├── Flutter │ ├── Runner │ ├── Runner.xcodeproj │ └── Runner.xcworkspace ├── .metadata ├── analysis_options.yaml ├── build │ ├── 892b637c16d52470fb7b14b9d9897587.cache.dill.track.dill │ ├── bc3cffc340e74ebd1dfb723aba8a6d51 │ ├── ios │ └── native_assets ├── lib │ └── main.dart ├── my_flutter_android.iml ├── my_flutter.iml ├── pubspec.lock ├── pubspec.yaml ├── README.md └── test └── widget_test.dart
开发好后,如果想在宿主 APP 集成调试,可以使用 :
$ flutter attach
启动调试,此时宿主 APP 中,打开一个 Flutter 引擎,默认会监听一个本地端口,等待 attach 程序发送 Dart Kenal Snapshot 中间字节码程序,实现 hot reload。

Dart VM 和 JS VM,Flutter 得 1分

对比下来,语言引擎这一层,一个 Dart VM,一个是 JS VM,由于 Dart VM 自身天然支持 AOT,所以,dart 可以直接 AOT 编译为机器码,让 Flutter 运行效率更高。同时,Debug 下,也可以像 JS 那样进行 JIT 执行,实时 Hot Reload,方便了开发和调试。这一点是 Flutter 的一大进步。

Flutter 渲染引擎,Flutter 得 1分

不想 RN ,需要将 VDOM 翻译为 Native 视图,Flutter 通过 Dart 写视图结构,然后一次通过:
  1. Widget Tree
  1. Element Tree
  1. Render Tree
  1. skia 或者 impeller 渲染引擎,渲染 UI
Flutter 可以自产自销 UI,完全独立渲染,整个链路完全自己掌握,UI 渲染的性能上限更高。并且很多基础的业务能力,也能很方便的支持:例如曝光埋点的实现,Flutter 自身在 Dart 侧就能自己判断视图是否曝光;而 RN 由于被翻译为了 Native,需要查询 Native Bridge 来判断。或者额外计算。

Dart FFI 和 JSI 通信

普通的跨端通信,Flutter 提供了 Platform Channel,这是通过通道的方式,序列号反序列化 JSON 实现的通信,相对低效,一些不频繁调用方法,适合使用 Platform Channel,但是需要频繁调用的场景和性能的时候,Flutter 也提供了 FFI 能力。
FFI 和 JSI 本质上都是跨语言交互,原理是一致的,都借助了 C++ 底层的跨平台能力实现。
  • Dart FFI 的本质就是 Dart 线程 ↔ C/C++ Runtime(FlutterEngine 内)零拷贝调用,原理和 JSI 十分相似:
    • Dart VM 是 C++ 实现,提供 native API 可以注册函数、访问内存
    • Dart 对象可通过 VM API 暴露给 C++,C++ 直接操作
理论上 JS 也可以实现 FFI,不过是通过 JSI 的方式提供。而 Flutter 对 FFI 做了很多基础建设,方便开发者使用。Dart VM 的 FFI 提供了 DynamicLibrary / Pointer / struct 映射,实现了语言层面的 FFI 对接,让开发者更快速的使用 FFI。
整体下来,我认为 Flutter 的设计更加彻底,唯一的缺陷是社区不如 RN,RN 源自 React,前端开发人员几乎可以零成本的开发 RN。
不过 Flutter 的能力上线显然更高,给 Flutter 投一票。

AI Runtime 跨端

(未完待续)