阅读 685

RN 通信原理(for 前端)

随着后起之秀 Flutter 的崛起,RN 渐渐失去光环。虽然有一天 RN 可能会退出历史的舞台,但它带来 JavaScript 与 Native 交互的思想依然会流传下去。

网上关于 RN 通信原理的文章,几乎都是站在客户端的角度来讲解,这篇文章想站在前端的角度聊一聊,JS 与 Native 是如何交互的。

如果您想阅读 RN 的源码,不建议选择最新的版本,新版本对部分底层代码采用 C++ 进行了重写,阅读和调试体验都不是很好。而在通信方式上其实没有多大变化。

我阅读的 RN 版本是 0.43.4,基于该版本写了一个可运行的 Demo,仅包含 JavaScript 和 Objective-C 通信的核心部分,只有几百行代码。

JS call Native

JS 端通信部分的核心代码不到两百行,主要由 NativeModules.jsMessageQueue.jsBatchedBridge.js三个文件构成。其中 BatchedBridge 是 MessageQueue 实例化的对象。

我把它们都放在 Demo 工程的Arch.js文件中。BatchedBridge 对象提供了 JS 触发 Native 调用的方法:

var enqueueNativeCall = function(moduleID, methodID, params, onFail, onSuccess) {
    if (onFail || onSuccess) {
        // 如果存在 callback 回调,添加到 callbacks 字典中
        // OC 根据 callbackID 来执行回调
        if (onFail) {
            params.push(this.callbackID);
            this.callbacks[this.callbackID++] = onFail;
        }
        if (onSuccess) {
            params.push(this.callbackID);
            this.callbacks[this.callbackID++] = onSuccess;
        }
    }   
        // 将 Native 调用存入消息队列中
        this.queue[MODULE_INDEX].push(moduleID);
        this.queue[METHOD_INDEX].push(methodID);
        this.queue[PARAMS].push(params);

        // 每次都有 ID
        this.callID++;

        const now = new Date().getTime();
        // 检测原生端是否为 global 添加过 nativeFlushQueueImmediate 方法
        // 如果有这个方法,并且 5ms 内队列还有未处理的调用,就主动调用 nativeFlushQueueImmediate 触发 Native 调用
        if (global.nativeFlushQueueImmediate && now - this.lastFlush > MIN_TIME_BETWEEN_FLUSHES_MS) {
            global.nativeFlushQueueImmediate(this.queue);
            // 调用后清空队列
            this.queue = [[], [], [], this.callID];
        }
}
复制代码

该函数将调用 Native 实例模块 ID,方法 ID,以及回调 ID 分别存入三个队列中,this.queue 持有这三个队列。

if (global.nativeFlushQueueImmediate && now - this.lastFlush > MIN_TIME_BETWEEN_FLUSHES_MS) {
    global.nativeFlushQueueImmediate(this.queue);
    // 调用后清空队列
    this.queue = [[], [], [], this.callID];
}
复制代码

这段代码是 JS 主动触发 Native 调用的关键所在,它有一个条件当 now - this.lasFlush > 5ms时才执行。也就是队列上一次被清空的时间如果已经超过 5ms 就执行nativeFlushQueueImmediate函数。

在下一节 Native call JS 我们将会讲到每次 Native 调用 JS 时,会将 queue 作为返回值传给 Native 执行,假设没有在 5ms 内没有 Native call JS,那么 JS call Native 都得不到执行。

所以这里设定了一个 5ms 的门限,如果在这段时间内,没有 Native 调用 JS,JS 就会主动触发 Native 调用。

在 global 的 nativeFlushQueueImmediate函数体,是在原生端实现的。执行时会触发原生端 block 的调用,并传入参数 queue,触发 native 调用。

self->_context[@"nativeFlushQueueImmediate"] = ^(NSArray<NSArray *> *calls) {
    AHJSExecutor *strongSelf = weakSelf;
    [strongSelf->_bridge handleBuffer:calls];
};
复制代码

现在的问题是,JS 如何知道 Native 有哪些方法可以调用的呢?是 Native 在开始执行 JS 代码前,提前注入到 JS 环境的,保存在 global 的__batchedBridgeConfig属性中。

它包含所有支持 JS 调用的模块和方法,同时会区分每个方法是同步、异步、还是 Promise,这些信息在 Native 初始化时会提供。

每个模块和方法,都会关联一个 ID,这个 ID 其实是模块和方法在各自列表中所处的下标。发起调用时,JS 端将 ID 存入消息队列,Native 拿到 ID,在 Native 端的配置表中,找到对应的原生类(实例)和方法,并进行调用。

对象和方法约定好了,还需要约定参数,将参数值按顺序放入一个数组中。使用者需要注意参数的个数和顺序,保持与 Native 端的方法匹配,否者会报错。

最后是 JS callback 的处理,JS 和 Native 通信是无法传递事件的,所以选择将事件序列化,给每个 callback 一个 ID,自己存一份,再将 ID 传给 Native,当 Native 要执行这个回调时,通过 invokeJSCallback 函数把这个 ID 回传给 JS,JS 再根据 ID 查找对应的 callback 并执行。

Native call JS

Native call JS 依赖于 JavaScriptCore,该框架提供创建 JS 上下文环境,以及执行 JS 代码的接口,相对来说直接很多,不过因为 Native 端是多线程的环境,所以需要分情况来讨论,主要可以分为三种:

  1. 同步调用 JS;
  2. 异步调用 JS;
  3. 异步执行 JS 的 callback

同步调用的场景非常少,因为它仅限于在 JS 线程调用,而实际情况是,Native 和 JS 的通信几乎都是跨线程的。因为页面刷新和事件回调都发生在主线程。

对于 Native 端来说,JS 线程是普通的一个线程,跟其他线程没有区别,只不过是用这个线程来初始化 JS 的上下文环境,以及执行 JS 代码。

同步调用支持有返回值,而异步调用的 api 是没有返回值的,只能使用 callback。

Native 异步调用 JS 主要由callFunctionReturnFlushedQueue函数分发:

var callFunctionReturnFlushedQueue = function(module, method, args) {
    this.__callFunction(module, method, args);
    return this.flushedQueue();
}
复制代码

该函数定义在BatchedBridge对象,并由 global 的__batchedBridge属性所持有。

Native 在调用时把 moduleName 和各参数值都放在一个数组中,使用 JSValue 包装,再通过 JavaScriptCore 提供的JSObjectCallAsFunction函数触发 JS 调用。

这里跟 JS call Native 不一样的是,不需要使用 ID,而是直接执行的。

在上述方法中 return 了this.flushedQueue(),它是前面提到的清理 JS call Native 消息队列,将队列中的信息返回给 Native 执行。

Native 调用 JS 如果有返回值,会有两种形式,一种是等待 JS 方法执行完成,拿到 return 的返回值;另一种是不等待 JS 方法执行完成,JS 执行完后通过 callback 回调给调用方。

其实不管是异步还是同步 Native 都是通过下面的方法执行 JS 调用的:

- (void)_executeJSCall:(NSString *)method
             arguments:(NSArray *)args
          unwrapResult:(BOOL)unwrapResult
                    callback:(AHJSCallbackBlock)onComplete
复制代码

如果该方法是在 JS 线程调用的,那么会同步处理返回值;如果是其他线程调用该方法,返回值是通过异步 callback 返回的。

最后是异步调用 JS callback 的情况,其实跟同步调用类似,只是在 JS 端定义了一个新的函数:

var invokeCallbackAndReturnFlushedQueue = function(callbackId, args) {
    this.__invokeCallback(callbackId, args);
    return this.flushedQueue();
}
复制代码

接受一个 callbackID,找到对应的 callback 并执行。

那么 Native 是怎么知道 JS 有哪些模块可以调用的呢?其实这只需 RN 框架内部定义就好,对于使用 RN 开发,主要是在 JS 端,知道 Native 有哪些功能提供给 JS 调用就好。

思考题

  1. 为什么 JS 和 Native 的通信只能是异步?

本质原因是 JS 是单线程执行的。而 Native 端负责 UI 展示的又只能是主线程,跨线程通信如果使用同步会阻塞 UI。

  1. 为什么 JS call Native 要设计一个消息队列,等待 Native 调用时才执行,而不像 Native call JS 每次调用都直接去执行呢?

为了提高性能,批量处理 JS 的 Native 调用,可以减少 JS 与 Native 通信开销。

感谢阅读。如果你对实现细节感兴趣,可以看一看我写的 Demo

关注下面的标签,发现更多相似文章
评论