iOSプログラミング:NSUserDefaultsのsynchronizeメソッドは気安く使ってはいけない

iPhoneアプリで、アプリの設定などを保存しておくのにまず使うのがNSUserDefaultsクラス。

// オブジェクトへの参照
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];

// データの保存
[ud setObject:myNumber forKey:@"My Number"];

// データの読み出し
NSNumber *myNumber = [ud objectForKey:@"My Number"];

といった感じ。
昨年、実は自分ひとりしかユーザーがいないけどApp Storeで絶賛公開中の俺アプリ「潜水日記」というのを開発した時にも悩んだのだけど、このNSUserDefaultsにはsynchronizeメソッドというのがあります。その名の通り、アプリが高速に参照できる一時的なメモリ領域と、最終的に保管する領域を同期させるメソッドです。
synchronizeメソッドは、保存を確実にするために呼び出しを推奨するような書き込みをネットで見かけます。自分も過去に経験した他のプラットフォームで、DBのコネクションだの、ファイルアクセスだの、いろいろなAPIsynchronizeメソッド的なのがあると、お約束として呼ぶクセがあるのですが、それはiOS11ではアウトだと気付きました。
顕著にその悪影響が露呈するのが写真の保存。たとえばUIImagePickerControllerクラスで写真を撮ったり、フォトライブラリから写真を選んでアプリのUIImageに貼るだけの下記のようなアプリ。

わざわざ写真をサムネイルに縮小してからNSDataオブジェトをNSUserDefaultsに保存しても、synchronizeメソッドだけで30秒とか尋常じゃない時間がかかりました。アプリのユーザーから見るとiPhoneが固まったみたい。最初はどこかのコードでメモリリークしているのかと思いました。
原因を調べていたら、本家アップルのリファレンスマニュアルのsynchronizeメソッドにこんな注意書きがありました。

Waits for any pending asynchronous updates to the defaults database and returns; this method is unnecessary and shouldn't be used.

この記述で大事なのは2点。

  1. ペンディングされている非同期の更新を待つということは、裏を返せばNSUserDefaultsはペンディングされる理由がなければほっといても非同期で粛々と更新が動くらしいこと。
  2. synchronizeメソッドは必要ないし、使ってはいけないこと。

うーん。必要ないならDepricatedしろよっていいたくなりますねw
推奨している人のブログを読んでいると、以前のiOSはこの記述なかったみたいです。自分の記憶でもiOS10で遅い症状は確認したもののマニュアルにこのような記述があった記憶がありません。iOS12あたりでDepricatedになりそうな気もします。
しかし、synchronizeメソッドを削除したこのテストアプリでいろいろ試したところ、以下のケースでは写真が保存されないことがありました。

  1. デバッガで終了
  2. アップスイッチャーでアプリ終了

デバッガは仕方がないとして、アップスイッチャーでアプリの終了には対応したいですね。
そこでNSUserDefaultsを変更しているところのコードではsynchronizeを一切呼ばないようにして、標準でプロジェクトに生成されるクラスAppDelegate.mに以下の2点を追記しました。

- (void)applicationDidEnterBackground:(UIApplication *)application {
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  [ud synchronize];
}
- (void)applicationWillTerminate:(UIApplication *)application {
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  [ud synchronize];
}

いまのiOS11ではアップスイッチャーになるとapplicationDidEnterBackgroundメソッドが呼ばれるのでそこでsynchronizeメソッドを呼ぶようにしました。あとはアプリが終了直前に呼ばれるハズのapplicationWillTerminateメソッド。だけどiOS11ではアップスイッチャーで終了するケースでは呼ばれないようです。じゃあDepricatedでもないのに、いつapplicationWillTerminateメソッドが呼ばれるんだよってのはまだ把握していないけど、念のため書きました。(というようなモグラ叩きな発想はいけませんねーw)予想ではメインメモリが足りなくなってiOSに強制的に終了されるケースです。
このワークアラウンドで「潜水日記」アプリは固まらず、かつ画像が今の所100%保存されています。