一、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;
}
}