tomoima525's blog

Androidとか技術とかその他気になったことを書いているブログ。世界の秘密はカレーの中にある!サンフランシスコから発信中。

React Nativeの次世代アーキテクチャTurboModuleとJSIの話

stand.fm 社が主催する React Native 勉強会にて Catching up with TurboModules という題名で未だ全容がはっきりしない TurboModules について発表しました。

speakerdeck.com

この記事は発表では伝えきれない部分を補うものとなっています。

現状の Native - React Native のブリッジ実装と課題

現在のバージョン(0.64.x)における React Native からのネイティブコードの呼び出しは Native Module によって実現されています。
Native Module の仕組みは以下のようになっています。

f:id:tomoima525:20201218182603p:plain

Native 関数の呼び出し
React Native は、ModuleRegistoryという JavaScript と Native 層をくっつける C++層を持ち、ここに Native Module のマップ配列を保持しています。Native Module は起動時に ModuleRegistory に登録されます。

Native 関数の実行
実行時はマップ配列から関数を呼び出し、非同期的に処理(NativeToJSBridge.cpp)されます。JavaScript からの呼び出しは Queue として処理され、渡される引数は JSON として扱われます。それゆえレスポンスは Promise ないしは Callback によって実現されています。

この実装における課題は2つあります。

(1) 同期的な処理ができない
非同期的な処理は多くの場合問題になりませんが、同期的に処理したい場合--ユーザーインタラクションに合わせたアニメーション描画、受け取ったデータをリアルタイムで JavaScript 側でハンドルする--といったユースケースでパフォーマンスの制限が出てきます。また JSONシリアライズ/デシリアライズ処理のオーバーヘッドもパフォーマンスへの影響が少なからずあります。

(2) 起動時のオーバーヘッド(Android)
React Native Android 固有の問題として、起動時に NativeModule の関数マッピングを行うため、本来は必要ない NativeModule の初期化(クラスコンストラクタ呼び出し)が発生します。これにより NativeModule が増えると起動時間が長くなる可能性があります。

JSI(JavaScript Interface)

JSI はこれらの問題を包括的に解決する手段です。一言でいうと JavaScriptC++で実装されたオブジェクトのリファレンスを持つことで、C++のメソッドを直接呼び出すことができるインタフェース層です。発想としては Browser における DOM の関数呼び出しに近いでしょう。JSI を介することで、ObjC や java の Native コードを同期的に呼び出すことができます。また、JSON に変換するブリッジコードも不要になります。

JSI の技術要素の定義がはっきり書かれたところは見当たらない*1のですが、 C++JavaScript をバインドするコードを自動化する実装も技術の要素として含まれているようです。

現在、C や C++で書かれていたライブラリを JavaScript で使う場合 emscriptenなどでコンパイルして参照しますが、JSI がより発展すればもっと容易に C/C++で使える可能性もあるのかも、とワクワクしてしまいますね!

TurboModule

TurboModule は JSI を利用して Native コードを呼び出す仕組みです。大まかなフローは以下のようになっています。

  1. global.__turboModuleProxy と呼ばれる JSI を JavaScript に定義します。
  2. Native モジュール(Java/objC)と JSI をバインドします
  3. Native のモジュールを呼び出す際は turboModuleProxy からモジュール名を指定し、Native コードをラップした JSI Object を呼び出します。

図にまとめると以下のようになっています。

f:id:tomoima525:20201218182735p:plain

実はすでに TurboModule の参考実装は ReactNative のプロジェクトに含まれています。SampleTurboModule という、与えられた引数を返すだけのシンプルなサンプルコードです。C++, JS と入り組んでいますが、追ってみましょう。

1. global.__turboModuleProxy の定義

TurboModule の C++オブジェクトのリファレンスを JS 側で持つようにするための JSI の実装になります。プロジェクト内のコードはこちらです。

2.Native Module(Java/objC)と JSI をバインド

JS 側の呼び出しと対となるように、バインドするコードがあります。Java 向けの実装objC 向けの実装があります。

内部的には、Java/objC のメソッドをコールするそれぞれのプラットフォームの C++モジュール*2があり、JSI の method 名や引数をこのモジュールに渡して Native のメソッドを実行できるようにしています。

3. Native のモジュールを呼び出す

JS で呼び出すモジュール(SampleTurboModule)を指定する部分がここにあります。以下のように、モジュールに型情報(Flow-Type)を添加して export するようになっています。

export interface Spec extends TurboModule {
  // Exported methods.
  +getConstants: () => {|
    const1: boolean,
    const2: number,
    const3: string,
  |};
  +voidFunc: () => void;
  +getBool: (arg: boolean) => boolean;
  +getNumber: (arg: number) => number;
  +getString: (arg: string) => string;
  +getArray: (arg: Array<any>) => Array<any>;
  +getObject: (arg: Object) => Object;
  // eslint-disable-next-line @react-native/codegen/react-native-modules
  +getRootTag: (arg: RootTag) => RootTag;
  +getValue: (x: number, y: string, z: Object) => Object;
  +getValueWithCallback: (callback: (value: string) => void) => void;
  +getValueWithPromise: (error: boolean) => Promise<string>;
}

export default (TurboModuleRegistry.getEnforcing<Spec>(
  'SampleTurboModule',
): Spec);

実際にこの TurboModule を呼び出し、メソッドをコールしている箇所がこちらです。

TurboModule は現状使えるのか

答えは Yes、ですがかなり大変です。すでに TurboModuleProxy や TurboModule を管理する TurboModuleManager 自体はすでにかなり以前から導入されています。*3しかしNative Module と JSI とバインドする C++コードは現時点の最新バージョン 0.64.x では自前で書くしかありません。

TurboModule 対応は必要か

アプリケーション開発者

ほぼないと思われます。ただ Android の場合は C++のコードをコンパイルして NDK として利用できる必要があるので、ビルド手順に一手間必要になるかもしれません。

ライブラリ開発者

現状わかっている情報としては、型情報について記載した JS ファイル(Flow-Type で書かれたインタフェースで、JS Spec と呼ばれています)を作る必要がありそうです。これはすでに React Native に組み込まれており、Libraries/Components にあるコンポーネントのファイルを見てみると Spec が定義されています。

export interface Spec extends TurboModule {
  +getConstants: () => {|
    SHORT: number,
    LONG: number,
    TOP: number,
    BOTTOM: number,
    CENTER: number,
  |};
  +show: (message: string, duration: number) => void;
  +showWithGravity: (
    message: string,
    duration: number,
    gravity: number,
  ) => void;
  +showWithGravityAndOffset: (
    message: string,
    duration: number,
    gravity: number,
    xOffset: number,
    yOffset: number,
  ) => void;
}

2020 年 12 月現在、Facebook の React Native チームは JSI と Native をバインドする C++コードを、JS Spec に記載された型情報に基づいて自動生成する仕組みを実装中で、アクティブにコードに手が加えられています。

(おまけ) TurboReactPackage を使って Android アプリの起動の高速化を試みる

前述のとおり TurboModule 関連のコードはすでに React Native にかなり組み込まれています。そのうちのひとつに TurboReactPackage という Native Module のパッケージクラスがあります。 TurboReactPackage を使うと Native Module 初期化時にコンストラクタを生成しないので、起動の初期化が見込めます。

Facebook で React Native Android のパフォーマンス改善を推進するエンジニアが書いたTurboReactPackageに関するブログ記事があり、それを参考に ReactPackage を TurboReactPackage でを置き換えてみました。

検証

今回は auto-link されてない 10 個の Native Module を TurboModule 化して高速化するか見てみます。

残念ながら現在 Android は正しく Native,JS 両方のパフォーマンスをモニタリングできるツールがないです。そこで今回は、react-native-perf-loggerというライブラリを使って起動時の初期化処理の時間を計測しました。これは React Native の内部で利用されているパフォーマンスモニタリングツールである ReactMarker を使って各処理のトラッキングができるライブラリです。端末は Samsung A10e (Android 9) を利用しました。
コードは以下です。

gist.github.com

結果

TurboReactPackageと通常をそれぞれ3回起動して計測した結果が以下です。

f:id:tomoima525:20201218182938p:plain

残念ながら誤差レベルの違い(平均80msec)しかでませんでした。コンストラクタで重い初期化処理を実行している、NativeModule数が更に多い場合は結果は変わるかもしれませんね。

まとめ

今回はJSI および TurboModuleについて色々と調べてみました。UI側のりアーキテクチャであるFiberも含めてアップデートが待ち遠しいですね!