iOSのウィジェットに新機能を追加したときにメモリで苦しんだ話

1. はじめに

こんにちは、UX部RetentionグループにてiOS開発を担当している山崎です。

先日のアップデートにてiOSのウィジェット機能を大きく拡充したのですが、みなさん使っていただけましたでしょうか?
各通貨の騰落率をさっと確認する上でとても便利なものに仕上がったのでまだ使っていない方はぜひ使ってみてください!

↓ ウィジェットの設定方法がわからない方はこちら
https://faq.coincheck.com/s/article/40116?language=ja

f:id:cc-yamazaki:20200701132945p:plain

今回のアップデートでの主な変更点は

①ミニチャートがウィジェット中央部分に追加された
②騰落率順にウィジェットの通貨を並び替えできるようになった

の2点です。今回この機能追加をするにあたって想定外の「メモリ」問題に苦しめられたので、これについて解説していきたいと思います。


2. メモリ問題に気づくまで

メモリ問題に気づくまでは、TodayViewControllerにAPIから返ってきた各通貨のレート情報をチャートで描画する、タブの切り替えに応じて通貨をソートさせるという実装を粛々と行っていました。ちなみにチャートの描画はChartsというグラフの描画などでよく使われるライブラリ ( https://github.com/danielgindi/Charts )、切り替えのタブはUISegmentedControl ( https://developer.apple.com/documentation/uikit/uisegmentedcontrol ) を用いて実装されています。

実装を終え、まず手元のiPhone XS Maxにてビルドしたところこのように綺麗に表示されました。タブを切り替えてもなんの問題も起きません。

 

f:id:cc-yamazaki:20200701133418p:plain


次にiPhone8にてビルドしてみます。最初はなんの問題もなく表示されます。しかしタブを2〜3回切り替えると…

f:id:cc-yamazaki:20200701133521p:plain

 

ウィジェットでたまに見かける悪魔のような画面が必ず現れてしまいます…

この画面はなんらかの原因(クラッシュなど)でウィジェットの画面更新に失敗したときに表示されます。まずはコードの問題によるクラッシュを疑いましたが、iPhone XS Maxでは問題なく動きますし、コードには問題ありません。次にウィジェット画面の描画に関する端末側のシステムプロセスの不具合の可能性を疑い、端末を再起動してみますがこれでも解決しません。

こんな感じで原因を探っていきましたがなかなか原因が掴めませんでした。


3.メモリ問題の解決に向けて

そんな感じで原因を掴みきれていない中、Apple開発者向けの「App Extension Programming Guide」に以下のような説明があることを見つけました。(https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionCreation.html


App extensions should feel nimble and lightweight to users. Design your app extension to launch quickly, aiming for well under one second. An extension that launches too slowly is terminated by the system.

Memory limits for running app extensions are significantly lower than the memory limits imposed on a foreground app. On both platforms, the system may aggressively terminate extensions because users want to return to their main goal in the host app. Some extensions may have lower memory limits than others: For example, widgets must be especially efficient because users are likely to have several widgets open at the same time.


App extensionsとはアプリ本体の動作を拡張し、アプリのコンテンツをiOS全体に広げる機能です。これには8種類あり、ウィジェットはその中の Today に該当します。

さて上のApple公式の文章を読むとApp extensionsはとにかく軽量であることが求められており、重くて起動が遅いとシステム側で強制的に終了させるぞと書いてあります。しかもウィジェットに関しては最後の方に名指しでメモリ制限が厳しいことが書いてあります。

さてそれでは今回の機能追加後 (ver3.3.0) のウィジェットのメモリ使用量を調べてみます。Xcodeのデバッグ機能であるDebug gaugesを使ってメモリの消費状況を確認してみます。

f:id:cc-yamazaki:20200701134227p:plain

 

赤く表示されている線がMemory Limitのラインです。(ちなみにこのMemory Limitは実機でビルドしている時にだけ表示され、シミュレーターでビルドした際は表示されません)

最初は、許容されているメモリ使用量内に収まっているので問題ないのですが、タブを切り替えた瞬間にメモリ使用量が急増し、許容されている値を超えてしまっていることがわかります。それにしても許容されているメモリが26MBしかないとは結構厳しいですね。


次に今回の機能追加前 (ver3.2.9以前) のウィジェットのメモリ使用量を調べてみます。

f:id:cc-yamazaki:20200701134322p:plain

 

赤く表示されているMemory Limitの線までは結構余裕がありますね。どうやらチャートの描画などによってメモリを使いすぎてしまったようです。iPhone XS Maxでは綺麗に表示されたのにiPhone8では不具合が起きてしまった原因は、iPhone XS Maxのような高性能端末では使用可能なメモリ量が多いので今回の変更でのメモリ使用量の増加に耐えることができたのですが、iPhone8くらいのスペックでは耐えられず、タブを何回か切り替えてメモリ使用量が上がりすぎたタイミングでシステムによって強制的に終了させられていたのが原因でした。

ここからメモリ使用量の削減にとりかかりました。主にやったことは以下です。

①APIからのデータ取得を効率的に(無駄にAPIを叩いている部分とかを見直しました)
②UITableViewのreloadDataの使用を最小限に(今回騰落率順での切り替えなどでセルを更新したいタイミングがたくさんあります。reloadDataを呼ぶとセルが再描画され一時的なメモリ使用量が上がってしまうため、reloadDataでのセルの更新タイミングを最小限にしました。)
③無駄な変数/定数を徹底的に削除

このようにメモリ使用量を徹底的に見直し最適化した結果、Debug gaugesでの実行結果はこのようになりました。

f:id:cc-yamazaki:20200701134518p:plain

 

改善前と比べるとメモリ使用量が大きく減少していることがわかります。またUITableViewのセルの更新などを見直したおかげでタブを切り替えても以前のように大きくメモリ使用量が増えるということはなくなりました。

そして…

f:id:cc-yamazaki:20200701133418p:plain

 

無事綺麗に表示されました!

4.最後に

今回はウィジェットのメモリ制限が厳しい話をまとめてみました。

昔と違ってメモリが潤沢になった現代のアプリ開発の世界でここまでメモリを意識してコードを書くこともなかなかないのではないでしょうか?特にウィジェットはユーザーが色々なアプリのコンテンツを同時に開く場所なので、効率的に設計しないとシステムによって強制的に終了させられてしまいます。

先日のWWDCで発表されたiOS14でもウィジェットの進化は大きく取り上げられており今後iOSにおけるウィジェットの重要性はますます高まっていきそうです。(https://www.apple.com/ios/ios-14-preview/features/

これからもウィジェットのみならずユーザーファーストなアプリを提供できるように開発してまいりますので、宜しくお願い致します。