iOSアプリで「保存したはずのデータが消えた」はなぜ起きる?バックグラウンド処理の落とし穴
はじめに
正直に言う。私はやらかした。
自作のデータ計測アプリを作っていたとき、ユーザーが走行データを記録してホームボタンを押した瞬間にデータが吹き飛ぶというバグを作り込んだことがある。テストでは再現しない。シミュレータでは問題ない。なのに実機でだけ起きる、あの感じ。
このバグの根っこは「バックグラウンド処理」だった。
iOS開発をはじめたばかりの人は、たぶんそんなに意識しないと思う。でも少し規模のあるアプリを作るようになると、絶対にぶつかる壁だ。
そもそも「メインスレッド」ってなんだ
まずここから話さないといけない。
iOSのアプリは、画面の描画やユーザーのタップ処理を「メインスレッド」という1本の道で処理している。渋谷のスクランブル交差点と違って、この道は1車線しかない。
重い処理をここに流し込むと、UI全体が固まる。ユーザーからすると「アプリが落ちた?」みたいな体験になる。
だから重い処理はメインスレッドから切り離して、バックグラウンドで動かす。これ自体は正しい判断だ。
ところが。
「切り離す」をやりすぎると今度は別の問題が起きる。
複数のスレッドが同じデータを触ると何が起きるか
ゲームで例えると伝わるかもしれない。
2人のキャラクターが同時に同じアイテムを取ろうとすると、ゲームの内部状態が壊れてバグる、あの現象に近い。プログラミングの世界では「データ競合」と呼ぶ。(こんなバグは最近はないが・・・)
ファイルへの書き込みを例にとろう。
スレッドAが「今から保存するよ」とデータをエンコードしはじめた瞬間、スレッドBが割り込んで同じデータを書き換えた。するとAが保存するのは「壊れかけのデータ」になる。
最悪の場合、ファイルの中身が中途半端な状態で保存される。次の起動時に読み込もうとするとクラッシュ、なんてことも起きる。
私のデータ消失バグも、突き詰めればこれだった。
「シリアルキュー」という解決策
解決策はシンプルだ。
「同時には1つしか処理させない」専用の道を作る。これをシリアルキューと呼ぶ。先ほどのゲームで言えば「アイテムには一人ずつしか近づけない」というルールを設けるイメージだ。
保存処理をこの道に流すようにすると、どんなに他のスレッドが暴れても保存処理だけは行儀よく順番待ちしてくれる。
ちょっとした気づきを言うと、シリアルキューを使い始めると「このコードって本当にメインスレッドで動いてるの?」と至るところで疑問を持つようになる。初めて開発するときにはなかなかここまで気が回らない。
バックグラウンド移行時の「猶予時間」問題
もう一つ、ハマりやすいポイントがある。
ユーザーがホームボタンを押したとき、iOSはアプリを即座に殺さない。数秒間だけ「後始末」をする猶予を与えてくれる。
この猶予を使ってデータを保存する、というのが定石だ。SwiftUIなら scenePhase の変化を監視して、.background に入ったタイミングで保存処理を走らせる。
ここで罠がある。
重い保存処理を同期的に(=呼び出しが終わるまで待つ形で)書いていると、メインスレッドをブロックしてしまう。すると保存中にiOSが「コイツ固まってるな」と判断して強制終了する。保存できないまま死ぬ。
非同期で保存処理を走らせると今度は「猶予時間が終わった後も処理が続く」という問題が出る。保存の途中でアプリが止まる。やっぱりデータが壊れる。
正解は UIApplication.beginBackgroundTask を使ってiOSに「まだ生きてるから待って」と明示的に伝えながら、かつ保存処理自体は非同期で動かすことだ。この二つをセットで使って初めてちゃんと動く。
なぜかこのことを説明している記事が少ないんだよな、と思っている。
「強制終了」したときのデータはどうなる
そこまで対策しても、電源ボタン長押しで強制終了されたら?というケースが残る。
これは正直、諦めていい部分がある。
強制終了はiOSが何の通知もなくプロセスを殺す。猶予すらない。このタイミングで書きかけだったデータは失われる。
ただ、「すでに完了した処理分のデータ」は残せる。記録が終わるたびに即座に保存する、という設計にしておけば、強制終了で失われるのは「最後の未完了分だけ」になる。
クラッシュ復元機能を複雑に作るよりも、「こまめに保存する」設計の方がよほど堅牢だというのが今の私の結論だ。複雑な復元機構を作ると、そこ自体がバグの温床になる(実際になった)。
まとめというか、結局何が言いたいか
バックグラウンド処理は「見えない場所で動く処理」だから、バグが出ても気づきにくい。ユーザーからのフィードバックは「なんかデータ消えた」という一言だけで、再現手順もない。
対策の優先度は高い割に、入門記事ではあまり扱われない領域だと思う。
・保存処理はシリアルキューに乗せる
・バックグラウンド移行を検知して保存する
・beginBackgroundTask で猶予を確保する
・こまめに保存して「失う範囲を最小化する」
この4つを押さえておくだけで、データ消失系のバグはかなり防げる。
最初から全部完璧にやる必要はない。まず「バックグラウンドに入ったら保存する」だけでもいい。それだけで体験はかなり変わる。
この記事は実際に計測アプリ開発中に踏んだバグと、その対処を元にしています。