一、RN整體架構(gòu)設(shè)計


二、JS調(diào)用原生及回調(diào)
1. 導(dǎo)出原生模塊
如何導(dǎo)出?
- iOS: 類通過RCT_EXTERN_MODULE宏來進行標(biāo)記 ,方法則通過RCT_EXTERN_METHOD宏標(biāo)記 ,如果是 UI控件,需要繼承RCTUIViewManager并自己實現(xiàn)View
- Android: 類加注解@ReactModule,繼承自ReactContextBaseJavaModule , 方法加注解 @ReactMethod
RN里面如何收集這些導(dǎo)出的方法和模塊?
iOS: RCTBridge里面定義了一些方法, RCT_EXTERN_MODULE 和RCT_EXTERN_METHOD實際上調(diào)用了這些方法把模塊和方法名存到了數(shù)組里面,再經(jīng)過RCTCxxBridge的加工(對各個模塊進行注冊和實例化),放到了ModuleRegistry里面,這個ModuleRegistry被JSToNativeBridge持有(C++實現(xiàn)),JSToNativeBridge又被RCTCxxBridge持有。
Android: Android這邊復(fù)雜一點,為了實現(xiàn)跟iOS代碼復(fù)用,中間有一層JNI轉(zhuǎn)換。 首先Java層有一個CatalystInstance, 他對應(yīng)的實現(xiàn)CatalystInstanceImpl (相當(dāng)于iOS的RCTCxxBridge)持有NativeModuleRegistry,NativeModuleRegistery里面通過解析Anotation拿到添加了@ReactModule的 原生模塊,以及添加了@ReactMethod 的方法。然后CatalystInstanceImpl通過JNI,把模塊信息傳遞給C++這邊的CatalystInstanceImpl,C++這邊的CatalystInstanceImpl也有一個ModuleRegistery(跟iOS的一樣),有著類似的結(jié)構(gòu)。
關(guān)于方法的識別,這里以iOS為例,在模塊加載的時候會根據(jù)方法名稱里面所帶的參數(shù)類型來生成方法簽名,這里面參數(shù)類型如果含有 RCTPromiseResolveBlock 和RCTPromiseRejectBlock,則添加一個argumentBlock(invokeWithBridge方法會調(diào)用),這個argumentBlock里面再調(diào)用 enqueueCallBack添加一個JS回調(diào), 把執(zhí)行結(jié)果或者是錯誤信息返回給JS。 具體代碼在RCTModuleMethod.mm的processMethodSignature函數(shù)里, 這個函數(shù)做了很多事情,包括方法簽名的解析,解析過程比較復(fù)雜,這里不貼了。這個解析過程會緩存下來,存到argumentBlocks里面,后續(xù)再調(diào)用這個方法都讀取緩存中的argumentBlocks。
總的來說通過宏或者注解的方式讓 RN Instance獲取到模塊和方法信息,存儲到moduleRegistry里, 然后把這些信息轉(zhuǎn)成數(shù)組傳遞給JS, JS這邊生成全局的NativeModules來存儲這些信息(只存儲模塊名稱、ID和方法名稱、ID、參數(shù)等相關(guān)信息,不存儲具體實現(xiàn))。
JS這邊根據(jù)方法的類型(原生這邊根據(jù)參數(shù)判斷生成的)生成對應(yīng)的方法原型: 同步/異步/普通方法,其中同步方法是立即調(diào)用,其他都要經(jīng)過messageQueue.js 排隊調(diào)用

2. JS調(diào)用原生流程
NativeModules默認(rèn)是懶加載的,也就是說第一次require的時候才會進行加載。 JS這邊調(diào)用Nativemodules["模塊名"]["方法名"]時,會在NativeModules里面查找對應(yīng)方法有無緩存,如果沒有,會先去原生這邊獲取模塊相關(guān)信息并生成,如果有,則直接調(diào)用。 具體可以看一下NativeModules.js
messageQuque.js 負責(zé)原生方法的排隊調(diào)用 ,主要邏輯在enqueueNativeCall這個方法里面, 原生方法是一批一批的調(diào)用的, 調(diào)用的最小時間間隔是5毫秒。實際調(diào)用是通過一個叫nativeFlushQueueImmediate的方法進行操作的,這個方法通過JSCore跟原生進行了綁定。 這個方法的參數(shù)被封裝到了queue里面,queue是一個數(shù)組,原型為
_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;
可以看出,前面三個參數(shù)都是數(shù)組,也就是說,當(dāng)多個方法批量調(diào)用時,會拆除其moduleId、methodId、params放到對應(yīng)的數(shù)組里面
this._queue[MODULE_IDS].push(moduleID);
this._queue[METHOD_IDS].push(methodID);
this._queue[PARAMS].push(params);
最后一個參數(shù)是callId,一般情況下是放在params里面一起傳遞的,只有一種情況需要特殊處理的,就是等到的時間到了,而原生還沒有主動回調(diào)時,需要JS主動觸發(fā),才傳遞這個參數(shù),并且這時候其他參數(shù)是空的
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);
}
那正常放到隊列里面的方法什么時候調(diào)用呢? 這個就比較復(fù)雜了??碕S代碼,可以發(fā)現(xiàn) callFunction這個方法觸發(fā)了原生的調(diào)用,callFunction又是通過callFunctionReturnFlushedQueue或者callFunctionReturnResultAndFlushedQueue 來調(diào)用的。
__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); //實際調(diào)用原生代碼的地方
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()];
}
這兩個如果你全局搜索,會發(fā)現(xiàn)他們其實也是通過原生來調(diào)用的,在原生這邊可以看到JSIExecutor::callFunction這么一個方法。這個方法是用于原生主動調(diào)用JS代碼的,這里面有一個 callFunctionReturnFlushedQueue_->call的調(diào)用。在這外面還有一個
scopedTimeoutInvoker_,用于延遲調(diào)用,具體為什么要延遲我們先不管??偟膩碚f,JS這邊觸發(fā)原生調(diào)用是需要定時器或者是>=5ms才觸發(fā)。
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);
}

經(jīng)過JSCore的轉(zhuǎn)換, 方法最終調(diào)用到原生這邊來。 到原生之后,會通過JSI層的 callNativeModules方法,調(diào)用到JSToNativeBridge這邊來。然后調(diào)用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); //調(diào)用對應(yīng)方法
}
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(); //批量調(diào)用結(jié)束
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方法,間接調(diào)用到RCTModuleMethod的invokeWithBridge , 生成對應(yīng)的NSInvocation執(zhí)行對應(yīng)原生方法的調(diào)用。
如果是Android,則通過Java層傳遞到instance的NativeModule來調(diào)用,這個invoke實際調(diào)用的是NativeModule.java里面的invoke,這是NativeMethod接口提供的方法,實際上的實現(xiàn)在JavaMethodWrapper.java里面,通過運行時反射機制觸發(fā)的對應(yīng)的原生方法。
在調(diào)用完各個模塊的方法之后,還會有一個 m_callback 的onBatchComplete方法,回調(diào)到OC和Java層的onBatchComplete。這個主要是供UIManager刷新用的,這里就不展開講了。
總結(jié)
JSIExecutor、MessageQueue是兩端交互的核心,通過這兩者注入代理對象供另一方調(diào)用,以實現(xiàn)Native&JS數(shù)據(jù)傳遞、互相調(diào)用。
-
JS call Native的觸發(fā)時機有:
- 1.調(diào)用
enqueueNativeCall函數(shù)入隊(存入暫存表)時發(fā)現(xiàn)距離上一次調(diào)用大于5毫秒時,通過nativeFlushQueueImmediate執(zhí)行調(diào)用; - 2.執(zhí)行
flushedQueue時(flushedQueue用于執(zhí)行JS端setImmediate異步任務(wù),在此不展開討論),把原生模塊調(diào)用信息作為返回值傳遞到原生端,執(zhí)行調(diào)用; - 3.通過
callFunctionReturnFlushedQueue執(zhí)行JS call Native也會觸發(fā)flushedQueue,同樣返回原生模塊調(diào)用信息 - 4.通過
invokeCallbackAndReturnFlushedQueue執(zhí)行JS回調(diào),同理。
筆者猜想這種設(shè)計的目的是:保證能及時發(fā)起函數(shù)調(diào)用的前提下,減少調(diào)用頻率。畢竟 JS call Native的調(diào)用是非常頻繁的。
- 1.調(diào)用
三、原生調(diào)用JS

1.一般調(diào)用
相比JS調(diào)原生,原生調(diào)JS則簡單的多,NativeToJSBridge有一個callFunction方法,其內(nèi)部是調(diào)用了JSIExecutor的 callFunction,而callFunction前面已經(jīng)講過了他的邏輯。NativeToJSBridge又被RCTCxxBridge和RCTBridge封裝了兩層,最終暴露給開發(fā)者的是enqueueJSCall這個方法。這里為什么要enqueue呢? 因為原生和JS代碼是運行在不同的線程,原生要調(diào)用JS,需要切換線程,也就是切到了對應(yīng)的MessageQueue上。iOS這邊是RCTMessageThread(被RCTCxxBridge持有的_jsThread),android這邊是MessageQueueThread(被CatalystInstanceImpl持有的mNativeModulesQueueThread)。然后通過消息隊列,觸發(fā)對應(yīng)的函數(shù)執(zhí)行。
這里有一個需要提及的,就是JS調(diào)用原生之后,promise的回調(diào),這個是如何實現(xiàn)的?
前面我們已經(jīng)看到JS調(diào)用C++的時候會在參數(shù)里面?zhèn)鱟allbackId過來,這個callBackID實際由兩部分組成,一個succCallId,一個failCallId,通過對callId移位操作得到(一個左移,一個右移)
原生這邊,前面已經(jīng)講過,在模塊加載的時候會根據(jù)方法名稱里面所帶的參數(shù)類型來識別是否需要回調(diào),并生成對應(yīng)的argumentBlock,等待原生橋接方法執(zhí)行完成然后再調(diào)用,這里截取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的,并且對應(yīng)的resolve和reject也已經(jīng)生成,原生這邊只需要根據(jù)參數(shù)對應(yīng)的位置直接調(diào)用即可。
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. 通知調(diào)用
原生調(diào)用JS還有另外一種方式, 即通過RCTDeviceEventEmitter 發(fā)通知。這種方式其實本質(zhì)上跟直接調(diào)用enqueueJSCall沒太大區(qū)別,只不過封裝了一個觀察者,使其可以達到一個程度上的解耦。我們看下他的實現(xiàn)
- (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也很簡單,這里只貼一個函數(shù),就是emit,可以看出,這個函數(shù)就是把傳入的eventName、body拿出來,然后通過listener的apply直接調(diào)用對應(yīng)的實現(xiàn),這個listener是EmitterSubscription的屬性, 會根據(jù)eventType過濾對應(yīng)的消息
/**
* 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;
}
}