RN 通讯原理(for 前端)

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

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

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

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

JS call Native

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

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

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函数。api

在下一节 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 与 RN 通讯的频率,是一种函数节流的方案。

感谢阅读。若是你对实现细节感兴趣,能够看一看我写的 Demo

相关文章
相关标签/搜索