元記事: http://tadeuzagallo.com/blog/react-native-bridge/
翻訳については https://github.com/januswel/react-native-bridge-ja で管理している。
また、元記事では文字で説明されている部分も含めて実行時の簡単なフローを描いてみた。ここで bundle
というのは React Native packager によって bundle されたファイルのこと。
というわけで、以下翻訳。
この投稿では React Native の基礎を知っている方を対象に、ネイティブと JavaScript の通信時における内部の動作へ焦点をあてます。
メインスレッド
何よりも先に、 React Native では 3 つの主要なスレッド1があることを覚えておいてください。
- シャドウキュー
- コンポーネント再配置時に使用されます
- メインスレッド
- UIKit が使用します
- JavaScript スレッド
- あなたの JavaScript コードが実際に走ります
さらに、すべてのネイティブモジュールは指定されないかぎりそれぞれの GCD2 キューを持っています ( 詳細についてはこれから述べます ) 。
ネイティブモジュール
もしあなたがネイティブモジュールの作り方を知らない場合、先にドキュメントを確認されることをおすすめします。
ここでは双方向、 JavaScript から呼ばれ JavaScript を呼ぶ、 Person
というネイティブモジュールを例に挙げます。
@interface Person : NSObject <RCTBridgeModule>
@end
@implementation Logger
RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(greet:(NSString *)name)
{
NSLog(@"Hi, %@!", name);
[_bridge.eventDispatcher sendAppEventWithName:@"greeted"
body:@{ @"name": name }];
}
@end
これら 2 つのマクロ、 RCT_EXPORT_MODULE
と RCT_EXPORT_METHOD
について焦点をあてましょう。どんなものに展開されるか、その役割とは何か、そこからどのように動くかといったことです。
RCT_EXPORT_MODULE([js_name])
名前が示すとおり、あなたのモジュールをエクスポートします。が、このときの「エクスポート」とはどういう意味でしょうか ? これは「ブリッジ」にあなたのモジュールを認識させることなのです。
その定義はとてもシンプルなものです。
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString \*)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
これは次に挙げることをしています。
- まず
RCTRegisterModule
をextern
関数として宣言しています。これは、関数の実装がコンパイラーから見えないですが、リンク時に使用可能であることを意味しています。 - 次に、任意のマクロパラメーター
js_name
を返すmoduleName
というメソッドを宣言しています。ここではあなたのモジュールが Objective-C のクラス名ではなく、 JavaScript での名前を持ってほしいからですね。 - 最後に、「ブリッジ」にこのモジュールを認識させるため、上で定義した
RCTRegisterModule
関数を呼び出すload
メソッドを宣言しています。アプリはメモリー上にロードされた際、すべてのクラスに対してこのload
メソッドを呼び出します。
RCT_EXPORT_METHOD(method)
このマクロは「より興味深い」です。あなたのメソッドには何もつけ加えませんが、指定されたメソッド名の宣言に加えて、新しいメソッドを作成します。
新しく定義されるメソッドは例のようなものになります。
+ (NSArray *)__rct_export__120
{
return @[ @"", @"log:(NSString *)message" ];
}
「いったいどうなってるんだ ?」というのはとてもいい反応です。
これは次の要素を連結して生成されています。
- プリフィクス
__rct_export__
- 任意の
js_name
- 上の例は
js_name
が空の場合です
- 上の例は
- 宣言されている行の行数
-
__COUNTER__
マクロの値
このメソッドの目的は任意の js_name
とメソッドシグネチャーを含む配列を返すだけです。名前の生成は単にメソッド名の衝突を避けているだけです。
実行時
これらすべての準備は「ブリッジ」へ情報を提供するためのものです。ですのでモジュールやメソッドなど、「ブリッジ」はエクスポートされているすべてのものを探すことができます。ただしすべてロード時にエクスポートされます。では、これらが実行時にどのように使われるか見ていきましょう。
次の図は「ブリッジ」初期化時の依存関係を表しています。
モジュールの初期化
新しい「ブリッジ」インスタンスが生成された際、すべての RCTRegisterModule
関数がすることはあとで「ブリッジ」が探せるようにそのクラス自身を配列に追加することです。「ブリッジ」はモジュールの配列を参照して、すべてのモジュールについて「ブリッジ」上の自身への参照を格納し、「ブリッジ」への参照を提供するインスタンスを生成します。だから双方向と呼べるわけですね。そしてすべての他のモジュールから切り離すために新しいキューを与えないかぎり、どのキューで走るべきかを確認します。
NSMutableDictionary *modulesByName; // = ...
for (Class moduleClass in RCTGetModuleClasses()) {
// ...
module = [moduleClass new];
if ([module respondsToSelector:@selector(setBridge:)]) {
module.bridge = self;
}
modulesByName[moduleName] = module;
// ...
}
モジュールの設定
一度モジュールが初期化されると、バックグラウンドスレッドではそれぞれのモジュールのすべてのメソッドを列挙し、 __rct_export__
ではじまるメソッドを呼び出します。こうすることでメソッドシグネチャーの文字列を得ることができます。パラメーターの型も知ることができるためこれは重要です。たとえば、実行時にはパラメーターが id
型だと知ることはできるでしょう。ただしこの方法ではそれが NSString *
型であることまでわかるのです。
unsigned int methodCount;
Method *methods = class_copyMethodList(moduleClass, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
IMP imp = method_getImplementation(method);
NSArray *entries = ((NSArray *(*)(id, SEL))imp)(_moduleClass, selector);
//...
[moduleMethods addObject:/* Object representing the method */];
}
}
JavaScript の実行環境を整える
JavaScript の実行環境は、より重い処理をバックグラウンドスレッドで実行可能な -setUp
メソッドを持っています。たとえば JavaScriptCore の初期化などです。すべての実行環境ではなく、アクティブな実行環境のみが setUp
の呼び出しを受けるため、いくらか処理の節約になります。
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
_context = [[RCTJavaScriptContext alloc] initWithJSContext:ctx];
JSON の設定を注入する
我々のモジュールの情報のみを持つ JSON の設定は次のようになります。
{
"remoteModuleConfig": {
"Logger": {
"constants": { /* If we had exported constants... */ },
"moduleID": 1,
"methods": {
"requestPermissions": {
"type": "remote",
"methodID": 1
}
}
}
}
}
これは JavaScript VM 上にグローバル変数の形で定義される格納場所です。そのため「ブリッジ」の JavaScript 側が初期化される際、モジュールを生成するためにこの情報を使うことができます。
JavaScript コードの読みこみ
これはとても直感的ですね。指定されたすべてのプロバイダーからソースコードを読みこむだけです。たいていの場合、開発時はパッケージャーからソースをダウンロードし、本番ではディスクから読みこむことになるでしょう。
JavaScript コードの実行
一度準備が整うと、 JavaScriptCore VM 上でアプリケーションのソースコードを読みこめるようになります。 VM はソースをコピーし、パースし、実行するでしょう。最初の実行ではすべての CommonJS モジュールを登録し、エントリーポイントのファイルを require します。
JSValueRef jsError = NULL;
JSStringRef execJSString = JSStringCreateWithCFString((__bridge
CFStringRef)script);
JSStringRef jsURL = JSStringCreateWithCFString((__bridge
CFStringRef)sourceURL.absoluteString);
JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx,
execJSString, NULL, jsURL, 0, &jsError);
JSStringRelease(jsURL);
JSStringRelease(execJSString);
JavaScript モジュール
上で示された JSON 設定から生成されたモジュールは react-native
の NativeModules
オブジェクトを通して JavaScript から使用可能になります。例えば、
var { NativeModules } = require('react-native');
var { Person } = NativeModules;
Person.greet('Tadeu');
これは次の仕組みで動作します。メソッドの呼び出しはモジュール名・メソッド名・すべての引数を含めてキューに積まれます。 JavaScript 実行の最後で、このキューが呼び出しを処理するネイティブ側に渡されるのです。
呼び出しサイクル
上のコードでモジュールを呼び出した場合、次の図に示されることがおこります。
呼び出しはネイティブ側からはじまらなければなりません3。実行にあたって NativeModules
のメソッドを呼ぶことで JavaScript を呼び出します。 NativeModules
はネイティブ側で実行される呼び出しをキューに積みます。 JavaScript 側が完了すると、ネイティブ側はキューに積まれた呼び出し群を参照し、それらを実行します。 JavaScript のコールバックや呼び出しは「ブリッジ」を経由して再び JavaScript 側で実行されます。その際、 _bridge
インスタンスを使うことでネイティブモジュールを通した enqueueJSCall:args:
の呼び出しが可能になります。
注意: React Native プロジェクトを追っている方はかつてネイティブ側から JavaScript 側の呼び出しにおいてもキューが同じように使われていたことをご存知かもしれません。それは vSYNC のたびに実行されるため、起動時間を短縮するために削除されました。
引数の型
ネイティブ側から JavaScript 側を呼び出す場合、引数は JSON に変換されただけの NSArray
として渡されます。が、 JavaScript 側からの呼び出しではネイティブ側の型が必要となります。 int, float, char など組み込み型を明示的にチェックするためです。しかし上で言及しているように、実行系がすべてのオブジェクトや構造体に対して NSMethodSignature
から十分な情報を渡せるとは限りません。そのため、型情報を文字列として保存するのです。
メソッドシグネチャーから型情報を得るために正規表現を使います。また、オブジェクト変換のために RCTConvert
というユーティリティークラスを使います。これは標準でサポートされているすべての型のためのメソッドを持っています。 JSON を任意の型に変換するメソッドです。
struct
でない限り、メソッドを動的に呼び出すには objc_msgSend
を使います。 objc_msgSend_stret
の arm64 版が存在せず、 NSInvocation
を使わざるを得ないからです。
すべての引数を変換するとすぐに、目的のモジュールとメソッドを呼び出すためにまた別の NSInvocation
を使います。
次は例です。
// たとえば MyModule モジュールなどで次のメソッドを定義していた場合
RCT_EXPORT_METHOD(methodWithArray:(NSArray *) size:(CGRect)size) {}
// そして JavaScript 側で次のように呼び出します
require('NativeModules').MyModule.method(['a', 1], {
x: 0,
y: 0,
width: 200,
height: 100
});
// JavaScript キューは次のようにネイティブ側へ送られます
// ** 呼び出しのキューなので、すべて配列であることを忘れないで下さい
@[
@[ @0 ], // モジュール ID 群
@[ @1 ], // メソッド ID 群
@[ // 引数群
@[
@[@"a", @1],
@{ @"x": @0, @"y": @0, @"width": @200, @"height": @100 }
]
]
];
// これは次の擬似コードのような呼び出しに変換されます
NSInvocation call
call[args][0] = GetModuleForId(@0)
call[args][1] = GetMethodForId(@1)
call[args][2] = obj_msgSend(RCTConvert, NSArray, @[@"a", @1])
call[args][3] = NSInvocation(RCTConvert, CGRect, @{ @"x": @0, ... })
call()
スレッド実行
上で言及しているように、すべてのモジュールはデフォルトでそれぞれの GCD キューを持っています。モジュールが実行されるべきキューの指定は -methodQueue
を実装するか、 methodQueue
プロパティを有効なキューにすることで可能です。例外は RCTViewManager
を継承している View Managers
4 です。これはデフォルトでシャドウキューを使います。また、RCTJSThread
は特別で、キューではなくスレッドを使います。ちなみに RCTJSThread
はただのプレイスホルダーです。
現在のスレッド実行における「ルール群」は次です。
-
-init
と-setBridge
はメインスレッドで呼び出されることが保証されています - すべてのエクスポートされたメソッドは対象キュー上で呼び出されることが保証されています
-
RCTInvalidating
プロトコルを実装した場合、invalidate
も対象キュー上で呼び出されることが保証されています -
-dealloc
がどのスレッドから呼び出されるかの保証はありません
JavaScript 側から連続して呼び出しを受けた場合、対象キューによってまとめられ、並行に実行されます。
// `buckets` の中の `calls` を `queue` がまとめている
for (id queue in buckets) {
dispatch_block_t block = ^{
NSOrderedSet *calls = [buckets objectForKey:queue];
for (NSNumber *indexObj in calls) {
// 実際の呼び出し
}
};
if (queue == RCTJSThread) {
[_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
} else if (queue) {
dispatch_async(queue, block);
}
}
最後に
「ブリッジ」がどのように動作するか、概観から少し踏み込んでみました。より複雑なモジュールを作りたい方やコアフレームワークをより有用なものにしたい方の一助になれば幸いです。
不明点やもっと簡単な説明が欲しい場合、他のハナシ聞きたいなどありましたら、お気軽にどうぞ !!