react-native通信流程

最近因为一些底层优化,需要了解一下RN原理,搭配着网上的资源和源码看得七七八八了,但有些问题失踪迷迷糊糊,后来一个偶然的机会,说不定也是必然,我碰到一个项目,他是使用RN原理自己实现了一套Native与JS的通信机制,因为这份代码简化了很多内容,直指RN通信原理核心,让我瞬间豁然开朗,特在此将这份代码文档按照我自己的理解在写一遍来加深记忆。另外为了方便理解下面的说明和尊重大神,相关类名都是按照大神的代码,如果想要看RN源码这一块的实现,将AH前缀改为RCT即可。

了解RN启动时Native都做了什么

1. 初始化AHBridge

之前只知道RN与Native通信是由Bridge负责,下面来了解一下他都做了啥。

  1. native通过是否遵循AHBridgeModule协议及是否有AH_EXPORT_METHOD相关宏定义来收集要桥接的类,并存储在AHModuleClasses
  2. AHModuleData中的模块转化为一个个RCTModuleData实例moduleData放到数组moduleDataById数组中,每个moduleData都会生成一个moduleData属性,用于执行实例方法。
  3. 将数组moduleDataById中的信息转化为如下格式的JSON字符串。能实现转换的原因是RCTModuleData实例都以字符串的形式存储着模块名、方法名、常量名
    {"remoteModuleConfig":
      [
        [
    	  "Person",
    	  {"name":"Smallfly","age":"18"},
    	  ["run"],
    	  null,
    	  null
    	]
      ]
    }
    
  4. 将上诉JSON字符串通过AHJSExector注入到JS环境的global的__batchedBridgeConfig属性上,Native就是通过这个JSON串告诉RN我都提供有什么模块给你。另外这个JSON串就是很多地方提到的模块表。该模块表会在js层转化成我们熟悉的NativeModule模块
  5. 利用AHJSExector(管理JavaScriptCode实例的一个类)实例创建一个JS环境,并通过JavaScriptCode的TextBlock属性给JS环境的global属性绑定一个名为nativeFlushQueueImmediate的回调,这个十分重要,因为Native就是通过执行它来接受JS环境传给Native的模块id及相应的方法id
  6. 通过AHJSExector执行Bundle的代码

2. 执行Bundle初始化JS数据

Bundle里跟Native通信最为关键的三个类在node_modules/react-native/Libraries/BatchedBridge 目录下,分别为NativeModule.jsBatchedbridge.jsMessageQueue.js,大神的demo将这三个类简化后放在一块了,我下面做拆分分析。

1. NativeModule

这个模块的功能就是给NativeModules变量赋值,让JS可以通过NativeModules来调用Native模块,比如调用原生的支付功能,NativeModules.weChat.pay()

// Native 暴露给 JS 的模块信息
var NativeModules = function() {
    
    // 构造 Native 对应的 JS Module
    function getModule(config, moduleID) {
        var [ moduleName, constants, methods, promiseMethods, syncMethods ] = config;
        if (!constants && !methods) {
            return { name: moduleName };
        }
        var module = {};
        methods && methods.forEach(function(methodName, methodID) {
           var isPromise = promiseMethods;
           var isSync = syncMethods;
           var methodType = isPromise ? 'promise' : (isSync ? 'sync' : 'async');
           module[methodName] = getMethod(moduleID, methodID, methodType);
        });
        module['constants'] = constants;

        return { name: moduleName, module: module };
    }
    
    // 构造与 Native 对应的 JS Method
    function getMethod(moduleID, methodID, type) {
        var fn = null;
        if (type === 'promise') {
        } else if (type === 'sync') { // 几乎没有
        } else {
            // 这里只处理了异步调用的方法
            // fn 的参数为 module method params failCallback successCallback
            // fn 闭包捕获了 moduleID, methodID
            // callback 为可选参数
            fn = function(...args) {
                const lastArg = args.length > 0 ? args[args.length - 1] : null;
                const secondLastArg = args.length > 1 ? args[args.length - 2] : null;
                
                const hasErrorCallback = typeof lastArg === 'function';
                const hasSuccessCallback = typeof secondLastArg === 'function';
                
                const onSuccess = hasSuccessCallback ? lastArg : null;
                const onFail = hasErrorCallback ? secondLastArg : null;
                
                const callbackCount = hasSuccessCallback + hasErrorCallback;
                args = args.slice(0, args.length - callbackCount);
                
                // Native 调用
                BatchedBridge.enqueueNativeCall(moduleID, methodID, args, onFail, onSuccess);
            }
        }
        // 通过 fn 闭包持有 moduleID 和 methodID
        // 根据 methodName 取
        fn.type = type;
        return fn;
    }
    // ------------------------------------我是第一个分割线---------------------------------------------
    var modules = {}
    // 取出 Native 暴露给 JS 的模块
    // 该属性在 AHJSExecutor.m 文件注入的
    var bridgeConfig = global.__batchedBridgeConfig;
    // [[name, constants, methods], ...]
    // 遍历每个模块,生成 JS 端调用信息
    (bridgeConfig.remoteModuleConfig || []).forEach(function(config, moduleID) {
        // ----------------------------我是第二个分割线----------------------------------------------
        var info = getModule(config, moduleID);
        if (!info) {
            return;
        }
        if (info.module) {
            // 原生 module 存入 NativeModules,提供 JS 调用
            modules[info.name] = info.module;
        }
    });
    return modules;
}();

尽管大神已经帮我们简化了,但第一次看还是有点懵懵,接下来我们一块分析一下。

  1. 整体是一个自执行函数,函数名为NativeModules,函数内又定义了getModulegetMethod函数,分析入口不在这两个函数上,而是在我再代码里设置的第一个分割线处。
  2. global._remoteModuleConfig获取的就是我们在初始化Bridge阶段注入到JS环境的JSON字符串,通过遍历这个JSON串将所有的变量给到modules数组,最后将该数组赋值给NativeModules,至此NativeModules上便绑定着桥接的所有模块
  3. 但是,因为global._remoteModuleConfig是字符串及JS环境不可能持有Native的实例,所以此时NodeModules仅仅知道有什么模块,及该模块有啥方法和常量,那么如何实现调用呢,
  4. 关键在于我设置的第二个分割线处,getModule的调用过程比较简单,就不做过多说明,关键是最后的调用BatchedBridge.enqueueNativeCall(下一小节讲),另外需要注意的一点就是getModule函数的第二个参数moduleID是数组remoteModuleConfig的遍历下标(没错就是如此随意,后续的methodId和Native通过通过id找模块都是如此,不再赘述)。

2. Batchedbridge

其实BatchedBridge就是MessageQueue的一个实例,在RN中该文件的源码如下:

const MessageQueue = require('MessageQueue');

const BatchedBridge = new MessageQueue();

Object.defineProperty(global, '__fbBatchedBridge', {
  configurable: true,
  value: BatchedBridge,
});

module.exports = BatchedBridge;

但大神将将这个过程给省掉了,直接将MessageQueuqe执行并赋值给了Batchedbridge,因此BatchedBridge.enqueueNativeCall其实执行的就是MessageQueue的方法。这里要千万不要忽略将BatchedBridge绑定到global,在Native调用RN会用到这个东东的

3.MessageQueue

JS与Native的通信控制都在该文件下,这里面跟NativeModules一样,也是一个执行函数,最后返回的返回值都被绑定在global上,供Native调用

var BatchedBridge = (function() {
    const MODULE_INDEX = 0;
    const METHOD_INDEX = 1;
    const PARAMS = 2;
    const MIN_TIME_BETWEEN_FLUSHES_MS = 5;

    // OC 直接调用 JS 的方法,没有返回值
    // 由 OC 拿到 global 对象直接调用
    // 每次 Native 调用 JS 时,会顺带将消息队列中未执行的 JS->Native 返回给 Native 执行
    var callFunctionReturnFlushedQueue = function(module, method, args) {
        this.__callFunction(module, method, args);
        return this.flushedQueue();
    }

    // OC 直接调用 JS 的方法,有返回值
    // 由 OC 拿到 global 对象直接调用
    // 每次 Native 调用 JS 时,会顺带将消息队列中未执行的 JS->Native 返回给 Native 执行
    var callFunctionReturnResultAndFlushedQueue = function(module, method, args) {
        const result = this.__callFunction(module, method, args);
        return [result, this.flushedQueue()];
    }

    // OC 执行 JS 的 Callback,顺带返回未处理的 JS->Native 消息
    // 由 OC 拿到 global 对象直接调用
    var invokeCallbackAndReturnFlushedQueue = function(callbackId, args) {
        this.__invokeCallback(callbackId, args);
        return this.flushedQueue();
    }

    // 返回未处理的 Native 调用,并清理消息队列
    var flushedQueue = function() {
        const queue = this.queue;
        this.queue = [[], [], [], this.callID];
        return queue[0].length ? queue: null;
    }

    var __callFunction = function(module, method, args) {
        // 记录消息队列清理时间
        this._lastFlush = new Date().getTime();
        const moduleMethods = this.callableModules[module];
        // 执行 JS 调用
        const result = moduleMethods[method].apply(null, args);
        return args;
    }

    var __invokeCallback = function(cbID, args) {
        // 记录消息队列清理时间
        this._lastFlush = new Date().getTime();
        const callback = this.callbacks[cbID];
        if (!callback) {
        return;
        }
        this.callbacks[cbID] = null;
        callback.apply(null, args);
    }

    // 所有 JS 需要调用 OC 时都会走这个方法
    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];
        }
    }

    // 注册暴露给 OC 的 JS 模块
    var registerJSModule = function(name, module) {
        this.callableModules[name] = module;
    }
  
    return {
        callID: 0,
        queue: [[], [], [], 0],
        callbacks: [],
        callbackID: 0,
        lastFlush: 0,
        // 支持 Native 调用的 JS modules
        callableModules: {},
        
        callFunctionReturnFlushedQueue: callFunctionReturnFlushedQueue,
        callFunctionReturnResultAndFlushedQueue: callFunctionReturnResultAndFlushedQueue,
        invokeCallbackAndReturnFlushedQueue: invokeCallbackAndReturnFlushedQueue,
        flushedQueue: flushedQueue,
        __callFunction: __callFunction,
        __invokeCallback: __invokeCallback,
        enqueueNativeCall: enqueueNativeCall,
    };
})();

接下来聊下里面的一些关键方法:

enqueueNativeCall :JS调用Native
  1. 前面收集JS调用Native的模块,并放到messageQueue(包括回调)。后面的5ms判断是因为每次调用(包括业务调用和框架调用)都跟Native交互有些浪费性能,所以采取5ms的时间间隔。
  2. 通过执行global.nativeFlushQueueImmediate(在bridge初始化时已经绑定了该回调)将收集到的模块调用传给Native并执行
  3. 有的文章说道有时JS调用Native的过程是被动的并且没有执行nativeFlushQueueImmediate。这是因为在Native调用JS的过程中会顺带做三件事:1. 将5ms的时间重置 2. 将当前messageQueue返回给Native并执行 3. 清空messageQueue
callFunctionReturnResultAndFlushedQueue:Native调用JS
  1. Native调用JS通过registerJSModule暴露给Native的模块,并且顺带执行上面提到的三件事。
  2. 消息队列给Native是通过函数的返回值实现的
__invokeCallback:RN执行JS回调
  1. 这个就感觉没啥可说的了,Native传过来要执行的回调id,接着通过回调id找到对应的js函数并执行

JS调用Native方法

  1. 其实在介绍Bundle中三个文件的时候已经知道了,NativeModule调用原生模块时,最终执行了在初始化Bridge时设置的回调nativeFlushQueueImmediate, Native收到nativeFlushQueueImmediate传过来的要执行的模块id及方法id,通过moduleIDs(在初始化阶段保存AHModuleData的数组)找到相应的AHModuleData,通过NSInvocation实行相应的消息转发,这个过程是异步
  2. 如何传参数:RN与JS之间的传参只支持基本数据类型/对象/数组,碰到不支持的需要通过id进行标识标识

Native调用JS方法

介绍messageQueue是已经聊过了,建议在过一遍,加深印象

总结

在这里插入图片描述

附录

这部分都是摘抄自另一个大神的博客ReactNative iOS源码解析,方便自己快速查找。

RN通信用到的相关类

这个是真实的RN源码中用到的
在这里插入图片描述

RN通信结构图

在这里插入图片描述

React是如何与Native关联在一起的?

React.JS是一个前端框架,在浏览器内H5开发上被广泛使用,他在渲染render()这个环节,在经过各种flexbox布局算法之后,要在确定的位置去绘制这个界面元素的时候,需要通过浏览器去实现。他在响应触摸touchEvent()这个环节,依然是需要浏览器去捕获用户的触摸行为,然后回调React.JS

上面提到的都是纯网页,纯H5,但如果我们把render()这个事情拦截下来,不走浏览器,而是走native会怎样呢?

当React.JS已经计算完每个页面元素的位置大小,本来要传给浏览器,让浏览器进行渲染,这时候我们不传给浏览器了,而是通过一个JS/OC的桥梁,去通过[[UIView alloc]initWithFrame:frame]的OC代码,把这个界面元素渲染了,那我们就相当于用React.JS绘制出了一个native的View

拿我们刚刚绘制出得native的View,当他发生native源生的- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event触摸事件的时候,通过一个OC/JS的桥梁,去调用React.JS里面写好的点击事件JS代码

这样React.JS还是那个React.JS,他的使用方法没发生变化,但是却获得了纯源生native的体验,native的组件渲染,native的触摸响应

于是,这个东西就叫做React-Native

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值