🦔
URLSession と delegate: self ― メモリリーク問題を整理する
一般的な URLSession の delegate の書き方
まず、よくある書き方の例を示します。
URLSession をプロパティで保持し、delegate: self を渡すシンプルなパターンです。
final class APIClient: NSObject, URLSessionDelegate {
private lazy var session = URLSession(
configuration: .default,
delegate: self,
delegateQueue: nil
)
}
なぜこの書き方でメモリリークするのか?
URLSession は class(参照型) です。そのため、生成したインスタンスをプロパティで保持すると self が 強参照 することになります。
一方で、URLSession は渡された delegate を 強参照 します。
この結果、以下のような循環参照が発生します。
self ── strong ─→ session(URLSession インスタンス) ── strong ─→ delegate(self)
つまり
-
selfはsessionを手放せない -
sessionはdelegate(self)を手放せない
→ どちらも解放されなくなり、「メモリリーク」となります。
正攻法の直し方
Apple が推奨する正しいやり方は、使い終わったらセッションを無効化すること です。
具体的には以下のメソッドを利用します。
-
finishTasksAndInvalidate()
→ 実行中のタスクを完了させた後にセッションを無効化(通常はこちらを推奨) -
invalidateAndCancel()
→ 実行中のタスクを即キャンセルしてセッションを無効化
修正版のサンプル
final class APIClient: NSObject, URLSessionDelegate {
private lazy var session = URLSession(
configuration: .default,
delegate: self,
delegateQueue: nil
)
deinit {
session.invalidateAndCancel()
}
}
weak で持つ場合はどうか?
もし weak var session: URLSession? とすれば循環参照は避けられます。
ただしこの場合、強参照するのは URLSession → delegate のみとなります。
つまり、他に URLSession を保持している強参照がなくなると、
通信中であっても session が解放されてしまう可能性 があります。
結果として「通信途中でセッションが突然解放される」などの不安定な挙動を引き起こすため、通常は非推奨です。
実際にはなぜ問題になりにくいのか?
「普段は気にしていないけどリークしないように見える」理由は以下の通りです。
- 多くのアプリでは APIClient(=URLSession)=アプリのライフサイクル と同じ寿命
- アプリ終了と同時にプロセスごとメモリが解放される
- そのため
invalidateを忘れても表面化しにくい
invalidate が必要となるケース
ただし次のようなケースではセッションを再構築するため、必ず invalidate を呼ぶべきです。
-
ログアウト/アカウント切替
古いセッションの Cookie やキャッシュを残さないため -
環境切替(開発 ↔ 本番)
設定を変えたのに古いセッションが再利用されないようにするため -
長寿命タスク
不要なセッションがバックグラウンドに残らないようにするため -
ユニットテスト
テストごとにセッションを作成する際、無効化しないと積み残りが発生
まとめ
-
URLSessionは class なので、delegate: self の書き方では 強参照ループ が発生する - 解決方法は invalidate 系メソッドを呼ぶこと
- weak で session を持つと今度は「通信途中で解放される」危険があるため非推奨
- アプリ全体で 1 セッションを持ち続ける場合は問題が表面化しにくいが、
ログアウト・環境切替・長寿命タスク・テスト では必ず invalidate を呼ぶ必要がある
Discussion