Swiftのメモリ管理 - Weak、Strong、Unowned Referenceについて

メモリ管理が思ったように動かないとき、多くのデベロッパーを驚かせます。AppleのAutomatic Reference Counting (ARC)は私たちがかつて手作業で行っていたメモリ管理のほとんどを、魔法のように処理します。しかし、それでもたびたび、うまく動作するための手引きを必要とします。Swiftにはそのために、キャプチャリスト(Capture List)、非所有参照(Unowned Reference)、弱参照(Weak Reference)のような素晴らしい仕組みがあります。この講演では、Hector Matosがメモリリークと循環参照の解説を通してその原因と解決方法をお話しします。私たちは、よりクリーンで、表現豊かで、リークのないコードを書くために、Swiftのコンパイラの魔法を活用します。メモリ管理の問題よ、さようなら!


イントロダクション (0:00)

こんにちは!私はHector Matosです。Capital Oneのシニアソフトウェアエンジニアです。そして4年近くiOSの開発をしてきました。私が開発を始めた時は、みなさんの多くが懐かしく思うであろう、手作業でのメモリ管理をしていた時代でした。私は約1年間スタートアップで働き、その後Appleで1年間、そのあと現在は、最近Capital Oneに買収された別のスタートアップのMonsoonで働いています。私は初日からSwiftの仕事をしています。そして他のAppleデベロッパー達と共にアプリをまるごと新規で開発する際に、Swiftについて多くを学ぶ機会に恵まれています。私のTwitterはこちらです。そしてKrakenDevにブログを書いています。私はそこでSwiftについて知っている限りのことを解説しています。

Automatic Reference Counting (ARC) (2:02)

強参照、弱参照、そして非所有参照は全てARCに関連しています。これらはARCに、使用しているメモリの扱い方を伝えます。これは解放するオブジェクトを実行時に決定するガベージコレクションではありません。オブジェクトを解放するためのコードをコンパイル時に生成します。

もしあなたが数年前からこの分野にいたのなら、これに見覚えがあるでしょう:

- (void)releaseTheKraken {
    Kraken *kraken = [[Kraken alloc] init]; //+1 retain
    [kraken release]; //now generated by ARC
}

これは最悪でした。けれどこれはもうおしまいです。私は嬉しいです。最後の行はコンパイル時にARCによって生成されましたが、循環参照が起こる場合にはさらに複雑になります。ARCは魔法のようですが、魔法にも限界があります。クロージャやブロックのようなしゃれたものを使用する場合、ARCはコンパイル時に、それらのリリースメッセージをどこに付ければ良いのかを知りません。私たちはそれを、弱参照と非所有参照と共にARCに知らせなければなりません。それが私がObjective-Cから離れ、Swiftに乗り換えた理由です。

メモリリークと循環参照 (3:58)

弱参照と非保有参照はメモリリークと循環参照を解決するために使われてきたので、私はそれらについてお話したいと思います。循環参照とはどのようなものかを下記の図に示します。

Human-Heart Retain Cycle

2つのオブジェクトがあります。HumanとHeartです。人間は心臓なしでは生きられませんし、心臓も人間なしでは生きられません。コード内で、HumanはHeartに語りかけます、その逆も同じです。それぞれ、互いに参照しあう必要があります。

この場合、HumanはHeartに対して強い参照をしていて、またHeartもHumanに対して強い参照をしています。2つのオブジェクトが互いに強く参照しあっているのが、循環参照の定義です。その結果、どちらかのメモリ割り当てを解放しようとしても、もう一方が強い参照をしているためにできません(結局どちらも解放できません)。

循環参照を防ぐために、これを使います。

Human-Heart Retain Cycle Broken!

HumanはHeartを自身の子として参照できます。そしてHeartはHumanに対して弱参照を持ちます。結果として、Humanのメモリ割り当てを解放することで参照カウントを0にします。HeartはHumanを強く参照してはいないのでHumanを解放することができ、循環参照ではなくなります。

強参照 (5:36)

Swiftでは、プロパティを定義する際は、強参照がデフォルトです。下記のKrakenクラスのyummyHumanプロパティはHumanを強参照しています。

//Kraken holds a strong reference to the yummy human.
class Kraken {
    var yummyHuman: Human
}

参照型と値型との間にもまた、違いは存在します。値型がEnumやStructで代入時にコピーされるのに対し、参照型はクラスです。値型はメモリ割り当ての解除に関しての心配はいりませんが、クラスは違います。

クロージャもまた、Swiftの一級市民として、クラスのように振る舞う参照型です。

// The animation closure holds a strong reference to self
// self.retainCount is 1 here
UIView.animateWithDuration(0.3) {
    // self.retainCount is 2 here
    self.view.alpha = 0.0
}

クラスとしてクロージャを考えるークロージャは内部の値をキャプチャします。アニメーションのような単純なものの場合、あなたの親(クロージャ)は、未来のある時点でそれが解除されないために、selfをキャプチャしています。クロージャは将来呼び出されるために保存されます。アニメーションはselfに対して強参照を持っているために、クロージャが解放されるまではselfが解放されることはありません。みなさんがご存知のように、参照カウントが0にならない限り、オブジェクトの割り当てを解除することはできません。

Q: もしクロージャに保有されているselfが外部で解放された場合、アニメーションクロージャの完了後にselfは解放されますか?

Hector: はい。この場合、アニメーションが完了するまではselfの割り当てを解除することはできません。アニメーション完了後にselfの所有を解放します。参照カウントは0になり、selfは解放され、そこへのポインタはnilにセットされます。そして、メモリが解放されます。 しかしアニメーションがselfをキャプチャし、selfが外部で解放されているが、アニメーションが一度も呼び出されない場合に、問題が発生します。selfはクロージャから強参照されているので決して解放されることはありません。メモリリークです。

弱参照 (9:30)

強参照とは違い、弱参照は参照カウントを増やしません。オブジェクトを渡しても、参照カウントは変更されません。その結果、クロージャの中で使用された場合は、弱参照はOptionalになります。強参照と同様に、オブジェクトが解放されると、そこへのポインターはnilになります。これは、あなたが精通しているであろうObjective-Cの’weak’に相当します。

あなたのビューコントローラにIBOutletをドラッグすると、それは自動的に弱参照のプロパティになります。これらは、Swiftでの弱参照の作り方の例です。weak 修飾子を使用します。

class KrakensFace: UIView {
  @IBOutlet weak var razorSharpTeeth: UIView!
}

KrakenAPI.eat(yummyHuman, whenFinished: { [weak self] in
  self?.waitForNextMealTime()
})

この例では、あなたのKrakenの顔は鋭い歯(razorSharpTeeth)をもっています。あなたはおいしい人間(yummyHuman)を食べる(eat)Kraken APIを持っています。もしselfへの弱参照を作りたい場合、このようにします。これは弱参照でOptionalなので、selfのメソッドを呼び出すには、下記のようにオプショナルチェーンを使う必要があります。

self?.waitForNextMealTime()

キャプチャリスト - [weak self] (12:17)

[weak self] inは最初は不安に思いましたが、実際にはこれはシンプルです。角括弧はキャプチャリストを示しますが、実はこれは単なる配列でweak selfを要素として1つ保持しています。配列なので、キャプチャリストはweak self以外でも任意の要素をキャプチャすることができます。

KrakenAPI.eat(yummyHuman, finished: { [weak self, unowned lovedOne = yummyHuman.lovedOne] in
    self?.waitForNextMealTime()
})

このキャプチャリストは、これらの値をどのようにキャプチャするかをシンプルにコンパイラに伝えます。この例では、selfは弱参照としてキャプチャされていて、lovedOneは非所有参照になります。

Q: 見当違いかもしれませんが、なぜクロージャの中でselfを使わないといけないのですか?

Hector: selfのプロパティにアクセスする時は、selfをキャプチャすることによって、Appleは私たちに、selfがキャプチャされてることを明示し、メモリ管理について考えさせようとしています。なぜなら参照カウントを1つ増やすからです。同じことを、selfに対してメソッドを呼び出すときも同様です。コンパイラはデベロッパーにより良いコードを書かせるために、そしてメモリ管理を忘れないためにselfのキャプチャをさせます。

Q: 弱参照は参照カウントを増やしませんが、オブジェクトに対するそれらの弱い参照がすぐに解放されてしまうのをどのように防ぐのでしょうか?オブジェクトを割り当てて、すぐにそれが解放されることがあります。

Hector: 弱参照は実際には強参照に大きく依存しています。あなたがおっしゃるように、強参照を持つものが何もないのに弱参照を使おうとすると、オブジェクトを生成した後すぐにそれは解放されます。すぐに解放させないために、他のオブジェクトはその弱いオブジェクトに対して強く参照する必要があります。これは弱参照を使う上での前提であり、またOptionalである所以です。

クロージャ内で使われている間に弱く参照されたオブジェクトが割り当て解除されないために、Objective-Cで行っていたように、強参照を弱い参照に対して保持していました。これは、Swiftでは、if-letによるアンラップを用いることで行われました。: if let reference = weakReference。これは、コツをつかむのに時間がかかります。

非所有参照 (17:17)

非所有参照は弱参照とは同じものではありません。どちらも参照カウントを増やしませんが、非所有参照は相互依存を必要とします。参照の割り当てが解除される度にweakはポインタがnilになります。非所有参照はObjective-Cの世界ではunsafe_unretainedです。これはポインタをnilにしません。そして参照カウントも増やしません。非所有参照は、少しだけより安全なところを除けば、とてもunsafe_unretainedに似ています。裏では、これはunsafe_unretainedにはない、いくつかの追加のチェックを持っています。それにも関わらず、非所有参照はそれ自身でnilにされないので、解放済みのポインタアクセス(ダングリングポインタ)が生じる可能性があります。

非所有参照は正しくないオブジェクトへのポインタを持つことができます。クロージャ内で使用された場合、非所有参照はImplicitlyUnwrappedOptionalのように動作します。私たちはOptionalと、ImplicitlyUnwrappedOptionalの違いを理解しています。弱参照はOptionalであり、非所有参照はImplicitlyUnwrappedOptionalのように振る舞うので、これらをクロージャで使用する際は、オプショナルチェーンを使用する必要はなく、アンラップする必要もありません。

非所有参照を使用する場所はいくつかあります。人間と心臓の例を出してみましょう。

class Human {
    var heart: 💖
    func seeKrakenComing() {
        heart.haveHeartAttack()
    }
}
class 💖: Organ {
    unowned let human: Human
    init(human: Human) {
        self.human = human
    }
    func haveHeartAttack() {
        human.die()
    }
}
let human = Human()
human.heart = 💖(human: human)

人間は心臓なしでは存在できませんし、心臓もまた、人間なしでは存在できません。私はここにHumanを生成して、彼に命を与え、そして心臓を与えたいと思います。このheartを初期化する場合、Humanのインスタンスを初期化する必要があります。先ほど行ったように、循環参照を防ぐためにあなたのHumanはHeartに対して強参照を持っており、そしてHumanに返した非所有参照と共に、これを初期化します。これはつまり、HeartがHumanに対して強参照を持っていないということですが、HumanはHeartに対して強参照を持っています。これはこの先起こりうる可能性のある循環参照をなくすでしょう。

これは、人間は心臓なしでは存在できず、心臓もまた人間なしでは存在できないという私が絶対に保証でき、非所有参照を使用するというたった一つの目的のためには、とても良い例です。私が開発しているコードなので、私自身がそのこと(心臓がHumanと必ず一緒に存在するということ)を保証することができます。

非所有参照とCore Foundation

非所有参照を使うべきそれ以外の場所は、Core Foundationオブジェクトです。Core FoundationオブジェクトはCocoaオブジェクトとは違います。Core Foundationオブジェクトは自身のライフタイムを管理するような感じです。それらをSwiftで使用する場合、UnretainedValueの値などを扱おうとする時に非所有参照が必要になってくるでしょう。そのようなときに非所有参照を使用するのは間違いなく良いやり方です。

Q: もし弱参照か非所有参照を変数にする場合、コンパイラが警告をすることはありますか?

Hector: いいえ、私の考えでは、ありません。参照型へのどんな参照も、弱参照、強参照、非所有参照になることができます。これらのキーワードは参照型にだけ適用されます。値型には適用されないので、それらは代入時にコピーされるのです。

デリゲートと弱参照 (Live Code) (24:43)

[Hectorはどのように循環参照が生成されるのか、Instruments内ではどのようになるのか、そしてサンプルアプリ内でデリゲートへの弱参照を使用することでどのようにそれを解決するのかをデモします。彼はまた、デリゲートを弱参照することができるクラスだけのプロトコルの考えについてもお話します。]

Q&A (35:09)

Q: 古くからのObjective-C開発者にとって、デリゲートはいつもいつも弱参照であるのが当然でした。

Hector: 完全にあなたの意見に賛成です。それは常にコードを安全にするために良いことでした。あなたのチームメイトが、例えばコードを変えようとする時、循環参照を防ぐのに役立ちます。

Q: Androidも、デベロッパーが積極的にメモリ管理に関与する必要のある潜在的なメモリの問題があると思いますか?

Hector: はい、Androidには確かに独自のメモリの問題があり、そしてそれらに対処するために弱参照と同様のソリューションを持っています。

Q: クラスプロトコルは、プロトコルが参照型にだけ採用されるように限定します。なぜ、プロトコルが参照型によってのみ使用される場合に、その制約を追加する必要があるのですか?

Hector: 通常の、非クラスプロトコルはSwift内で、クラス、StructそしてEnum(参照型と値型)と共に使用することができます。プロトコルを参照型にのみ限定することにより、弱参照か強参照に準拠したオブジェクトに対して参照を行うことができます。もしプロトコルがクラス専用ではなかったら、私たちは参照を行うことができません。なぜなら弱参照、強参照、非所有参照は値型に適用されないからです。

翻訳: Sohei Kitada, Sayuri Takizawa


Hector Matos

Hector Matos

Raised by llamas in the great state of Texas, Hector grew to be an avid couch potato who likes spending his precious couch time playing The Legend of Zelda or yelling at the TV whilst watching Game of Thrones, and his other time with his lovely daughter and wife. When not vegging at home or blogging about Swift, you can find him sitting at the office writing mobile apps for iOS & Android for Capital One. With a particular penchant for great mobile UI/UX, Hector writes the code that makes the world go round.