ネイティブブリッジ(JNI・JSI)
アプリが重い・カクつく原因は言語間の橋渡しにあることが多い。JNIのオーバーヘッドとJSIが直接バインディングでそれをどう消したかが分かる。
- 1.JNIはJVMとネイティブコードの間でスレッドをアタッチし引数をマーシャリングする重い呼び出しで、頻度の高いFFI呼び出しはボトルネックになりやすい。
- 2.従来のReact NativeブリッジはJSとネイティブ間をJSON文字列にシリアライズして非同期メッセージパッシングする方式で、シリアライズコストと非同期ゆえの遅延が付きまとった。
- 3.JSIはJavaScriptエンジンにC++オブジェクトを直接持たせ、シリアライズもメッセージキューも経ずに同期関数呼び出しでネイティブを叩けるようにした。
言語の壁をどう越えるか
モバイルアプリの多くは複数の実行環境をまたいでいます。AndroidならKotlin/JavaのコードがJVM上で動きつつ、画像処理や暗号化などはC/C++の共有ライブラリに任せたい。React Nativeなら、開発者が書くJavaScriptとOS標準のネイティブUIコンポーネントの間に、そもそも別の言語・別のランタイムという壁があります。この壁を越える仕組みが甘いと、越境のたびにコストがかかり、越境回数が多い処理(スクロール中の頻繁な描画更新など)でアプリ全体が重くなります。JNIとJSIは、まったく別の時代・別の設計思想で「言語の壁をどう安く越えるか」に答えを出した二つの仕組みです。
JNI:JVMとネイティブの間の正式な手続き
JNI(Java Native Interface)は、JVM上のJava/KotlinコードとC/C++の共有ライブラリを相互に呼び出すための標準インターフェースです。仕組みを理解する鍵は「JVMとネイティブスレッドは別世界である」という前提です。
Java側からnative修飾子を付けたメソッドを呼ぶと、JVMはロード済みの共有ライブラリ内の対応するシンボル(Java_パッケージ名_クラス名_メソッド名という命名規則の関数)を探して呼び出します。このとき単に関数を呼ぶだけでは済みません。
| 処理 | 内容 | コストが発生する理由 |
|---|---|---|
| スレッドのアタッチ | ネイティブスレッドがJVMに未登録なら JNIEnv を割り当てて紐付ける | JVMのGCやモニタと安全に協調するための管理構造が必要 |
| 引数のマーシャリング | Javaオブジェクトの参照をネイティブ側で扱える形式に変換 | GC対象のJavaヒープと非GC対象のネイティブメモリでは表現が異なる |
| ローカル参照の管理 | ネイティブ側で保持するJavaオブジェクト参照をJNIEnv経由で登録・解放 | GCが参照を見失わないよう明示的な参照カウント相当の管理が要る |
| 例外の橋渡し | ネイティブ側のエラーをJava例外として積み直す | C/C++には例外機構がないため手動でJVM側に例外を設定する |
配列や文字列を渡す場合はさらに重くなります。Java のStringはUTF-16の内部表現を持つため、ネイティブ側でUTF-8として使うにはGetStringUTFCharsのような変換関数を通す必要があり、これはコピーを伴います。配列もGet<Type>ArrayElementsでJVM実装によってはヒープ領域をコピーしてネイティブ側に渡し、書き戻し時にまたコピーが走ります。つまりJNI呼び出し1回あたり、関数呼び出し自体のコストに加えて、スレッド管理・参照登録・データ変換という何重ものオーバーヘッドが乗ります。
ネイティブの関数呼び出しそのものは数ナノ秒でも、JNI経由では数百ナノ秒から数マイクロ秒のオーバーヘッドが乗ることが珍しくありません。原因は速度が遅い処理系だからではなく、GC管理下のJVMヒープと管理外のネイティブメモリという異なるメモリモデルの整合性を、呼び出しごとに保証する必要があるためです。これゆえ実務では「ネイティブ層を細かく何度も呼ぶ」のではなく「まとめて1回呼ぶ」設計(バッチ化)が定石になります。
ブリッジ方式:JSON文字列に載せた非同期メッセージ
React Nativeは当初、JavaScriptCoreというJSエンジン上でJSスレッドを動かし、ネイティブ側のUIスレッドとは完全に切り離す設計を取りました。両者は同じアドレス空間で直接オブジェクトを共有できないため、通信は「ブリッジ」と呼ばれるメッセージキューを介して行われます。
流れはこうです。JS側でネイティブモジュールのメソッドを呼ぶと、呼び出し内容(モジュール名・メソッド名・引数)はJSON文字列にシリアライズされ、非同期メッセージとしてネイティブ側のキューに積まれます。ネイティブ側はキューをポーリングして取り出し、対応するネイティブメソッドを実行し、戻り値があれば再びJSONにシリアライズしてJS側に返します。
この設計には二つの明確なコストがありました。ひとつはシリアライゼーションコストです。すべての引数と戻り値をJSON文字列に変換し、パースし直す作業はデータ量に比例して重くなり、頻繁な呼び出し(例えばスクロール位置の更新やアニメーションのフレームごとの値渡し)では無視できない負荷になります。もうひとつは非同期性そのものが持つ遅延です。呼び出しは即座に結果を返さず、必ずメッセージキューを経由してイベントループの次のタイクルまで待つため、同期的に値が欲しい場面(レイアウト計算の途中結果を即座に使うなど)では原理的に不向きでした。
JSI:シリアライズもキューも経ない直接バインディング
JSI(JavaScript Interface)は、この非同期メッセージパッシングそのものを排除する設計です。核心のアイデアは、JavaScriptエンジン(Hermesなど)が内部で使うC++のHostObjectという仕組みを使って、ネイティブのC++関数やオブジェクトをJavaScriptの値であるかのようにJS側から直接参照させることです。
JSI経由でネイティブ関数を呼ぶとき、JSON文字列への変換は起きません。JS側の呼び出しは、JSエンジンのC++実装内で直接C++関数ポインタを辿って実行され、戻り値もJSの値としてそのまま返ります。呼び出しは同期的に完結するため、JSスレッドはネイティブ側の処理結果をその場で受け取って次の行に進めます。
| 観点 | 旧ブリッジ方式 | JSI |
|---|---|---|
| データ受け渡し | JSON文字列にシリアライズ/デシリアライズ | C++オブジェクトへの直接参照。変換不要 |
| 通信方式 | 非同期メッセージキュー | 同期関数呼び出し(必要なら非同期も選べる) |
| 呼び出しの往復 | キューに積んでから消費されるまで遅延 | 即座に戻り値を受け取れる |
| 共有メモリ | 不可。値は都度コピー | HostObjectやArrayBufferを介して参照を共有可能 |
| 適した処理 | 低頻度・大きめのペイロード | 高頻度・小さなペイロード(毎フレームの値渡しなど) |
この直接バインディングによって恩恵を受けるのは、特に高頻度に発火する処理です。例えばアニメーションライブラリが毎フレームごとにアニメーション値をネイティブ側に渡す場合、旧方式ではフレームごとにJSONシリアライズと非同期往復が発生し、60fpsを維持しづらくなります。JSIならその場でC++関数を同期的に呼べるため、往復コストがほぼ関数呼び出し相当まで下がります。
JSIが消すのはブリッジ特有のシリアライズと非同期往復のコストであり、JNI自体が持つJVM↔ネイティブ間のオーバーヘッドを消すわけではありません。AndroidでJSIから先の実装がJavaクラスを呼ぶ構成であれば、結局その区間ではJNIのスレッドアタッチやマーシャリングが発生します。したがって高頻度処理は可能な限りC++層で完結させ、JNIをまたぐ回数自体を減らす設計が引き続き重要です。
まとめ
JNIはJVMとネイティブコードという異なるメモリモデル・異なる実行環境を安全に橋渡しするための正式な手続きであり、スレッドのアタッチ・引数のマーシャリング・参照管理という多層のコストを呼び出しのたびに払います。React Nativeの旧ブリッジは、JSとネイティブという同様の壁をJSON文字列と非同期メッセージキューで越えており、シリアライズコストと非同期特有の遅延という制約を抱えていました。JSIはJavaScriptエンジンにC++オブジェクトを直接持たせることでこの壁を薄くし、シリアライズも非同期往復も経ない同期呼び出しを可能にしましたが、その先でJNIのような別のFFI境界をまたぐ場合はそちらのコストがなお残ります。結局のところ、越境の回数を減らし、越境1回あたりのペイロードをまとめることが、どの時代のブリッジ設計にも共通する最適化の要諦です。
モバイル開発 Article
ネイティブブリッジ(JNI・JSI)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
JNI
比較で見る軸
難易度: advanced / カテゴリ: モバイル開発 / タグ数: 6
導入後に効く点
従来のReact NativeブリッジはJSとネイティブ間をJSON文字列にシリアライズして非同期メッセージパッシングする方式で、シリアライズコストと非同期ゆえの遅延が付きまとった。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- モバイル開発
- タグ数
- 6
判断チェックリスト
- 自社の用途が「JNI / JSI」に近いか確認する。
- 強みである「JNIはJVMとネイティブコードの間でスレッドをアタッチし引数をマーシャリングする重い呼び出しで、頻度の高いFFI呼び出しはボトルネックになりやすい。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。