開発Tips

Flutter化でハマった不具合と解消①: 一部端末で起きる libflutter.so の MissingLibraryException

ネイティブ Android から Flutter へ移行したログ簿をリリースした直後、特定の端末でだけ起動と同時にクラッシュするという報告が届きました。Crashlytics のスタックトレースが示していたのは、Flutter のランタイム本体である libflutter.so のロード失敗——UnsatisfiedLinkError / MissingLibraryException 系のエラーでした。

この記事は、「全端末では起きない」「手元では再現しない」「ネイティブライブラリが原因」という、切り分けが難しいクラッシュにどう向き合ったかの記録です。

症状

  • クラッシュは全端末ではなく一部の端末・一部の配信経路でのみ発生。
  • 開発端末・エミュレータ・手元の実機では一切再現しない
  • スタックは Dart のコードではなく、アプリ起動時のネイティブライブラリ読み込み(.so のロード)で失敗している。

Dart のロジックではなく、ビルド成果物に .so がどう梱包され、端末側でどう展開されるかが焦点だと、この時点で見当がつきます。

なぜ「手元で再現しない」のか

Android アプリのネイティブライブラリは、端末の ABI(arm64-v8a / armeabi-v7a / x86_64 など)ごとに分かれています。Play ストアは App Bundle(AAB) から端末に最適な分割 APK を配って容量を削りますが、この「分割・展開」の過程は、

  • 端末の ABI
  • OS バージョン
  • インストール経路(ストア配信 / 手動 APK / 再インストール)

の組み合わせで挙動が変わります。開発中は自分の端末に丸ごと入るため、分割配信で初めて表面化する問題は手元で再現しません。これが厄介さの正体でした。

切り分けの手順

  1. クラッシュ収集基盤で発生端末の傾向を見る。 Crashlytics で「どの ABI・OS・機種に偏っているか」を集計する。特定 ABI に偏っていれば、.so の梱包漏れや展開失敗を強く疑える。
  2. 配布物(AAB)そのものに問題がないかを確認する。 生成された AAB を展開し、対象 ABI の libflutter.so/アプリの .so が期待どおり含まれているかを検証する。ここで「成果物側に問題はない」と確認できる状態を作るのが重要。
  3. コードではなくパッケージング設定を疑う。 成果物が正しいなら、原因は端末側の展開・ロード条件にある。

ログ簿のケースでは、AAB を検証した範囲では問題は見つかりませんでした。つまり作っているものは壊れておらず、問題は特定環境の組み合わせに起因していた、という結論です。

いちばんの教訓: パッケージング設定を安易に変えない

この種のクラッシュを調べると、必ず「android:extractNativeLibs を切り替える」「レガシーパッケージング(useLegacyPackaging)を変える」といった対処が候補に挙がります。しかし——

「効きそうだから」で .so の梱包・展開に関わる設定を変えると、ある端末では直っても別の端末で新しいクラッシュを生む。

.so を APK 内に圧縮したまま置くか展開して置くか(extractNativeLibsuseLegacyPackaging)は、OS バージョンやインストーラの挙動と密接に絡みます。手元で再現しない問題に対して、再現しない設定変更を重ねると、検証できないまま別の環境を壊すリスクだけが増えます。

ログ簿では、これらの設定は現行のまま変更しないという判断に落ち着きました。AAB が正しいことを確定させたうえで、設定を闇雲に触らない——これが結果的に一番安全でした。

まとめ

  • 「一部端末だけ・手元で再現しない・ネイティブライブラリのロード失敗」が揃ったら、まず ABI/OS で発生傾向を切り分ける
  • 配布物(AAB)に問題がないかを先に確認する。作っているものに問題がないと分かれば、闇雲な変更を止められる。
  • .so の梱包・展開に関わるパッケージング設定は安易に変えない。再現しない環境を、再現しない変更でさらに壊さない。

派手な修正ではありませんが、「触らない」という判断に自信を持つために、成果物の検証と発生傾向の分析に時間を使う——それがネイティブ境界のクラッシュへの向き合い方だと学びました。