RN 的初版架构——通信机制

我们知道 RN 是一个跨语言跨线程的框架,所以要了解 RN 的通信机制,我们有两个问题需要解决:

  1. 如何跨线程?
  2. 如何跨语言?

因为 RN 的初版架构是基于 Android 与 IOS 搭建的,所以我们要先来聊聊这两个系统是如何处理跨线程通信(ITC)的

Android 的跨线程通信

最原始的 Android 跨线程通信(后文简写为 ITC)包含了三个概念:LooperMessageQueueHandler

MessageQueue 可以简单理解为一个代办任务清单,而 Looper 是一个循环机制,每次循环都会从 MessageQueue 中取出任务来执行

Handler 则是负责将代办任务添加进 MessageQueue 中

下面是一个简单的例子:

java 复制代码
 // 获取主线程的 Handler
 Handler mainHandler = new Handler(Looper.getMainLooper());
 
 // 模拟在背景线程中的操作
 new Thread(new Runnable() {
     @Override
     public void run() {
         // 给主线程发送一段可执行程序
         mainHandler.post(new Runnable() {
             @Override
             public void run() {
                 System.out.println("Runnable running on: " +
                         Thread.currentThread().getName());
             }
         });
     }
 }).start();

在 Android 中的 RN 就是基于这三个概念封装了一套平台无关的跨线程通信方法

IOS 的跨线程通信

GCD

IOS 的 ITC 跟 Android 有比较大的差异,主要源于系统中的 GCD(Grand Central Dispatch) APIs

GCD 将跨线程的资源调度与任务分配抽象成了不同的队列(Queue)

默认情况下,IOS 系统会创建 1 个 Main queue 以及 5 个 Global queue

Main queue 是唯一一个与主线程绑定的队列,dispatch_get_main_queue() 方法会返回 Main queue

Global queue 有 5 个,分别代表了系统规划的 5 种 QoS(Quality of Service,代表任务的重要性,高优的任务有优先调度资源的权力),分别为:

  1. userInteractive:优先级最高,代表用户交互的事件,比如触摸事件,动画事件,滚动事件
  2. userInitiated:优先级次高,代表用户发起且希望能够马上获得结果的事件,比如打开一篇文档,加载另一个页面
  3. default:优先级中等,代表默认的事件,如果不知道当前任务要放哪个优先级,就放这个
  4. utility:优先级次低,代表用户不会期望能立刻获得结果的事件,这类事件通常会有一个进度条来表示,比如加载批量文件
  5. background:优先级最低,代表用户不会感知发生的事件,比如内容预加载、清理内存、数据同步

GCD 优雅的地方在于,除了 Main queue 绑定了主线程之外,其他的 Queue 都没有绑定特定线程

开发者只要把任务塞给相对应优先级的 Queue,系统就会帮其分配线程资源来执行任务,这个期间开发者是无感知的(不用分配线程,也不需要销毁资源)

除了上述系统创建的两种队列之外,开发者还可以根据自身需求创建队列,创建队列方法如下:

arduino 复制代码
 /**
 * dispatch_queue_create 用于创建自定义队列
 * label 一个字符串用于标识该队列,用于 debug
 * attr 队列的属性,可以在这里设置队列的 QoS,以及设置队列为 Serial/Concurrent 模式
 **/
 dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);

系统创建的 Main queue 与 Global queue 都是 Serial 模式,Serial 模式下的队列一次只会在一个线程执行一个任务,保证队列中的任务会以 FIFO 的顺序执行

自定义的队列可以手动设置为 Concurrent 模式,改模式下队列一次可能会在多个线程中同步执行多个任务,好处就是能加快队列中任务处理速度,坏处就是无法保证该队列中任务执行完成的顺序

如果要将任务委托给 queue 的时候,可以使用 dispatch 方法:

objc 复制代码
 // 同步方式,会冻结当前 queue 直到 block 中的任务结束
 void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
 ​
 // 异步方式,会直接执行当前 queue 后面的代码,如果执行 block 的 queue 需要返回结果,则再使用一次 dispatch_async 就好
 void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
NSThread

IOS 的 GCD 提供了一种从系统层面调度线程的方案,开发者无法感知也无法控制特定的线程

但是当我们从 RN 的视角看这个方案的时候,情况就有点尴尬了:RN 要求开发者至少能够控制 JS 线程的创建与销毁(这样才能保证 JS 代码始终有环境可以运行)

还好 IOS 提供了一种不用 GCD 控制线程的解决方案:NSThread

NSTread 简单来说就是完全受控于开发者的 IOS 线程,RN 在创建 NSThread 后做了四件事:

  1. 创建了一个永不停止的循环任务(RunLoop)确保线程不会闲置后被清理
  2. RCTMessageThread 类包裹了这个 RunLoop,并对外提供了 runOnQueuerunOnQueueSync 方法来发送可执行代码给 RunLoop 执行(这俩方法底层调用了 IOS 系统提供的 CFRunLoopPerformBlock 方法)
  3. 使用上述方法,让 JSC 引擎在 RunLoop 中创建并完成初始化
  4. 使用上述方法,将打包好的 js 代码交给 JSC 执行

至此,我们获得了一个完全受我们控制的线程,并且完成了运行 js 代码的前置配置

runOnQueuedispatch_async 则为其他线程与 NSThread 的通信提供了底层支持

跨语言通信

了解完 IOS 与 Android 是如何跨线程通信之后,我们来聊聊 RN 是如何跨不同语言来通信的

我们知道 IOS 与 Android 的应用程序分别是使用 Objc 与 Java 来编写的,并且他们通过运行 JS 引擎(由 C++ 编写)来执行 JS 代码

所以,我们可以得到以下四种通信方向(以函数调用为例):

  • Java -> C++ -> JS:Android App 调用 JS 函数
  • Objc -> C++ -> JS:IOS App 调用 JS 函数
  • JS -> C++ -> Java:JS 调用 Android 的 Native module
  • JS -> C++ -> Objc:JS 调用 IOS 的 Native module

再总结一下,我们想要解决 RN 不同语言之间通信,只需要解决以下三个问题:

  1. Java <-> C++:Java 与 C++ 函数如何互相调用
  2. Objc <-> C++:Objc 与 C++ 函数如何互相调用
  3. JS <-> C++:JS 与 C++ 函数如何互相调用
Java <-> C++

关于 Java 与 C++ 之间的通信,Java 社区已经有了解决方案:JNI (Java Native Interface)

JNI 是由 JVM(Java 虚拟机)提供的一系列 C 接口 API 组成的机制

C 系列语言(包括 C++)只需要通过调用这些 API(比如 FindClass, CallVoidMethod 等)就可以调用 Java 的方法

反之,Java 也可以调用一些 C++ 侧通过 JNI 注册的一些方法

JNI 甚至允许 Java 与 C++ 同时持有对同一块内存的引用

所以这个问题被完美解决了

Objc <-> C++

关于 Objc 与 C++ 之间的通信也有现成的解决方案:Objective-C++

Objective-C++(.mm 文件)允许在同一个文件中混写 C++ 与 Objc 代码(包括类型与方法等)也能调用一些 C API(比如上述 dispatch_queue_t

当然,持有同一份引用以及互相调用方法也不在话下

这一切要归功于 Objc 的编译器,它支持将 .mm 文件中的 C++ 与 Objc 编译为同一份目标代码

所以这个问题也被完美解决啦

JS <-> C++

最后,我们来聊聊最关键的一环,也就是 Js 与 C++ 之间的通信

我们以函数调用为例,来分别聊聊这两者是如何互相调用对方的函数的

一般来说,要跨语言调用对方函数,有两个问题需要克服:

  1. 调用方式:要么取得对方函数的引用,要么将意图封装成某种 DSL(Domain specific language) 并让对方解析
  2. 参数类型转换:将调用方的调用参数转换成被调用方的类型以供被调用方执行

首先是调用方式:

我们知道 JS 是运行在由 C++ 编写而成的引擎中的(在 RN 初版架构的例子中,这个引擎是 JSCore)

JSCore 除了提供了一个运行 JS 的环境之外,它还对外暴露了一系列的 C API 来让开发者控制它的部份能力

RN 就是使用了其中的 JSGlobalContextCreateInGroup 来创建了一个 context

context 代表了 JS 执行的上下文,当 JSCore 完成了一个 context 的创建,我们可以认为他完成了运行 JS 代码前的准备工作

除此之外,RN 还调用了 JSContextGetGlobalObject 来通过 context 获取 JS Global 对象的引用

有了 Global 的引用,C++ 侧就有了在任意时候读取 JS 放在 Global 对象上的属性/方法的能力

其次是类型转换:

类型转换分成两个情况:

  • JS 调用 C++ 方法时,我们需要将 JS 的参数类型转换成 C++ 的类型
  • 如果 C++ 调用 JS 方法时,我们则需要将 C++ 的类型转换成 JS 类型
cpp 复制代码
 // in JSCExecutor.cpp
 ​
 // JS 调用 C++ 方法
 void JSCExecutor::flushQueueImmediate(Value&& queue) {
   // 将 JS 类型的 queue 通过 JSON 转换成 C++ 的 folly::dynamic 类型
   auto queueStr = queue.toJSONString();
   m_delegate->callNativeModules(*this, folly::parseJson(queueStr), false);
 }
 ​
 // C++ 调用 JS 方法
 void JSCExecutor::callFunction(
     const std::string& moduleId,
     const std::string& methodId,
     const folly::dynamic& arguments) {
   // ... 略过部份代码
   
   // 将 C++ 的 string,folly::dynamic 类型转换成 JS 的类型
   m_callFunctionReturnFlushedQueueJS->callAsFunction(
           {Value(m_context, String::createExpectingAscii(m_context, moduleId)),
            Value(m_context, String::createExpectingAscii(m_context, methodId)),
            Value::fromDynamic(m_context, std::move(arguments))});
   
   // ... 略过部份代码
 }

让我们先看看 JS 调用 C++ 方法的部份,可以看到 RN 选择了先将 JS 的类型转换成了 JSON,再转换成 C++ 的类型,那么问题来了,为什么要这么麻烦呢?为什么不直接转换呢?

个人认为有两个因素:

  1. RN 在真机上运行的时候,是否可以直接转换类型呢?其实答案是可以的,但是会非常局限:JSCore 提供的公开 API 中确实有类似于 JSValueToBooleanJSValueToNumber 能将原始类型转换成对应 C++ 类型的方法,但是面对 String,Object 类型的时候就有点力不从心了
  2. 在聊 JS 线程的时候,我们有聊到 RN 支持在 chrome 上调试 js 代码,在这个场景下 JS 跟 Native 的沟通主要依赖 ws 的连接,在这种情况下,我们只能把数据通过 JSON 进行传送,这个是无法绕过的,再进一步考虑到 dev/prod 环境下的表现一致性,JSON 是权衡之下的较优解

当然,RN 也在这个背景下做了些优化,queue 中其实并不是一个方法调用,而是类似一个先进先出的栈,里面存放了多个调用,最大程度的减少 JSON 带来的性能损耗

接下来我们来看看 C++ 调用 JS 方法的部份,这个部份主要用了两个类型转换的方法:String::createExpectingAscii, Value::fromDynamic,我们来看看他们是怎么实现的:

cpp 复制代码
 // in Value.h
 static String createExpectingAscii(JSContextRef context, const char* ascii, size_t len) {
   #if WITH_FBJSCEXTENSIONS
       return String(context, JSC_JSStringCreateWithUTF8CStringExpectAscii(context, ascii, len), true);
   #else
       return String(context, JSC_JSStringCreateWithUTF8CString(context, ascii), true);
   #endif
 }
 ​
 // in Value.cpp
 Value Value::fromDynamic(JSContextRef ctx, const folly::dynamic& value) {
   #if USE_FAST_FOLLY_DYNAMIC_CONVERSION
     JSDeferredGCRef deferGC = JSDeferGarbageCollection(ctx);
     JSLock(ctx);
     JSValueRef jsVal = Value::fromDynamicInner(ctx, value);
     JSUnlock(ctx);
     JSResumeGarbageCollection(ctx, deferGC);
     return Value(ctx, jsVal);
   #else
     auto json = folly::toJson(value);
     return fromJSON(String(ctx, json.c_str()));
   #endif
 }

非常有意思的是,这两个方法都用了两套实现

还记得我们之前聊 JS thread 的时候提到的 android-jsc 吗?WITH_FBJSCEXTENSIONSUSE_FAST_FOLLY_DYNAMIC_CONVERSION 中的实现是专门为了它写的,不像在 ios 中的 jsc,RN 团队可以完全控制 Android 的 jsc,所以也可以用一些比较 "野" 的写法

先来看看 createExpectingAscii 这个方法的两个实现,区别在于在 Android 中用了 JSStringCreateWithUTF8CStringExpectAscii ,在 IOS 中用了 JSStringCreateWithUTF8CString

这两个方法的区别在于,前者保证传进去的参数是 ascii 编码的字符,所以当 JSC 在转换类型的时候,只需要考虑 7-bit 的 ascii 编码就好,速度会比需要考虑 UTF-8 的后者快,而 createExpectingAscii 方法传入的 ascii 参数是类似于 AppRegistry 之类的字符,且完全受 RN 所控制,所以能保证编码类型;至于为什么 IOS 不能用前者,自然是 Apple 没有公开这个 API 啦~

再来看看 fromDynamic 这个方法,Android 中的实现(if 的部份)做了三件事:

  1. 暂停了 JSC 的垃圾回收 GC:这个目的是为了防止转换类型的中间产物被 GC 回收导致不可控的错误
  2. JSLock 锁住了当前的 context(这里有个小知识点,由于 JSC 是单线程的,为了防止多个线程同时调用其 API,JSC 会把大部分的 API 调用前后锁住,以免发生资源争夺的情况,由于 fromDynamicInner 内部会调用大量的 JSC API,所以这里提前锁上了避免大量额的加锁解锁资源损耗)
  3. 使用 fromDynamicInner 转换类型:
cpp 复制代码
 JSValueRef Value::fromDynamicInner(JSContextRef ctx, const folly::dynamic& obj) {
   switch (obj.type()) {
     // For primitive types (and strings), just create and return an equivalent JSValue
     case folly::dynamic::Type::NULLT:
       return JSC_JSValueMakeNull(ctx);
 ​
     case folly::dynamic::Type::BOOL:
       return JSC_JSValueMakeBoolean(ctx, obj.getBool());
 ​
     case folly::dynamic::Type::DOUBLE:
       return JSC_JSValueMakeNumber(ctx, obj.getDouble());
 ​
     case folly::dynamic::Type::INT64:
       return JSC_JSValueMakeNumber(ctx, obj.asDouble());
 ​
     case folly::dynamic::Type::STRING:
       return JSC_JSValueMakeString(ctx, String(ctx, obj.getString().c_str()));
 ​
     case folly::dynamic::Type::ARRAY: {
       // Collect JSValue for every element in the array
       JSValueRef vals[obj.size()];
       for (size_t i = 0; i < obj.size(); ++i) {
         vals[i] = fromDynamicInner(ctx, obj[i]);
       }
       // Create a JSArray with the values
       JSValueRef arr = JSC_JSObjectMakeArray(ctx, obj.size(), vals, nullptr);
       return arr;
     }
 ​
     case folly::dynamic::Type::OBJECT: {
       // Create an empty object
       JSObjectRef jsObj = JSC_JSObjectMake(ctx, nullptr, nullptr);
       // Create a JSValue for each of the object's children and set them in the object
       for (auto it = obj.items().begin(); it != obj.items().end(); ++it) {
         JSC_JSObjectSetProperty(
           ctx,
           jsObj,
           String(ctx, it->first.asString().c_str()),
           fromDynamicInner(ctx, it->second),
           kJSPropertyAttributeNone,
           nullptr);
       }
       return jsObj;
     }
     default:
       // Assert not reached
       LOG(FATAL) << "Trying to convert a folly object of unsupported type.";
       return JSC_JSValueMakeNull(ctx);
   }
 }

至于为什么 IOS 用的是 JSON 转换呢~当然是因为阻止 GC 以及 JSLock 的 API 在 IOS 中并不是公开的 API 啦~

至此!我们讲完了 RN 跨语言通信的一些基础机制与准备,接下来我们看看 Bridge 是如何利用这些构建连接的桥梁

Bridge in Native

接下来我们来看看 bridge 做了什么,以及它是如何与其他模块交互的

首先我们需要介绍三个重要角色:

  • Instance:代码实现在 Instance.cpp

    • 可以看成 RN app 的大管家,它接受原生程序的委托,主要负责管理 Native module 注册、初始化 Bridge、加载 JS bundle 等事务,同时它也是原生程序调用 JS 方法的唯一入口
  • bridge:代码实现在 NativeToJsBridge.cpp

    • bridge 由两个部分组成:NativeToJsBridgeJsToNativeBridge
    • 前者主要把 Instance 的委托的加载 JS bundle 以及调用 JS 方法的任务传递给 JSCExecutor(此外它还肩负着委托 JSCExecutor 启动 JS 引擎的重责大任)
    • 后者则是负责处理从 JS 传来的 Native module 调用请求
  • JSCExecutor:代码实现在 JSCExecutor.cpp

    • 接受 NativeToJsBridge 的请求启动 JS 引擎(用 JSGlobalContextCreateInGroup 创建 JS context)
    • 负责消费在 JSC 中加载 JS bundle 的任务
    • 往 JS global 对象上挂方法/读取 JS global 对象上的方法,为相互调用做准备
    • 负责将从 bridge 传来的调用 JS 方法的请求通过 global 对象传递到 JS 侧
    • 负责将从 JS 侧传来的调用 Native module 的请求传递给 bridge

三个角色的关系如图所示:

从图中可以看到,RN 在代码结构上大致分了三层,Instance 之下是原生代码写的 APP 的壳(包括但不限于 APP 启动的原生代码,Native modules 的原生代码实现等)这个部份是平台区分的

Instance 开始到 Bridge 再到 JSCExecutor,这三个部份都是通过 C++ 语言实现,这个部份提供了跨平台的实现(同一套代码运行在不同的原生平台上)

JSCExecutor 之上,就是 JS 的领域了,JS 跟 JSCExecutor 主要是通过 global 对象进行通信,上述类型转换的代码也大多被封装到 JSCExecutor 的实现中

从原生代码或者 Native module 的角度来看,调用 JS 方法只需要调用 Instance 提供的 callJSFunction 后面的流程他们不需要感知

从 JS 的角度来看,调用原生方法则只需要调用 global 对象上的 nativeFlushQueueImmediate 方法,后面的流程也无需感知了

其实到这里 RN 的通信机制也讲的差不多了,后面我写了一点关于 JSCExecutor 跟 JS 互相调用的核心代码解析,有兴趣的可以看看

JS <-> C++ 互相调用核心代码解析

  • 首先是 C++ 调用 JS 函数:

我们来看看 JS 侧,为了能够让 C++ 顺利调用我们的方法,我们需要往 Global 上挂点东西:

typescript 复制代码
 // in MessageQueue.js
 class MessageQueue {
     // ...
     callFunctionReturnFlushedQueue(module: string, method: string, args: any[]) {
         const moduleMethods = this.getCallableModule(module);
         moduleMethods[method].apply(moduleMethods, args);
         return this.flushedQueue();
   }
 }
 ​
 // in BatchedBridge.js
 const BatchedBridge = new MessageQueue();
 Object.defineProperty(global, '__fbBatchedBridge', {
   configurable: true,
   value: BatchedBridge,
 });

这两个文件在 JS 侧提供了给 C++ 调用特定函数的途径(为了方便讲解我简化了调用,但是方法名不变)

首先是 MessageQueuecallFunctionReturnFlushedQueue 方法,它会根据 module 去查找是否有可被调用的 module,如果有,则将其初始化后返回,最后使用 apply 调用其方法(flushedQueue 我们会在后续的布局与绘制篇中讲解)

接着是 BatchedBridge,它将上述的 MessageQueue 作为 __fbBatchedBridge 的值挂上了 globa 对象(configurable 值设为 true 是为了方便框架测试)

让我们再看回 C++ 侧,有了 JS 的 global 之后,我们可以通过以下方式调用 JS 方法:

cpp 复制代码
 // 根据 context 获得 global 对象引用
 auto global = Object::getGlobalObject(m_context);
 // 从 global 对象中获取 __fbBatchedBridge 属性得到 MessageQueue 的实例
 auto batchedBridgeValue = global.getProperty("__fbBatchedBridge");
 auto batchedBridge = batchedBridgeValue.asObject();
 // 从 MessageQueue 实例中取得 callFunctionReturnFlushedQueue 方法的引用
 m_callFunctionReturnFlushedQueueJS = batchedBridge.getProperty("callFunctionReturnFlushedQueue").asObject();
 // 调用 JSC 的 callAsFunction api,传入对应 moduleId,methodId,arguments 以调用 callFunctionReturnFlushedQueue 方法
 m_callFunctionReturnFlushedQueueJS->callAsFunction({
     Value(m_context, String::createExpectingAscii(m_context, moduleId)),
     Value(m_context, String::createExpectingAscii(m_context, methodId)),
     Value::fromDynamic(m_context, std::move(arguments))
 })

值得注意的是,由于 C++ 与 JS 的类型是不同的,所以在 callAsFunction 中,C++ 需要先进行类型转换

其中 moduleIdmethodId 都是固定的 String 类型,可以直接转换,但是 arguments 类型不固定,所以 RN 在 Value::fromDynamic 方法中采用了最简单粗暴的方法:序列化与反序列化

cpp 复制代码
 Value Value::fromDynamic(JSContextRef ctx, const folly::dynamic& value) {
   auto json = folly::toJson(value);
   return fromJSON(String(ctx, json.c_str()));
 }

至此我们完成了所有 C++ -> JS 的函数调用准备工作

  • 接下来我们来看看 JS 是如何调用 C++ 函数的:

与 C++ 调用 JS 函数类似,只不过这次变成了 C++ 往 Object 上挂东西:

cpp 复制代码
 // in JSCExecutor.cpp
 ​
 /** 
 * installNativeHook 主要用于将特定 C++ 方法的钩子安到 global 对象中
 * template 是 C++ 的语法,他会接收符合对应返回值(JSValueRef)与参数(size_t, const JSValueRef[])的 JSCExecutor 实例方法并将其放到 installNativeHook 中(在这个例子里,他会将其放进 exceptionWrapMethod 这个方法的 template 中)
 **/
 template <JSValueRef (JSCExecutor::*method)(size_t, const JSValueRef[])>
 void JSCExecutor::installNativeHook(const char* name) {
   installGlobalFunction(m_context, name, exceptionWrapMethod<method>());
 }
 ​
 ​
 /**
 * exceptionWrapMethod 主要职责是调用传入的 method,并且处理其的 Error
 **/
 template <JSValueRef (
     JSCExecutor::*method)(JSObjectRef object, JSStringRef propertyName)>
 inline JSObjectGetPropertyCallback exceptionWrapMethod() {
   struct funcWrapper {
     static JSValueRef call(
         JSContextRef ctx,
         JSObjectRef object,
         JSStringRef propertyName,
         JSValueRef* exception) {
       try {
         auto executor = Object::getGlobalObject(ctx).getPrivate<JSCExecutor>();
         if (executor &&
             executor->getJavaScriptContext()) { // Executor not invalidated
           // 核心代码,执行对应的 C++ 方法
           return (executor->*method)(object, propertyName);
         }
       } catch (...) {
         *exception = translatePendingCppExceptionToJSError(ctx, object);
       }
       return Value::makeUndefined(ctx);
     }
   };
   // 返回对于 call 方法的引用,这个 call 方法会执行传入的 method 方法并返回其返回值
   return &funcWrapper::call;
 }
 ​
 /**
 * installGlobalFunction 主要职责是将上述的 call 方法放进 JS 的 global 对象中,以便后续让 JS 调用
 **/
 void installGlobalFunction(
     JSGlobalContextRef ctx,
     const char* name,
     JSObjectCallAsFunctionCallback callback) {
   String jsName(ctx, name);
   JSObjectRef functionObj = JSC_JSObjectMakeFunctionWithCallback(
     ctx, jsName, callback);
   Object::getGlobalObject(ctx).setProperty(jsName, Value(ctx, functionObj));
 }
 ​
 // 最后 C++ 调用上面的所有方法把 nativeFlushQueueImmediate 方法放进了 global 对象中
 installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate");

从上述代码中,我们能看到 C++ 侧成功的将 nativeFlushQueueImmediate 方法放进了 global 对象中

接下来我们来看看 JS 是如何使用这个方法的:

js 复制代码
 // in MessageQueue.js
 
 class MessageQueue {
     // ...
     // JS 侧调用 C++ 方法的入口,为了简化我删除了部份代码(包括开发模式、性能打点、注册回调函数等等),有兴趣的可以自己去看看:https://github.com/facebook/react-native/blob/v0.57.0/Libraries/BatchedBridge/MessageQueue.js#L168
     enqueueNativeCall(
     moduleID: number,
     methodID: number,
     params: any[],
     ){
         // 将当前要调用的方法放入队列中
         this._queue[MODULE_IDS].push(moduleID);
         this._queue[METHOD_IDS].push(methodID);
         this._queue[PARAMS].push(params);
 ​
         const now = new Date().getTime();
         // 如果符合清空队列的条件,则一股脑将当前队列全部发给 C++
         if (global.nativeFlushQueueImmediate &&
         now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS
         ) {
             const queue = this._queue;
             this._queue = [[], [], [], this._callID];
             this._lastFlush = now;
             // 调用 global 中 C++ 的方法
             global.nativeFlushQueueImmediate(queue);
         }
     }
 }

JS 传入 nativeFlushQueueImmediate 方法的是一个队列,它的类型是:_queue: [number[], number[], any[], number];

  • 第一个元素是一个数字数组,代表需要调用的 native module 的 id
  • 第二个元素也是一个数字数组,代表需要调用的方法的 id
  • 第三个元素也是数组,代表调用方法的参数
  • 最后一个元素是一个数字,唯一标识了当前的调用 id
  • 如果我们需要从 queue 中取出方法调用,我们只需要对前三个元素取出同一下标的元素即可
scss 复制代码
 // 举个例子
 module = _queue[0][0][0]
 method = _queue[0][1][0]
 args = _queue[0][2][0]
 module[method](...args)

最后,我们来看看 C++ 是如何处理这个 queue 以及处理 JS -> C++ 的类型转换的:

cpp 复制代码
 // in JSCExecutor.cpp
 
 JSValueRef JSCExecutor::nativeFlushQueueImmediate(
     size_t argumentCount,
     const JSValueRef arguments[]) {
         Value queue = Value(m_context, arguments[0])
         // 序列化 queue
         auto queueStr = queue.toJSONString();
       // 反序列化 queue 得到 folly:dynamic
         m_delegate->callNativeModules(*this, folly::parseJson(queueStr), false);
         return Value::makeUndefined(m_context);
 }

可以看到,RN 使用了序列化-反序列化 的操作将 JS 的 Value 类型转换成了可以在 C++ 中使用的 folly:dynamic 类型

这个方法无疑简单粗暴的解决了类型转换的问题,但同时也增加了 JS 与 C++ 方法互相调用的性能开销,考虑到这条链路的触发频率,这个部份有较大概率会成为 RN 的性能瓶颈,所以才有了后来用 JSI 取代序列化-反序列化的新架构(这个我们后面聊)

相关推荐
Swift社区4 小时前
从 0 到 1 构建一个完整的 AGUI 前端项目的流程在 ESP32 上运行
前端·算法·职场和发展
一只小风华~5 小时前
学习笔记:Vue Router 中的链接匹配机制与样式控制
前端·javascript·vue.js·笔记·学习·ecmascript
Jerry_Rod5 小时前
react+umijs 项目快速学习
前端·react.js
京东云开发者5 小时前
浅析cef在win和mac上的适配
前端
uhakadotcom5 小时前
在chrome浏览器插件之中,options.html和options.js常用来做什么事情
前端·javascript·面试
西瓜树枝5 小时前
Chrome 扩展开发从入门到实践:以 Cookie 跨页提取工具为例,拆解核心模块与交互逻辑
前端·javascript·chrome
gplitems1235 小时前
Download:Blaxcut - Barbershop & Hair Salon WordPress Theme
前端
拜无忧5 小时前
【DEMO】互动信息墙 - 无限流动版-点击放大
前端