从 react-native-reanimated、微信小程序的 skyline, 到今年字节跳动开源的 Lnyx,实实在在地说明了双线程能优化 JS 开发和原生交互的体验。那么,为了探究如何实现这种双线程的效果,我选择通过 react-native-reanimated 来了解。
以下全部分析均来自于 3.5.4 版本的 react-native-reanimated。
先来一段代码,来看看 runOnUI 和 runOnJS 有什么区别。
typescript
运行后的结果,可以看到他们打印出来的_WORKLET不一样。
babel 插件识别 UI 线程
通过文档,我们可以知道 RNA 通过 babel 查找到 UI 线程的函数,并在其中插入一些变量,RNA把这些函数称作 worklet。所谓 worklet ,在计算机图形学里面表示一段运行在渲染管线里面的函数。而在这里主要指的是在 JS 中定义,然后运行在 UI 线程的函数。
下面分析插件的工作机制。
插件入口
入口主要做了这么两件事:
- 添加全局量定义,让这些全局量能够在 UI 线程中进行调用。
- 处理 worklet 调用表达式。
typescript
Worklet 识别机制
Babel 插件通过以下两种方式识别 worklet。
第一,直接声明:
typescript
第二,自动识别:
typescript
babel 处理流程
在 processForCalleesWorklets 中,会对代码中的符合条件的函数进行捕捉并转换。
typescript
例如这样,转换前的代码:
javascript
转换后:
javascript
如何实现双线程
从上面的 babel 分析我们知道了在JS端那些跑在 UI 线程的函数的一些实现逻辑。为了窥探双线程机制,通过runOnUI就能很好的看到具体的实现流程和作者的思路。接下来,我们来看看 runOnUI 是如何实现的。
JS侧具体实现
先来看看 JS 侧的实现。runOnUI 函数返回一个新的函数,这个函数会将 worklet 和参数添加到队列中。
- 使用
queueMicrotask调度,确保不会阻塞优先级高的操作。 - 使用
NativeReanimatedModule.scheduleOnUI将队列中的所有 worklet 执行。 makeShareableCloneRecursive函数用于把数据放入共享内存里,这样在 UI 线程就能读取到从 JS 线程发送过来的。
看看具体源码:
typescript
调度器
上面的调度函数NativeReanimatedModule.scheduleOnUI实际上对应的是c++代码中的NativeReanimatedModule.scheduleOnUI
cpp
这里,uiScheduler_->scheduleOnUI需要根据不同平台去执行相应的UI线程调度工作,
ios 对应REAIOSUIScheduler.scheduleOnUI,而 Android 则AndroidUIScheduler.scheduleOnUI(由于我个人对Android不太熟悉,所以下面关于Native部分的代码,仅分析 iOS 的部分)。
REAIOSUIScheduler.scheduleOnUI的主要功能内容是,判断当前线程,如果是主线程,那就直接执行,如果不是,那么就放入主线程调用。
cpp
uiWorkletRuntime_.runGuarded 则是通过 UI Runtime 直接执行对应的JS函数。
cpp
下面将详细说明,UI Runtime 和 RN Runtime 都分别做了什么。
UI Runtime 和 RN Runtime
UI Runtime 在 RNA 中的具体类是 WorkletRuntime,他的实现主要做了这么些事情。
- 通过
ReanimatedRuntime::make, 获取UI线程下的运行时。
cpp
- 调用
WorkletRuntimeDecorator::decorate给对应的当前的 JS Runtime 增加全局对象,实际上就是标记其为 UI Runtime。
global._WORKLET = trueglobal._LABEL = "Reanimated UI runtime"global._toString函数global._log函数global._makeShareableClone函数global._scheduleOnJS函数
WorkletRuntime 具体做的事情已经结束。
对应的源码:
cpp
接下来,为了对已有的 RN Runtime 做出区分,调用 RNRuntimeDecorator::decorate,给 RN Runtime 增加全局对象。
global._WORKLET = falseglobal._WORKLET_RUNTIME= UI Runtime 的具体指针global.__reanimatedModuleProxy= Native侧的 NativeReanimatedModule 实例
所以,这里就解释了,为什么开头的代码global._WORKLET在不同的函数里面会不一样。
runOnJS 实现
最后,runOnJS的实现也大概能猜到了,就是调用 UI Runtime 提供的 global._scheduleOnJS
typescript
总结
好了到这里就要结束了,本文主要介绍了 React Native Reanimated 如何实现其双线程架构。
- 自动识别机制:使用 Babel 插件自动识别和转换运行在UI线程的函数。
- 线程通信:通过 runOnUI,runOnJS,以及共享内存机制。
- 运行时隔离:维护独立的 UI Runtime 和 RN Runtime,通过 _WORKLET 标识区分执行环境。
最后,喜欢本文可以评论,点赞,收藏。