React NativeがExpo SDKが要求するバージョンと差異がある場合にクラッシュする問題
Expo SDK 57を使っているReact Nativeアプリで、WebViewを開こうとすると実機のiOSアプリでクラッシュする問題に遭遇した。原因はExpo SDKが要求するReact Nativeと、実際にインストールされていたReact Nativeのバージョンの差異だった。0.86.0と0.86.2というパッチバージョンの違いだけでも、ネイティブ側では実行時クラッシュにつながることがある。めちゃくちゃハマったので今後のためにログとして残しておく。
発生したエラー
クラッシュ時には次のエラーが出ていた。
*** Assertion failure in -[RCTComponentViewFactory createComponentViewWithComponentHandle:](),
RCTComponentViewFactory.mm:211
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'ComponentView with componentHandle `4901207840` (`<garbled>`) not found.'componentHandleの値は実行するたびに異なり、その後ろに表示されるコンポーネント名も文字化けしていた。DebugビルドだけでなくReleaseビルドでも再現した。最初はWebページ画面で使っているreact-native-webviewを疑ったが、WebViewを外してもクラッシュした。
原因はReact Nativeのpatch version
使用していた主なバージョンは次の通り。
{
"expo": "~57.0.8",
"react-native": "0.86.2"
}しかし、インストール済みのexpo@57.0.8が要求するReact Nativeは0.86.0だった。要求されるバージョンは次のコマンドで確認できる。
node -p "require('./node_modules/expo/bundledNativeModules.json')['react-native']"また、expo prebuildコマンドでも差異を警告していた。
Using react-native@0.86.2 instead of recommended react-native@0.86.0.expo-modules-coreは、Expo SDKが要求するReact Nativeに対してコンパイルされたxcframeworkとして配布される。一方、アプリ側のReact本体にはreact-native@0.86.2のReact.frameworkが入っていた。そのため、FabricのcomponentHandleを登録するExpoModulesCore.framework側と、それを検索するReact.framework側が異なるバージョンのReact Nativeを前提に動作していた。両者の間でC++オブジェクトをやり取りした結果、ABIの不整合によってhandleが壊れたと考えられる。エラーに表示されたコンポーネント名が毎回異なる文字化けになっていたことも、handleが有効な文字列を指していなかったと考えると説明がつく。
修正方法
以下のようにReact NativeをExpo SDK 57.0.8が要求するバージョンへ戻したことでクラッシュしなくなった。
- "react-native": "0.86.2"
+ "react-native": "0.86.0"
expo install --checkにも注意する
調査中にnpx expo install --checkを実行すると、いくつかのパッケージについて推奨バージョンとの差異が報告された。しかし、それらを指示通りに揃えてもクラッシュは直らなかった。今回の環境では、expo install --checkと--fixはインストール済みのexpo@57.0.8ではなく、同じSDK世代の新しいパッチリリースを基準にバージョンを解決していた。そのため、expo@57.0.8を残したままreact-native@0.86.2を推奨する状態になっていた。
インストール済みExpoパッケージに対応する正確なバージョンを知りたい場合は、node_modules/expo/bundledNativeModules.jsonを確認するのが確実だった。expo prebuildが出すバージョン差異の警告も、単なる参考情報ではなくエラーとして扱った方がよい。
Expo SDKをアップグレードするときは、expo本体を更新した後にnpx expo install --fixを実行し、SDKが管理する依存関係をまとめて更新する。
自動更新による再発を防ぐ
今回のreact-native@0.86.2への更新は、Dependabotによる自動更新によるものだった。再発防止として、React NativeアプリについてはDependabotの自動更新から次のパッケージを除外した。
expo,expo-*,@expo/*react,react-domreact-native,react-native-*
まとめ
React Nativeでは、パッチバージョンの違いだから安全とは限らない。特にExpoがビルド済みのネイティブバイナリを配布している場合、Expo SDKが要求するバージョンとのわずかな差異でもABI不整合によるクラッシュにつながる可能性がある。
今回得た教訓は次の3つ。
expo prebuildのバージョン差異の警告は無視しない- 実際にインストールしたExpoが要求するバージョンは
bundledNativeModules.jsonで確認する - Expo SDK管理下の依存関係は自動で個別更新せず、SDKとまとめて更新する