はじめに:実は、元々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通信原理分析シリーズ
この記事がお役に立ちましたら、お気軽にください





