字节跳动全新框架 Lnyx 的双线程灵感来源—— 探究 react-native-reanimated 的双线程机制

先来一段代码,来看看 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 线程查找到,并在其中插入一些变量。下面分析插件的工作机制。

插件入口

入口主要做了这么两件事:

  1. 添加全局量定义,让这些全局量能够在 UI 线程中进行调用。
  2. 处理 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 和参数添加到队列中。

  1. 使用 queueMicrotask 调度,确保不会阻塞优先级高的操作。
  2. 使用 NativeReanimatedModule.scheduleOnUI 将队列中的所有 worklet 执行。
  3. 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 如何实现其双线程架构。

  1. 自动识别机制:使用 Babel 插件自动识别和转换运行在UI线程的函数。
  2. 线程通信:通过 runOnUI,runOnJS,以及共享内存机制。
  3. 运行时隔离:维护独立的 UI Runtime 和 RN Runtime,通过 _WORKLET 标识区分执行环境。

最后,喜欢本文可以评论,点赞,收藏。

相关推荐
北京_宏哥1 分钟前
《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
java·前端·selenium
Lei活在当下4 分钟前
【尚未完成】【Android架构底层逻辑拆解】Google官方项目NowInAndroid研究(4)数据层的设计和实现之data
架构
Nu115 分钟前
weakMap 和 weakSet 原理
前端·面试
顾林海7 分钟前
深入理解 Dart 函数:从基础到高阶应用
android·前端·flutter
比特鹰11 分钟前
桌面端跨端框架调研
前端·javascript·前端框架
Ratten12 分钟前
【JavaScript】---- JS原生的深拷贝API structuredClone 使用详解与注意事项
前端·javascript
DarisX13 分钟前
JupyterLab前端二开基础上手指南
前端
ZZZzh13 分钟前
前端开发浏览器调试方法
前端
shmily_yy14 分钟前
Ts支持哪些类型和类型运算(上)
前端