先来一段代码,来看看 runOnUI 和 runOnJS 有什么区别。
typescript
import { Button, View, StyleSheet } from 'react-native';
import {
runOnJS,
runOnUI,
useDerivedValue,
useSharedValue,
} from 'react-native-reanimated';
import React from 'react';
export default function App() {
// runOnUI demo
const someWorklet = (number: number) => {
'worklet';
// _WORKLET = true
console.log(_WORKLET, number);
};
const handlePress1 = () => {
runOnUI(someWorklet)(Math.random());
};
// runOnJS demo
const x = useSharedValue(0);
const someFunction = (number: number) => {
// _WORKLET = false
console.log(_WORKLET, number);
};
useDerivedValue(() => {
runOnJS(someFunction)(x.value);
});
const handlePress2 = () => {
x.value = Math.random();
};
return (
<View style={styles.container}>
<Button onPress={handlePress1} title="runOnUI demo" />
<Button onPress={handlePress2} title="runOnJS demo" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
babel 插件识别 UI 线程
通过文档,我们可以知道 RNA 通过 babel 主动去把 UI 线程查找到,并在其中插入一些变量。下面分析插件的工作机制。
插件入口
入口主要做了这么两件事:
- 添加全局量定义,让这些全局量能够在 UI 线程中进行调用。
- 处理 Worklets 调用表达式
typescript
export function initializeGlobals() {
globals = new Set([
// 省略其他
// Reanimated
'_WORKLET',
'_WORKLET_RUNTIME',
'_log',
'_toString',
'_scheduleOnJS',
'_makeShareableClone',
'_runOnUIQueue',
]);
}
module.exports = function (): PluginItem {
return {
pre() {
// 在UI线程中调用的全局函数或值
initializeGlobals();
// 自行添加能够在UI线程中调用的函数或值
// e.g: plugins: [['react-native-reanimated/plugin', { globals: ['myHostFunction'] }]]
addCustomGlobals.call(this);
},
visitor: {
CallExpression: {
enter(path: NodePath<CallExpression>, state: ReanimatedPluginPass) {
// 处理 Worklets 调用表达式
processForCalleesWorklets(path, state);
}
}
}
}
}
Worklet 识别机制
Babel 插件通过以下两种方式识别 worklet:
第一,直接声明:
typescript
function someWorklet() {
'worklet'; // worklet 指令标识
// worklet 代码
}
第二,自动识别:
typescript
// 自动识别特定 hooks
useAnimatedStyle(() => {
// 自动被识别为 worklet
return {
transform: [{ translateX: x.value }]
};
});
// 下面是支持的列表,后面的数字是标明这个函数的哪些参数是需要添加 worklet 的
const functionArgsToWorkletize = new Map([
['useFrameCallback', [0]],
['useAnimatedStyle', [0]],
['useAnimatedProps', [0]],
['createAnimatedPropAdapter', [0]],
['useDerivedValue', [0]],
['useAnimatedScrollHandler', [0]],
['useAnimatedReaction', [0, 1]],
['useWorkletCallback', [0]],
// animations' callbacks
['withTiming', [2]],
['withSpring', [2]],
['withDecay', [1]],
['withRepeat', [3]],
// scheduling functions
['runOnUI', [0]],
]);
babel 处理流程
在 processForCalleesWorklets
中,会对代码中的符合条件的函数进行捕捉并转换。
typescript
export function makeWorklet(
fun: NodePath<WorkletizableFunction>,
state: ReanimatedPluginPass
): FunctionExpression {
// 1. 移除 worklet 指令
removeWorkletDirective(fun);
// 2. 生成代码
const codeObject = generate(fun.node, {
sourceMaps: true,
sourceFileName: state.file.opts.filename
});
// 3. 转换代码
const transformed = transformSync(codeObject.code, {
filename: state.file.opts.filename,
presets: [require.resolve('@babel/preset-typescript')],
plugins: [
// Babel 转换插件
require.resolve('@babel/plugin-transform-shorthand-properties'),
require.resolve('@babel/plugin-transform-arrow-functions'),
// ...其他插件
]
});
// 4. 依赖收集
const variables = makeArrayFromCapturedBindings(transformed.ast, fun);
// 5. 构建 worklet 字符串
const [funString, sourceMapString] = buildWorkletString(
transformed.ast,
variables,
functionName,
transformed.map
);
// 6. 构建新的函数
const statements = [
// 源码主要做了这些定义:
// const func = 原函数
// func.__closure : 函数的外部依赖
// func.__initData : 函数的初始化数据
// func.__workletHash 函数的唯一哈希值
...
];
}
例如这样,转换前的代码:
javascript
function someAnimation() {
'worklet';
return withSpring(1);
}
转换后:
javascript
const _worklet_123_init_data = {
code: "function someAnimation() { return withSpring(1); }",
location: "path/to/your.js",
sourceMap: "...",
version: "2.x.x"
};
(() => {
const someFunc = function() { return withSpring(1); };
someFunc.__closure = [];
someFunc.__initData = _worklet_123_init_data;
someFunc.__workletHash = 123;
return someFunc;
})();
如何实现双线程
从上面的 babel 分析我们知道了在JS端那些跑在 UI 线程的函数的一些实现逻辑。为了窥探双线程机制,我们需要找到一个切入点。那么runOnUI
就很好,接下来,我们来看看 runOnUI
是如何实现的。
JS侧具体实现
先来看看 JS 侧的实现。runOnUI
函数返回一个新的函数,这个函数会将 worklet 和参数添加到队列中。
- 使用
queueMicrotask
调度,确保不会阻塞优先级高的操作。 - 使用
NativeReanimatedModule.scheduleOnUI
将队列中的所有 worklet 执行。 makeShareableCloneRecursive
函数用于把数据放入共享内存里,这样在 UI 线程就能读取到从 JS 线程发送过来的。
看看具体源码:
typescript
const _runOnUIQueue: [Function, any] = [];
export function runOnUI<Args extends unknown[], ReturnValue>(
worklet: WorkletFunction<Args, ReturnValue>
): (...args: Args) => void {
'worklet';
return (...args) => {
// 将worklet和参数添加到队列
_runOnUIQueue.push([worklet, args]);
if (_runOnUIQueue.length === 1) {
// 使用 microtask 调度,确保不会阻塞优先级高的操作
queueMicrotask(() => {
const queue = _runOnUIQueue;
_runOnUIQueue = [];
// 在UI线程执行队列中的所有worklet
NativeReanimatedModule.scheduleOnUI(
makeShareableCloneRecursive(() => {
'worklet';
queue.forEach(([worklet, args]) => {
worklet(...args);
});
global.__callMicrotasks();
})
);
});
}
};
}
调度器
上面的调度函数NativeReanimatedModule.scheduleOnUI
实际上对应的是c++代码中的NativeReanimatedModule.scheduleOnUI
cpp
void NativeReanimatedModule::scheduleOnUI(
jsi::Runtime &rt,
const jsi::Value &worklet
) {
// 从当前运行时提取出来对应的JS端配置后的共享函数
auto shareableWorklet = extractShareableOrThrow<ShareableWorklet>(
rt, worklet, "[Reanimated] Only worklets can be scheduled to run on UI.");
// 具体调度器
uiScheduler_->scheduleOnUI([=] {
// 具体运行时
uiWorkletRuntime_->runGuarded(shareableWorklet);
});
}
这里,uiScheduler_->scheduleOnUI
需要根据不同平台去执行相应的UI线程调度工作, ios 对应REAIOSUIScheduler.scheduleOnUI
,而 Android 则AndroidUIScheduler.scheduleOnUI
(由于我个人对Android不太熟悉,所以下面关于Native部分的代码,仅分析 iOS 的部分)。 REAIOSUIScheduler.scheduleOnUI
的主要功能内容是,判断当前线程,如果是主线程,那就直接执行,如果不是,那么就放入主线程调用。
cpp
void REAIOSUIScheduler::scheduleOnUI(std::function<void()> job) {
if ([NSThread isMainThread]) {
job();
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
job();
});
}
uiWorkletRuntime_.runGuarded
则是通过 UI Runtime 直接执行对应的JS函数。
cpp
template <typename... Args>
inline void runGuarded(
const std::shared_ptr<ShareableWorklet> &shareableWorklet,
Args &&...args
) const {
// 具体的 Runtime 实例,比如 V8, HERMES, JSC 等
jsi::Runtime &rt = *runtime_;
// 取出对应的JS函数
auto function = shareableWorklet->getJSValue(rt);
// 在指定的运行时执行函数
function.asObject(rt).asFunction(rt).call(rt, std::forward<Args>(args)...);
}
下面将详细说明,UI Runtime 和 RN Runtime 都分别做了什么。
UI Runtime 和 RN Runtime
UI Runtime 在 RNA 中的具体类是 WorkletRuntime
,他的实现主要做了这么些事情。 为了处理不同线程下的JS代码,RNA需要使用对应线程下的运行时。通过 ReanimatedRuntime::make
, 这样能够得到的具体对应的引擎运行时。
cpp
std::shared_ptr<jsi::Runtime> ReanimatedRuntime::make(
jsi::Runtime &rnRuntime,
const std::shared_ptr<MessageQueueThread> &jsQueue,
const std::string &name) {
(void)rnRuntime; // used only for V8
#if JS_RUNTIME_HERMES
auto runtime = facebook::hermes::makeHermesRuntime();
return std::make_shared<ReanimatedHermesRuntime>(
std::move(runtime), jsQueue, name);
#elif JS_RUNTIME_V8
auto config = std::make_unique<rnv8::V8RuntimeConfig>();
return rnv8::createSharedV8Runtime(&rnRuntime, std::move(config));
#else
return facebook::jsc::makeJSCRuntime();
#endif
}
调用 WorkletRuntimeDecorator::decorate
给对应的JS引擎增加全局对象。
global._WORKLET = true
global._LABEL = "Reanimated UI runtime"
global._toString
函数global._log
函数global._makeShareableClone
函数global._scheduleOnJS
函数
WorkletRuntime
具体做的事情已经结束。
接下来,为了对已有的 RN Runtime 做出区分,调用 RNRuntimeDecorator::decorate
,给 RN Runtime 增加全局对象。
global._WORKLET = false
global._WORKLET_RUNTIME
= UI Runtime 的具体指针global.__reanimatedModuleProxy
= Native侧的 NativeReanimatedModule 实例
所以,这里就解释了,为什么开头的代码global._WORKLET
在不同的函数里面会不一样。
runOnJS 实现
最后,runOnJS的实现也大概能猜到了,就是调用 UI Runtime 提供的 global._scheduleOnJS
typescript
export function runOnJS<Args extends unknown[], ReturnValue>(
fun:
| ((...args: Args) => ReturnValue)
| RemoteFunction<Args, ReturnValue>
| WorkletFunction<Args, ReturnValue>
): (...args: Args) => void {
'worklet';
if ((fun as FunWorklet).__workletHash) {
return (...args) => runOnJS(runWorkletOnJS<Args, ReturnValue>)(
fun as WorkletFunction<Args, ReturnValue>,
...args
);
}
return (...args) => {
_scheduleOnJS(
fun,
args.length > 0 ? makeShareableCloneOnUIRecursive(args) : undefined
);
};
}
总结
好了到这里就要结束了,本文主要介绍了 React Native Reanimated 如何实现其双线程架构。
- 自动识别机制:使用 Babel 插件自动识别和转换运行在UI线程的函数。
- 线程通信:通过 runOnUI,runOnJS,以及共享内存机制。
- 运行时隔离:维护独立的 UI Runtime 和 RN Runtime,通过 _WORKLET 标识区分执行环境。
最后,喜欢本文可以评论,点赞,收藏。