目录
最近因为一些底层优化,需要了解一下RN原理,搭配着网上的资源和源码看得七七八八了,但有些问题失踪迷迷糊糊,后来一个偶然的机会,说不定也是必然,我碰到一个项目,他是使用RN原理自己实现了一套Native与JS的通信机制,因为这份代码简化了很多内容,直指RN通信原理核心,让我瞬间豁然开朗,特在此将这份代码和文档按照我自己的理解在写一遍来加深记忆。另外为了方便理解下面的说明和尊重大神,相关类名都是按照大神的代码,如果想要看RN源码这一块的实现,将AH前缀改为RCT即可。
了解RN启动时Native都做了什么
1. 初始化AHBridge
之前只知道RN与Native通信是由Bridge负责,下面来了解一下他都做了啥。
- native通过是否遵循
AHBridgeModule
协议及是否有AH_EXPORT_METHOD
相关宏定义来收集要桥接的类,并存储在AHModuleClasses
- 将
AHModuleData
中的模块转化为一个个RCTModuleData
实例moduleData
放到数组moduleDataById
数组中,每个moduleData
都会生成一个moduleData
属性,用于执行实例方法。 - 将数组
moduleDataById
中的信息转化为如下格式的JSON字符串。能实现转换的原因是RCTModuleData
实例都以字符串的形式存储着模块名、方法名、常量名{"remoteModuleConfig": [ [ "Person", {"name":"Smallfly","age":"18"}, ["run"], null, null ] ] }
- 将上诉JSON字符串通过
AHJSExector
注入到JS环境的global的__batchedBridgeConfig
属性上,Native就是通过这个JSON串告诉RN我都提供有什么模块给你。另外这个JSON串就是很多地方提到的模块表
。该模块表会在js层转化成我们熟悉的NativeModule
模块 - 利用
AHJSExector
(管理JavaScriptCode实例的一个类)实例创建一个JS环境,并通过JavaScriptCode的TextBlock
属性给JS环境的global属性绑定一个名为nativeFlushQueueImmediate
的回调,这个十分重要,因为Native就是通过执行它来接受JS环境传给Native的模块id及相应的方法id - 通过
AHJSExector
执行Bundle的代码
2. 执行Bundle初始化JS数据
Bundle里跟Native通信最为关键的三个类在node_modules/react-native/Libraries/BatchedBridge
目录下,分别为NativeModule.js
、Batchedbridge.js
、MessageQueue.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;
}();
尽管大神已经帮我们简化了,但第一次看还是有点懵懵,接下来我们一块分析一下。
- 整体是一个自执行函数,函数名为NativeModules,函数内又定义了
getModule
和getMethod
函数,分析入口不在这两个函数上,而是在我再代码里设置的第一个分割线处。 global._remoteModuleConfig
获取的就是我们在初始化Bridge阶段注入到JS环境的JSON字符串,通过遍历这个JSON串将所有的变量给到modules数组,最后将该数组赋值给NativeModules,至此NativeModules
上便绑定着桥接的所有模块- 但是,因为
global._remoteModuleConfig
是字符串及JS环境不可能持有Native的实例,所以此时NodeModules仅仅知道有什么模块,及该模块有啥方法和常量,那么如何实现调用呢, - 关键在于我设置的第二个分割线处,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
- 前面收集JS调用Native的模块,并放到
messageQueue
(包括回调)。后面的5ms判断是因为每次调用(包括业务调用和框架调用)都跟Native交互有些浪费性能,所以采取5ms的时间间隔。 - 通过执行
global.nativeFlushQueueImmediate
(在bridge初始化时已经绑定了该回调)将收集到的模块调用传给Native并执行 - 有的文章说道有时JS调用Native的过程是被动的并且没有执行
nativeFlushQueueImmediate
。这是因为在Native调用JS的过程中会顺带做三件事:1. 将5ms的时间重置 2. 将当前messageQueue
返回给Native并执行 3. 清空messageQueue
callFunctionReturnResultAndFlushedQueue:Native调用JS
- Native调用JS通过
registerJSModule
暴露给Native的模块,并且顺带执行上面提到的三件事。 - 消息队列给Native是通过函数的返回值实现的
__invokeCallback:RN执行JS回调
- 这个就感觉没啥可说的了,Native传过来要执行的回调id,接着通过回调id找到对应的js函数并执行
JS调用Native方法
- 其实在介绍Bundle中三个文件的时候已经知道了,NativeModule调用原生模块时,最终执行了在初始化Bridge时设置的回调
nativeFlushQueueImmediate
, Native收到nativeFlushQueueImmediate
传过来的要执行的模块id及方法id,通过moduleIDs
(在初始化阶段保存AHModuleData的数组)找到相应的AHModuleData,通过NSInvocation
实行相应的消息转发,这个过程是异步 - 如何传参数: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