iOS OCRアプリのメモリ起因クラッシュの調査と対策
KotenOCRは古典籍のOCR処理にONNX Runtimeを使用しているiOSアプリです。6つのONNXモデル(合計約230MB)を搭載しています。ダウンロード数が300に達した時点で、クラッシュ率が6.7%(20件)に上昇していることが確認されました。Xcode Organizerにクラッシュログが表示されなかったため、原因の特定には別のアプローチが必要でした。 調査体制 4つの調査方針を並行して進めました。 メモリ・モデルサイズの調査 画像処理パイプラインの調査 ONNX Runtimeのスレッドセーフティの調査 カメラ・UIライフサイクルの調査 原因の特定 調査の結果、以下の4つの問題が確認されました。影響度の大きい順に記載します。 1. 起動時の全モデル一括ロード 6つのONNXモデルをアプリ起動時にすべてメモリに展開していました。ディスク上では約230MBですが、ONNX Runtimeがモデルをメモリに展開すると1.5〜2.5倍程度に膨張するため、RAM上では推定350〜550MB程度を占有していたと考えられます。 iOSにはjetsam(カーネルレベルのメモリ監視機構)があり、アプリ単位のメモリ上限を超えるとプロセスが強制終了されます。デバイスごとのRAMとアプリあたりのメモリ上限の目安は以下のとおりです。 デバイス RAM リスク iPhone 8 2GB CRITICAL — iOS 16対応機種で最もメモリが少ない iPhone X / XR / XS 3GB HIGH iPhone 11 4GB MEDIUM iPhone 12以降 4〜6GB LOW iPhone 8やiPhone Xでは、アプリあたりのメモリ上限が200〜300MB程度とされており、全モデル一括ロードだけで上限を超過する状況でした。 2. DEIMDetectorの画像前処理 DEIMDetectorが入力画像をリサイズせずにフルサイズのまま正方形パディング用のCGContextを生成していました。12MPの写真(4032×3024)の場合、4032×4032ピクセルのCGContextが作成され、これだけで約63MBのメモリを消費します。 3. 無制限の並列認識タスク withThrowingTaskGroupで最大100件の認識タスクを同時に実行可能な状態でした。各タスクがモデル推論時にメモリを確保するため、並列数に比例してメモリ使用量が増加します。 4. メモリ警告のハンドリング不在 UIApplication.didReceiveMemoryWarningNotificationを購読しておらず、iOSからのメモリ警告を受け取っても何も対処していませんでした。jetsam発動前の最後の解放機会を逃していたことになります。 実施した修正 Lazy Model Loading 起動時にすべてのモデルをロードする方式から、選択中のモードに必要なモデルのみをロードする方式に変更しました。初期メモリ使用量が約350MBから約150MBに減少しました。 func loadModelsForMode(_ mode: OCRMode) { // 選択中のモードに必要なモデルのみロード let requiredModels = mode.requiredModelKeys for key in requiredModels { if sessionCache[key] == nil { sessionCache[key] = try? createSession(for: key) } } } 画像前処理の改善 DEIMDetectorの前処理で、パディング前にリサイズを行うように変更しました。メモリ使用量が63MBから約2.4MBに削減されました。 ...


