一、RN整体架构设计
二、JS调用原生及回调
1. 导出原生模块
如何导出?
- iOS: 类通过RCT_EXTERN_MODULE宏来进行标记 ,方法则通过RCT_EXTERN_METHOD宏标记 ,如果是 UI控件,需要继承RCTUIViewManager并自己实现View
- Android: 类加注解@ReactModule,继承自ReactContextBaseJavaModule , 方法加注解 @ReactMethod
RN里面如何收集这些导出的方法和模块?
iOS: RCTBridge里面定义了一些方法, RCT_EXTERN_MODULE 和RCT_EXTERN_METHOD实际上调用了这些方法把模块和方法名存到了数组里面,再经过RCTCxxBridge的加工(对各个模块进行注册和实例化),放到了ModuleRegistry里面,这个ModuleRegistry被JSToNativeBridge持有(C++实现),JSToNativeBridge又被RCTCxxBridge持有。
Android: Android这边复杂一点,为了实现跟iOS代码复用,中间有一层JNI转换。 首先Java层有一个CatalystInstance, 他对应的实现CatalystInstanceImpl (相当于iOS的RCTCxxBridge)持有NativeModuleRegistry,NativeModuleRegistery里面通过解析Anotation拿到添加了@ReactModule的 原生模块,以及添加了@ReactMethod 的方法。然后CatalystInstanceImpl通过JNI,把模块信息传递给C++这边的CatalystInstanceImpl,C++这边的CatalystInstanceImpl也有一个ModuleRegistery(跟iOS的一样),有着类似的结构。
关于方法的识别,这里以iOS为例,在模块加载的时候会根据方法名称里面所带的参数类型来生成方法签名,这里面参数类型如果含有 RCTPromiseResolveBlock 和RCTPromiseRejectBlock,则添加一个argumentBlock(invokeWithBridge方法会调用),这个argumentBlock里面再调用 enqueueCallBack添加一个JS回调, 把执行结果或者是错误信息返回给JS。 具体代码在RCTModuleMethod.mm的processMethodSignature函数里, 这个函数做了很多事情,包括方法签名的解析,解析过程比较复杂,这里不贴了。这个解析过程会缓存下来,存到argumentBlocks里面,后续再调用这个方法都读取缓存中的argumentBlocks。
总的来说通过宏或者注解的方式让 RN Instance获取到模块和方法信息,存储到moduleRegistry里, 然后把这些信息转成数组传递给JS, JS这边生成全局的NativeModules来存储这些信息(只存储模块名称、ID和方法名称、ID、参数等相关信息,不存储具体实现)。
JS这边根据方法的类型(原生这边根据参数判断生成的)生成对应的方法原型: 同步/异步/普通方法,其中同步方法是立即调用,其他都要经过messageQueue.js 排队调用
2. JS调用原生流程
NativeModules默认是懒加载的,也就是说第一次require的时候才会进行加载。 JS这边调用Nativemodules["模块名"]["方法名"]时,会在NativeModules里面查找对应方法有无缓存,如果没有,会先去原生这边获取模块相关信息并生成,如果有,则直接调用。 具体可以看一下NativeModules.js
messageQuque.js 负责原生方法的排队调用 ,主要逻辑在enqueueNativeCall这个方法里面, 原生方法是一批一批的调用的, 调用的最小时间间隔是5毫秒。实际调用是通过一个叫nativeFlushQueueImmediate的方法进行操作的,这个方法通过JSCore跟原生进行了绑定。 这个方法的参数被封装到了queue里面,queue是一个数组,原型为
_queue: [number[], number[], any[], number];// 四个元素分别为ModuleIds、 methodIds、params、callId
const MODULE_IDS = 0;
const METHOD_IDS = 1;
const PARAMS = 2;
const MIN_TIME_BETWEEN_FLUSHES_MS = 5;
可以看出,前面三个参数都是数组,也就是说,当多个方法批量调用时,会拆除其moduleId、methodId、params放到对应的数组里面
this._queue[MODULE_IDS].push(moduleID);
this._queue[METHOD_IDS].push(methodID);
this._queue[PARAMS].push(params);
最后一个参数是callId,一般情况下是放在params里面一起传递的,只有一种情况需要特殊处理的,就是等到的时间到了,而原生还没有主动回调时,需要JS主动触发,才传递这个参数,并且这时候其他参数是空的
const now = Date.now();
if (
global.nativeFlushQueueImmediate &&
now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS
) {
const queue = this._queue;
this._queue = [[], [], [], this._callID];
this._lastFlush = now;
global.nativeFlushQueueImmediate(queue);
}
那正常放到队列里面的方法什么时候调用呢? 这个就比较复杂了。看JS代码,可以发现 callFunction这个方法触发了原生的调用,callFunction又是通过callFunctionReturnFlushedQueue或者callFunctionReturnResultAndFlushedQueue 来调用的。
__callFunction(module: string, method: string, args: any[]): any {
this._lastFlush = Date.now();
this._eventLoopStartTime = this._lastFlush;
if (__DEV__ || this.__spy) {
Systrace.beginEvent(`${module}.${method}(${stringifySafe(args)})`);
} else {
Systrace.beginEvent(`${module}.${method}(...)`);
}
if (this.__spy) {
this.__spy({type: TO_JS, module, method, args});
}
const moduleMethods = this.getCallableModule(module);
invariant(
!!moduleMethods,
'Module %s is not a registered callable module (calling %s)',
module,
method,
);
invariant(
!!moduleMethods[method],
'Method %s does not exist on module %s',
method,
module,
);
const result = moduleMethods[method].apply(moduleMethods, args); //实际调用原生代码的地方
Systrace.endEvent();
return result;
}
callFunctionReturnFlushedQueue(module: string, method: string, args: any[]) {
this.__guard(() => {
this.__callFunction(module, method, args);
});
return this.flushedQueue();
}
callFunctionReturnResultAndFlushedQueue(
module: string,
method: string,
args: any[],
) {
let result;
this.__guard(() => {
result = this.__callFunction(module, method, args);
});
return [result, this.flushedQueue()];
}
这两个如果你全局搜索,会发现他们其实也是通过原生来调用的,在原生这边可以看到JSIExecutor::callFunction这么一个方法。这个方法是用于原生主动调用JS代码的,这里面有一个 callFunctionReturnFlushedQueue_->call的调用。在这外面还有一个
scopedTimeoutInvoker_,用于延迟调用,具体为什么要延迟我们先不管。总的来说,JS这边触发原生调用是需要定时器或者是>=5ms才触发
。
void JSIExecutor::callFunction(
const std::string& moduleId,
const std::string& methodId,
const folly::dynamic& arguments) {
SystraceSection s(
"JSIExecutor::callFunction", "moduleId", moduleId, "methodId", methodId);
if (!callFunctionReturnFlushedQueue_) {
bindBridge();
}
// Construct the error message producer in case this times out.
// This is executed on a background thread, so it must capture its parameters
// by value.
auto errorProducer = [=] {
std::stringstream ss;
ss << "moduleID: " << moduleId << " methodID: " << methodId
<< " arguments: " << folly::toJson(arguments);
return ss.str();
};
Value ret = Value::undefined();
try {
scopedTimeoutInvoker_(
[&] {
ret = callFunctionReturnFlushedQueue_->call(
*runtime_,
moduleId,
methodId,
valueFromDynamic(*runtime_, arguments));
},
std::move(errorProducer));
} catch (...) {
std::throw_with_nested(
std::runtime_error("Error calling " + moduleId + "." + methodId));
}
callNativeModules(ret, true);
}
经过JSCore的转换, 方法最终调用到原生这边来。 到原生之后,会通过JSI层的 callNativeModules方法,调用到JSToNativeBridge这边来。然后调用moduleRegistry的callNativeModules
void callNativeModules(
JSExecutor& executor, folly::dynamic&& calls, bool isEndOfBatch) override {
CHECK(m_registry || calls.empty()) <<
"native module calls cannot be completed with no native modules";
m_batchHadNativeModuleCalls = m_batchHadNativeModuleCalls || !calls.empty();
// An exception anywhere in here stops processing of the batch. This
// was the behavior of the Android bridge, and since exception handling
// terminates the whole bridge, there's not much point in continuing.
for (auto& call : parseMethodCalls(std::move(calls))) {
m_registry->callNativeMethod(call.moduleId, call.methodId, std::move(call.arguments), call.callId); //调用对应方法
}
if (isEndOfBatch) {
// onBatchComplete will be called on the native (module) queue, but
// decrementPendingJSCalls will be called sync. Be aware that the bridge may still
// be processing native calls when the birdge idle signaler fires.
if (m_batchHadNativeModuleCalls) {
m_callback->onBatchComplete(); //批量调用结束
m_batchHadNativeModuleCalls = false;
}
m_callback->decrementPendingJSCalls();
}
}
void ModuleRegistry::callNativeMethod(unsigned int moduleId, unsigned int methodId, folly::dynamic&& params, int callId) {
if (moduleId >= modules_.size()) {
throw std::runtime_error(
folly::to<std::string>("moduleId ", moduleId, " out of range [0..", modules_.size(), ")"));
}
modules_[moduleId]->invoke(methodId, std::move(params), callId);
}
到这里IOS和android就有一个分化了。如果是iOS,会通过 RCTNativeModule的invoke方法,间接调用到RCTModuleMethod的invokeWithBridge , 生成对应的NSInvocation执行对应原生方法的调用。
如果是Android,则通过Java层传递到instance的NativeModule来调用,这个invoke实际调用的是NativeModule.java里面的invoke,这是NativeMethod接口提供的方法,实际上的实现在JavaMethodWrapper.java里面,通过运行时反射机制触发的对应的原生方法。
在调用完各个模块的方法之后,还会有一个 m_callback 的onBatchComplete方法,回调到OC和Java层的onBatchComplete。这个主要是供UIManager刷新用的,这里就不展开讲了。
总结
JSIExecutor、MessageQueue是两端交互的核心,通过这两者注入代理对象供另一方调用,以实现Native&JS数据传递、互相调用。
-
JS call Native的触发时机有:
- 1.调用
enqueueNativeCall
函数入队(存入暂存表)时发现距离上一次调用大于5毫秒时,通过nativeFlushQueueImmediate
执行调用; - 2.执行
flushedQueue
时(flushedQueue
用于执行JS端setImmediate异步任务,在此不展开讨论),把原生模块调用信息作为返回值传递到原生端,执行调用; - 3.通过
callFunctionReturnFlushedQueue
执行JS call Native也会触发flushedQueue
,同样返回原生模块调用信息 - 4.通过
invokeCallbackAndReturnFlushedQueue
执行JS回调,同理。
笔者猜想这种设计的目的是:保证能及时发起函数调用的前提下,减少调用频率。毕竟 JS call Native的调用是非常频繁的。
- 1.调用
三、原生调用JS
1.一般调用
相比JS调原生,原生调JS则简单的多,NativeToJSBridge有一个callFunction方法,其内部是调用了JSIExecutor的 callFunction,而callFunction前面已经讲过了他的逻辑。NativeToJSBridge又被RCTCxxBridge和RCTBridge封装了两层,最终暴露给开发者的是enqueueJSCall这个方法。这里为什么要enqueue呢? 因为原生和JS代码是运行在不同的线程,原生要调用JS,需要切换线程,也就是切到了对应的MessageQueue上。iOS这边是RCTMessageThread(被RCTCxxBridge持有的_jsThread),android这边是MessageQueueThread(被CatalystInstanceImpl持有的mNativeModulesQueueThread)。然后通过消息队列,触发对应的函数执行。
这里有一个需要提及的,就是JS调用原生之后,promise的回调,这个是如何实现的?
前面我们已经看到JS调用C++的时候会在参数里面传callbackId过来,这个callBackID实际由两部分组成,一个succCallId,一个failCallId,通过对callId移位操作得到(一个左移,一个右移)
原生这边,前面已经讲过,在模块加载的时候会根据方法名称里面所带的参数类型来识别是否需要回调,并生成对应的argumentBlock,等待原生桥接方法执行完成然后再调用,这里截取RCTModuleMethod的部分代码看下逻辑
else if ([typeName isEqualToString:@"RCTPromiseResolveBlock"]) {
RCTAssert(i == numberOfArguments - 2,
@"The RCTPromiseResolveBlock must be the second to last parameter in %@",
[self methodName]);
BLOCK_CASE((id result), {
[bridge enqueueCallback:json args:result ? @[result] : @[]];
});
} else if ([typeName isEqualToString:@"RCTPromiseRejectBlock"]) {
RCTAssert(i == numberOfArguments - 1,
@"The RCTPromiseRejectBlock must be the last parameter in %@",
[self methodName]);
BLOCK_CASE((NSString *code, NSString *message, NSError *error), {
NSDictionary *errorJSON = RCTJSErrorFromCodeMessageAndNSError(code, message, error);
[bridge enqueueCallback:json args:@[errorJSON]];
});
}
到JS这边,生成方法的时候是会生成并返回promise的,并且对应的resolve和reject也已经生成,原生这边只需要根据参数对应的位置直接调用即可。
function genMethod(moduleID: number, methodID: number, type: MethodType) {
let fn = null;
if (type === 'promise') {
fn = function(...args: Array<any>) {
return new Promise((resolve, reject) => {
BatchedBridge.enqueueNativeCall(
moduleID,
methodID,
args,
data => resolve(data),
errorData => reject(createErrorFromErrorData(errorData)),
);
});
};
}
.......
}
2. 通知调用
原生调用JS还有另外一种方式, 即通过RCTDeviceEventEmitter 发通知。这种方式其实本质上跟直接调用enqueueJSCall没太大区别,只不过封装了一个观察者,使其可以达到一个程度上的解耦。我们看下他的实现
- (void)sendEventWithName:(NSString *)eventName body:(id)body
{
RCTAssert(_bridge != nil, @"Error when sending event: %@ with body: %@. "
"Bridge is not set. This is probably because you've "
"explicitly synthesized the bridge in %@, even though it's inherited "
"from RCTEventEmitter.", eventName, body, [self class]);
if (RCT_DEBUG && ![[self supportedEvents] containsObject:eventName]) {
RCTLogError(@"`%@` is not a supported event type for %@. Supported events are: `%@`",
eventName, [self class], [[self supportedEvents] componentsJoinedByString:@"`, `"]);
}
if (_listenerCount > 0) {
[_bridge enqueueJSCall:@"RCTDeviceEventEmitter"
method:@"emit"
args:body ? @[eventName, body] : @[eventName]
completion:NULL];
} else {
RCTLogWarn(@"Sending `%@` with no listeners registered.", eventName);
}
}
在JS这边RCTDeviceEventEmitter是继承自EventEmitter的
class RCTDeviceEventEmitter extends EventEmitter
EventEmitter也很简单,这里只贴一个函数,就是emit,可以看出,这个函数就是把传入的eventName、body拿出来,然后通过listener的apply直接调用对应的实现,这个listener是EmitterSubscription的属性, 会根据eventType过滤对应的消息
/**
* Emits an event of the given type with the given data. All handlers of that
* particular type will be notified.
*
* @param {string} eventType - Name of the event to emit
* @param {...*} Arbitrary arguments to be passed to each registered listener
*
* @example
* emitter.addListener('someEvent', function(message) {
* console.log(message);
* });
*
* emitter.emit('someEvent', 'abc'); // logs 'abc'
*/
emit(eventType: string) {
const subscriptions: ?[EmitterSubscription] = (this._subscriber.getSubscriptionsForType(eventType): any);
if (subscriptions) {
for (let i = 0, l = subscriptions.length; i < l; i++) {
const subscription = subscriptions[i];
// The subscription may have been removed during this event loop.
if (subscription) {
this._currentSubscription = subscription;
subscription.listener.apply(
subscription.context,
Array.prototype.slice.call(arguments, 1)
);
}
}
this._currentSubscription = null;
}
}