はじめに:実は、元々iOSでreact-nativeを実現するjsbridgeの記事を書きたかったのです。公式ドキュメントを読んだ学生は、RCTBridgeModuleというモジュールを使ってrnとiOSの通信を実現することが明確だと思います!そこで、rnのソースコードを調べて、rnとiOSの通信の仕組みを探ることにしました。その結果、発見した内容の深さを分析することで、 編の2本のRNソースコード分析記事を書きました。
この記事では、上記の2つの記事に基づいて、RNとiOSのネイティブ通信メカニズムについて理解を深めていきます。
免責事項:この記事で使用しているrnのバージョンは0.63.0です。
オリジン
前回の記事ReactNativeとiOS Nativeの通信原理分析-JSのロードと実行を読んだ学生は、jsコードの実行後、JSIExecutorでflush関数が実行されること、flush関数がJSとネイティブを初めてバインドすること、バインド後、ネイティブはJS関数を呼び出すことができるようになります。
// 様々なjsメソッドのネイティブへのバインディング
void JSIExecutor::bindBridge() {
 std::call_once(bindFlag_, [this] {
 // js側を使うことで__fbBatchedBridge対応するbatchedBridgeを取得する
 Value batchedBridgeValue =
 runtime_->global().getProperty(*runtime_, "__fbBatchedBridge");
 if (batchedBridgeValue.isUndefined()) {
 throw JSINativeException(
 "Could not get BatchedBridge, make sure your bundle is packaged correctly");
 }
// batchedBridgeのcallFunctionReturnFlushedQueueとJSIExecutorオブジェクトのcallFunctionReturnFlushedQueue。_バインディング
 Object batchedBridge = batchedBridgeValue.asObject(*runtime_);
 callFunctionReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction(
 *runtime_, "callFunctionReturnFlushedQueue");
 // batchedBridgeにinvokeCallbackAndReturnFlushedQueueを、JSIExecutorにinvokeCallbackAndReturnFlushedQueueを入れる。_バインディング
 invokeCallbackAndReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction(
 *runtime_, "invokeCallbackAndReturnFlushedQueue");
 // batchedBridgeにflushedQueueを、JSIExecutorにflushedQueueを入れる_バインディング
 flushedQueue_ =
 batchedBridge.getPropertyAsFunction(*runtime_, "flushedQueue");
 });
}
さて、JS実行の最後にjs関数をnativeにバインドすることはわかりましたが、nativeはどのようにJS関数を実行するのでしょうか?それを見てみましょう。
Native to JS
まだ覚えていらっしゃるかどうかわかりませんが、nativeがjsコードを実行するとき、コールバック関数があり、イベントによってjavascriptがロードされたことをRCTRootViewに通知します。
// jsコードの実行
 - (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync{
 // jsコード実行コールバック
 dispatch_block_t completion = ^{
 // jsコードの実行が完了したら、js実行イベントキューをリフレッシュする必要がある。
 [self _flushPendingCalls];
 // メインスレッドでRCTRootViewを通知する; jsコードが実行された。RCTRootViewが通知を受け取ると、ハングアップして
 dispatch_async(dispatch_get_main_queue(), ^{
 [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification
 object:self->_parentBridge
 userInfo:@{@"bridge" : self}];
 [self ensureOnJavaScriptThread:^{
 // タイマーが実行され続ける
 [self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop]];
 }];
 });
 };
 if (sync) {
 // jsコードの同期実行
 [self executeApplicationScriptSync:sourceCode url:self.bundleURL];
 completion();
 } else {
 // jsコードの非同期実行
 [self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:completion];
 }
 [self.devSettings setupHotModuleReloadClientIfApplicableForURL:self.bundleURL];
}
RCTJavaScriptDidLoadNotification RCTRootViewはこのイベントをリスンします。
 (void)javaScriptDidLoad:(NSNotification *)notification
{
 // RCTBridgeインスタンスbatchedBridgeを取得する
 RCTBridge *bridge = notification.userInfo[@"bridge"];
 if (bridge != _contentView.bridge) {
 [self bundleFinishedLoading:bridge];
 }
}
- (void)bundleFinishedLoading:(RCTBridge *)bridge
{
 // ...
 [_contentView removeFromSuperview];
 _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds
 bridge:bridge
 reactTag:self.reactTag
 sizeFlexiblity:_sizeFlexibility];
 // RCTBridgeを使ってjsメソッドを呼び出し、ページを開始する
 [self runApplication:bridge];
 // ページを表示する
 [self insertSubview:_contentView atIndex:0];
}
- (void)runApplication:(RCTBridge *)bridge
{
 NSString *moduleName = _moduleName ?: @"";
 NSDictionary *appParameters = @{
 @"rootTag" : _contentView.reactTag,
 @"initialProps" : _appProperties ?: @{},
 };
 // RCTCxxBridgeのenqueueJSCallを呼び出す:method:args:completion: 
 [bridge enqueueJSCall:@"AppRegistry" method:@"runApplication" args:@[ moduleName, appParameters ] completion:NULL];
}
Instance->NativeToJsBridge->JSIExecutorcallFunctionReturnFlushedQueue_bindBridgeはjsの関数にネイティブを指し示すランタイムです。
// RCTxxBridge.mm
- (void)enqueueJSCall:(NSString *)module
 method:(NSString *)method
 args:(NSArray *)args
 completion:(dispatch_block_t)completion{
 if (strongSelf->_reactInstance) {
 // 呼び出されるインスタンス.callJSFunction
 strongSelf->_reactInstance->callJSFunction(
 [module UTF8String], [method UTF8String], convertIdToFollyDynamic(args ?: @[]));
 }
 }];
}
// Instance.cpp
void Instance::callJSFunction(
 std::string &&module,
 std::string &&method,
 folly::dynamic &¶ms) {
 callback_->incrementPendingJSCalls();
 // NativeToJsBridgeのcallFunctionを呼び出す
 nativeToJsBridge_->callFunction(
 std::move(module), std::move(method), std::move(params));
}
// NativeToJsBridge.cpp
void NativeToJsBridge::callFunction(
 std::string &&module,
 std::string &&method,
 folly::dynamic &&arguments) {
 runOnExecutorQueue([this,
 module = std::move(module),
 method = std::move(method),
 arguments = std::move(arguments),
 systraceCookie](JSExecutor *executor) {
 // JSIExecutorでcallFunctionを呼び出す。
 executor->callFunction(module, method, arguments);
 });
}
// JSIExecutor.cpp
void JSIExecutor::callFunction(
 const std::string &moduleId,
 const std::string &methodId,
 const folly::dynamic &arguments) {
// もしまだなら、callFunctionReturnFlushedQueueを例として使うことができる。_とcallFunctionReturnFlushedQueue関数をバインドするjs関数の中で呼び出すと、最初のバインドが行われる
 if (!callFunctionReturnFlushedQueue_) {
 bindBridge();
 }
 Value ret = Value::undefined();
 try {
 scopedTimeoutInvoker_(
 [&] {
 // callFunctionReturnFlushedQueueを呼び出す_ JSのmoduleId、methodId、引数パラメータを渡すと、JS側がキューを返す
 ret = callFunctionReturnFlushedQueue_->call(
 *runtime_,
 moduleId,
 methodId,
 valueFromDynamic(*runtime_, arguments));
 },
 std::move(errorProducer));
 } catch (...) {
 }
// ネイティブモジュールを実行する
 callNativeModules(ret, true);
}
// MessageQueue.js
 callFunctionReturnFlushedQueue(
 module: string,
 method: string,
 args: any[],
 ): null | [Array<number>, Array<number>, Array<any>, number] {
 this.__guard(() => {
 this.__callFunction(module, method, args);
 });
 return this.flushedQueue();
 }
 __callFunction(module: string, method: string, args: any[]): void {
 this._lastFlush = Date.now();
 this._eventLoopStartTime = this._lastFlush;
 const moduleMethods = this.getCallableModule(module);
 moduleMethods[method].apply(moduleMethods, args);
 }
callFunctionReturnFlushedQueue_callFunctionReturnFlushedQueueinvokeCallbackAndReturnFlushedQueueバインディングのための関数のand js側に加えて、flushedQueueもバインディングがあります。invokeCallbackAndReturnFlushedQueue callFunctionReturnFlushedQueueここではあまり説明しませんが、興味のある学生はflushedQueueとチェックアウトに行くことができます;その実装原理と似ています。
フローチャートは記事の最後をご覧ください!
JSからネイティブへ
AppRegistry.runApplication前回のnative to jsでは、ページが開始された状態から開始する話をしましたが、なぜjs to nativeの話をしないのでしょうか? 真面目な話、怠け者というわけではないのですが、RNの初期化の全体的なプロセス、RNのjsbundleロードと実行プロセス、JSへのネイティブ呼び出しの3つの山の基礎を知った上で、JS to nativeについて深く掘り下げていきたいと思います。
jsからネイティブへの移行はかなり複雑なので、まずは全体の流れを見てみましょう。
RNの公式ドキュメントには、iOSとの通信にNativeModulesが使えると書かれています。では、JS側でNativeModulesを使う方法を見てみましょう。
import { NativeModules } from "react-native";
// iOSのネイティブモジュールを手に入れよう:ReactJSBridge
const JSBridge = NativeModules.ReactJSBridge;
// 対応するモジュールの対応するメソッドを呼び出す
JSBridge.callWithCallback();
let NativeModules: { [moduleName: string]: Object, ... } = {};
if (global.nativeModuleProxy) {
 NativeModules = global.nativeModuleProxy;
}
// NativeToJsBridge.cpp
void NativeToJsBridge::initializeRuntime() {
 runOnExecutorQueue(
 [](JSExecutor *executor) mutable { executor->initializeRuntime(); });
}
// JSIExecutor.cpp
void JSIExecutor::initializeRuntime() {
 SystraceSection s("JSIExecutor::initializeRuntime");
 runtime_->global().setProperty(
 *runtime_,
 "nativeModuleProxy",
 Object::createFromHostObject(
 *runtime_, std::make_shared<NativeModuleProxy>(nativeModules_)));
}
NativeModules.自分のモジュール名NativeModuleProxy::getJSINativeModules::getModuleJSINativeModules::createModuleJSINativeModules::createModuleJS側で呼び出すとネイティブ側のメソッドも起動され、同期してメソッドとメソッドが呼び出されます。メソッドの中では、JS側の__fbGenNativeModuleがモジュール情報の取得に使われます。JS側の__fbGenNativeModule関数を確認すると、**__fbGenNativeModule==JS側のgenModuleメソッド**であることがわかりました。
// JSIExecutor.cpp NativeModuleProxy
 Value get(Runtime &rt, const PropNameID &name) override {
 if (name.utf8(rt) == "name") {
 return jsi::String::createFromAscii(rt, "NativeModules");
 }
 auto nativeModules = weakNativeModules_.lock();
 if (!nativeModules) {
 return nullptr;
 }
 return nativeModules->getModule(rt, name);
 }
// JSINativeModules.cpp
Value JSINativeModules::getModule(Runtime &rt, const PropNameID &name) {
 if (!m_moduleRegistry) {
 return nullptr;
 }
 std::string moduleName = name.utf8(rt);
 const auto it = m_objects.find(moduleName);
 if (it != m_objects.end()) {
 return Value(rt, it->second);
 }
 auto module = createModule(rt, moduleName);
 if (!module.hasValue()) {
 return nullptr;
 }
 auto result =
 m_objects.emplace(std::move(moduleName), std::move(*module)).first;
 return Value(rt, result->second);
}
folly::Optional<Object> JSINativeModules::createModule(
 Runtime &rt,
 const std::string &name) {
 if (!m_genNativeModuleJS) {
 m_genNativeModuleJS =
 rt.global().getPropertyAsFunction(rt, "__fbGenNativeModule");
 }
 auto result = m_moduleRegistry->getConfig(name);
 Value moduleInfo = m_genNativeModuleJS->call(
 rt,
 valueFromDynamic(rt, result->config),
 static_cast<double>(result->index));
 folly::Optional<Object> module(
 moduleInfo.asObject(rt).getPropertyAsObject(rt, "module"));
 return module;
}
moduleName,moduleInfoBatchedBridge.enqueueNativeCallJS側のgetModule関数は、ネイティブ・モジュールから渡されたモジュール情報( )を使って、現在実行中の関数をキューに入れます。ネイティブ・モジュールがJSのメソッドを呼び出そうとすると、このキューをネイティブに返し、ネイティブはキュー内のメソッドを実行します。
ネイティブがJSを呼び出さない場合、JSは5msのしきい値を設定し、5ms後にまだネイティブの呼び出しがない場合、JSはキューのリフレッシュをトリガーします。
// NativeModules.js
function genModule(
 config: ?ModuleConfig,
 moduleID: number
): ?{
 name: string,
 module?: Object,
 ...
} {
 const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
 if (!constants && !methods) {
 // Module contents will be filled in lazily later
 return { name: moduleName };
 }
 const module = {};
 methods &&
 methods.forEach((methodName, methodID) => {
 const isPromise =
 promiseMethods && arrayContains(promiseMethods, methodID);
 const isSync = syncMethods && arrayContains(syncMethods, methodID);
 const methodType = isPromise ? "promise" : isSync ? "sync" : "async";
 // genMethodは現在のMethodをキューに入れる。
 module[methodName] = genMethod(moduleID, methodID, methodType);
 });
 Object.assign(module, constants);
 return { name: moduleName, module };
}
// export this method as a global so we can call it from native
global.__fbGenNativeModule = genModule;
function genMethod(moduleID: number, methodID: number, type: MethodType) {
 let fn = null;
 // プロミス型なら実行キューに詰め込む必要がある。
 if (type === "promise") {
 fn = function promiseMethodWrapper(...args: Array<any>) {
 // In case we reject, capture a useful stack trace here.
 const enqueueingFrameError: ExtendedError = new Error();
 return new Promise((resolve, reject) => {
 BatchedBridge.enqueueNativeCall(
 moduleID,
 methodID,
 args,
 (data) => resolve(data),
 (errorData) =>
 reject(updateErrorWithErrorData(errorData, enqueueingFrameError))
 );
 });
 };
 } else {
 fn = function nonPromiseMethodWrapper(...args: Array<any>) {
 const lastArg = args.length > 0 ? args[args.length - 1] : null;
 const secondLastArg = args.length > 1 ? args[args.length - 2] : null;
 const hasSuccessCallback = typeof lastArg === "function";
 const hasErrorCallback = typeof secondLastArg === "function";
 const onSuccess = hasSuccessCallback ? lastArg : null;
 const onFail = hasErrorCallback ? secondLastArg : null;
 const callbackCount = hasSuccessCallback + hasErrorCallback;
 args = args.slice(0, args.length - callbackCount);
 if (type === "sync") {
 return BatchedBridge.callNativeSyncHook(
 moduleID,
 methodID,
 args,
 onFail,
 onSuccess
 );
 } else {
 // を恐れてキューに差し込むことも忘れずに。
 BatchedBridge.enqueueNativeCall(
 moduleID,
 methodID,
 args,
 onFail,
 onSuccess
 );
 }
 };
 }
 fn.type = type;
 return fn;
}
// MessageQueue.js
// 時間のしきい値
const MIN_TIME_BETWEEN_FLUSHES_MS = 5;
enqueueNativeCall(
 moduleID: number,
 methodID: number,
 params: any[],
 onFail: ?Function,
 onSucc: ?Function,
 ) {
 this.processCallbacks(moduleID, methodID, params, onFail, onSucc);
 // モジュール、メソッド名、引数をキューに詰め込む
 this._queue[MODULE_IDS].push(moduleID);
 this._queue[METHOD_IDS].push(methodID);
 this._queue[PARAMS].push(params);
 const now = Date.now();
 // ネイティブがJSを呼び出さない場合、JSは5msの時間しきい値を提供し、5ms以上経ってもネイティブがJSを呼び出さない場合、JSは率先してキューのリフレッシュをトリガーする、つまり、即座にネイティブ側にキューにキャッシュされた一連のメソッドを実行させる。
 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);
 }
 this.__spy({
 type: TO_NATIVE,
 module: moduleID + '',
 method: methodID,
 args: params,
 });
 }
// JSIExecutor.cpp
void JSIExecutor::initializeRuntime() {
 runtime_->global().setProperty(
 *runtime_,
 "nativeFlushQueueImmediate",
 Function::createFromHostFunction(
 *runtime_,
 PropNameID::forAscii(*runtime_, "nativeFlushQueueImmediate"),
 1,
 [this](
 jsi::Runtime &,
 const jsi::Value &,
 const jsi::Value *args,
 size_t count) {
 if (count != 1) {
 throw std::invalid_argument(
 "nativeFlushQueueImmediate arg count must be 1");
 }
 callNativeModules(args[0], false);
 return Value::undefined();
 }));
}
この時点で、jsからnativeへの説明は終わり、次はjsからnativeへの呼び出しで簡単なまとめを行います。
- js to nativejs側のgetModule関数は、現在のモジュールに関する情報をネイティブに返し、現在のmoduleId,methodIdとparamsをキューに詰め込みます。次のリクエストと次のリクエストの間の時間間隔が5ms以上かどうかを比較することで、ネイティブモジュールをすぐに呼び出すために使用されます。
質問ですか?なぜjsはネイティブを直接呼び出さず、キューに詰め込むのですか?
個人的な理解:jsのトリガーネイティブは、実際には非常に頻繁に処理され、あなたはscrollViewスクロール、アニメーションの実装などを想像することができます、非常に大きなパフォーマンスのオーバーヘッドをもたらすでしょう。を最適化します。
まとめ
NativeからJS、JSからNativeのプロセスは上記で学びましたが、ここではJSとNativeが全体としてどのように相互作用するのかを見てみましょう。
JSネイティブ
- getModule->createModuleネイティブの実行が完了し、jsコードがRCTRootViewに時間を送信します。
- js __fbGenNativeModuleRCTRootViewは時刻を受け取り、関数を実行します。
- RCTJavaScriptDidLoadNotificationJS側ではすでにメソッドにバインドされているので、このjs関数が実行されるとcallFunctionメソッドが実行され、jsのapply関数でmodule.methodNameの呼び出しが実行されます。
JSからネイティブへ
- batchedBridge->enqueueJSCalljs側のgetModule関数は、現在のモジュールに関する情報をネイティブに返し、現在のmoduleId,methodId,paramsをキューに詰め込みます。次の2つのリクエスト間の時間間隔が5ms以上かどうかを比較することで、ネイティブモジュールをすぐに呼び出すために使用されます。
ReactNativeとiOS Native通信原理分析シリーズ
この記事がお役に立ちましたら、お気軽にください





