iOSアプリ開発でのやらかしを振り返る(1)
メモリリークによるアプリクラッシュ
Swiftでのメモリ管理について(ARC)
Swift(およびObjective-C)では、ARC(Automatic Reference Count)というメモリ領域管理システムを採用しています(GCは使っていません)
ARCのおかげで、メモリの開放などをあまり意識せずにコーディングを進めることができますが、意識するべきことがいくつかあります。
私は、メモリ管理についての意識が薄弱だったために、問題を引き起こしてしまいました。
ARCの挙動
ARCでは、オブジェクトが参照された数をカウンタに保持しています。
カウンタが0になるとメモリ上から開放されます。
もし、何らかの理由で参照カウンタが0にならない場合、メモリリークを起こしてしまうわけです。
実際にコードを書いて実験してみます。playgroundで実際にやってみてもいいかもしれません。
// リークしないパターン
class Hoge {
deinit {
print("deinited")
}
}
var hoge: Hoge? = Hoge() // 参照カウンタ = 1
var huga: Hoge? = hoge // 参照カウンタ = 2
huga = nil // 参照カウンタ = 1
hoge = nil // 参照カウンタ = 0 -> deinit
// deinitedと表示される
// 参照カウンタが0になったときにインスタンスが開放される
きちんと参照カウンタが0になるような場合はよいのですが、例えば循環参照などを引き起こすと参照カウンタが0にならず、インスタンスが開放されません。
iOSアプリでそのようなやらかしをしてしまうと、徐々にメモリ使用量が増加する危険なアプリになってしまい、最終的にアプリが落ちます。業務でやってしまうと恐ろしいですね。
今回は実業務でやらかしてしまったメモリリークについて例を挙げて説明します。
実際に起きたクラッシュ事案について
- クロージャでのメモリリーク
// リークするパターン(closure)
class Hoge {
var name: String = "tallestorange"
var hogeandfuga: (() -> ())!
deinit {
print("deinited hoge")
}
func doSomething() {
self.hogeandfuga = {
// hogeandfugaクロージャとHogeクラスインスタンスがオブジェクトを保持しあっている
self.name = "kyomu"
print(self.name)
}
self.hogeandfuga()
}
}
var ins: Hoge? = Hoge()
ins?.doSomething()
ins = nil // 参照カウンタが2->1になる
// 参照カウンタは0にならないのでいつまでもdeinitされない
この場合、hogeandfugaクロージャとHogeクラスインスタンスが互いに参照を持ち合っています。
参照カウンタが0にならないので、いつまでもdeinitされません。
- デリゲートでのメモリリーク
何度か起こしてしまっているので自戒の意味も込めて書きます
// リークするパターン(delegate)
protocol KyomuDelegate: class {
func onCompletion()
}
class Kyomu {
var delegate: KyomuDelegate?
deinit {
print("deinit kyomu")
}
func doSomething() {
print("kyomukyomu")
delegate?.onCompletion()
}
}
class Hoge {
var kyomu: Kyomu!
deinit {
print("deinit hoge")
}
init() {
self.kyomu = Kyomu()
self.kyomu.delegate = self
self.kyomu.doSomething()
}
}
extension Hoge: KyomuDelegate {
func onCompletion() {
print("dual_kyomu")
}
}
var ins: Hoge? = Hoge() // 参照カウンタは2
ins = nil // 参照カウンタ2->1
// deinitされない
HogeクラスインスタンスとKyomuクラスインスタンスが互いに参照を持ち合っているので参照カウンタは2です。
この場合もいつまで経ってもdeinitされません。自分はこれで1度アプリを落としたことがあります
原因
どちらも循環参照により参照カウンタが0にならなかったことが原因です
対策
参照カウンタに加算されない参照を行えばいいです。Swiftだとそのような参照は2種類あります。
- 非所有参照(unowned)
- 弱参照(weak)
ちなみにSwiftの場合、特に指定しないと参照カウンタを増加させる参照(強参照)になります。
非所有参照と弱参照と使い分けについては、
if もう一方のインスタンスなしにインスタンスが存在し得ない {
非所有参照をつかう
}
else {
弱参照をつかう
}
例えばこんな感じです(delegateの場合)
// リークしないパターン(delegate)
protocol KyomuDelegate: class {
func onCompletion()
}
class Kyomu {
weak var delegate: KyomuDelegate? // 弱参照
deinit {
print("deinit kyomu")
}
func doSomething() {
print("kyomukyomu")
delegate?.onCompletion()
}
}
class Hoge {
var kyomu: Kyomu!
deinit {
print("deinit hoge")
}
init() {
self.kyomu = Kyomu()
self.kyomu.delegate = self
self.kyomu.doSomething()
}
}
extension Hoge: KyomuDelegate {
func onCompletion() {
print("dual_kyomu")
}
}
var ins: Hoge? = Hoge() // 参照カウンタは1
ins = nil // 参照カウンタ1->0
// deinitされる!やったね!
クロージャで循環参照を回避する場合はこんな感じです
// リークしないパターン(closure)
class Hoge {
var name: String = "tallestorange"
var hogeandfuga: (() -> ())!
deinit {
print("deinited hoge")
}
func doSomething() {
self.hogeandfuga = { [unowned self] in
// 非所有参照で循環参照を回避
self.name = "kyomu"
print(self.name)
}
self.hogeandfuga()
}
}
var ins: Hoge? = Hoge()
ins?.doSomething()
ins = nil // 参照カウンタが1->0になる
// deinitされた!