weak(弱参照)の使うタイミング(特にクロージャの中)について説明してみる

  • 76
    いいね
  • 8
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

僕がobjective-cを触っていて長年理解ができなかった概念がweak(弱参照)である。
weakに関する様々な文献はあるものの、僕が理解できる文献はなく結局プログラムを書いているうちに徐々にわかってきた概念である。
(僕にとってはそれだけ、難解な概念だった。というか、今も難儀することがある。)

はじめに断っておくが、僕自身がweakなどについて熟知しているとは言い難いところがある。
しかし世の中の文献はweakについて熟知している人の記事と、わかっていない人の記事が多く見受けられる。その中間層である僕ような理解度の人向けの記事が少ないと感じたためこの記事を書こうと思った。
大筋の説明は間違っていないと思うが、多少の間違いはあるかもしれないことを断っておく。(間違いがあればコメントで指摘してくれると嬉しい。)

この記事では説明にSwiftを使うが、objective-cでも記述方法が違うだけで概念は同じである。
SwiftはJavaやRubyなどを学んでいる人にも理解しやすい言語なので、JavaやRubyエンジニアにも参照していただければ幸いである。

なお、本記事は概念的なものから実際のプログラムに関しての説明をしているため若干長めのエントリーになっています。ご了承ください。

この記事を書く理由

現在Cocos2d v3 でゲーム作るについての連載記事を書いているが、weakに関しての理解がどうしても必要になる。

というのも、この問題を曖昧にしたままiPhoneアプリを作成していくと最後に待っているのはメモリリークや謎のヌルポ(厳密にはヌルポでは無いが、破壊されている参照オブエジェクトにアクセスして落ちてしまう現象)が発生してしまうからだ。
なので、多少回り道をしてでもこの基礎の部分を多くの人に理解してほしいと思い投稿することにした。

※一番の理由は連載中の記事内でweakについて説明すると大幅に脱線するため、脱線部分を本記事へのリンクを張って脱線しにくい記事にしようとしているためである。

参照カウンター(Retain)とガベージコレクションについて

weak(弱参照)について話すためにはこの参照カウンター(Retain)について学ばなければならない。
JavaやRubyとC系の言語であるobjective-cとSwiftではメモリ管理方法が大きく異なる。

ガベージコレクション

ここでは詳しく説明しないが、参照カウンターを理解する上で対象となっている仕組みであるガベージコレクションに関しては多少の理解をしていないと参照カウンターの良さが説明できないため記載しておく。

ガベージコレクションを使う場合はメモリ管理に関してプログラマーが意識する必要はなく、言語レベルで不要となったオブジェクト(ガベージ)を収集(コレクション)して定期的に開放してくれる仕組みである。
プログラマーがメモリに関して意識しないため、プログラムの生産性が高くなる傾向にある。

一方で、いつ実行されるかわからないガベージコレクションが突然実行されてCPUを占有してしまうという欠点がある。

余談

強いてもう一つ欠点を挙げると、メモリ管理ができない僕のようなプログラマーを量産してしまい相互参照などのメモリ管理に関わる問題を残したままのプログラムを書いてしまうケースがある。
・・・・かと思った。

しかし、JavaのGC(ガベージコレクション)ついて調べたところこの問題はJavaでは発生しないように工夫がされていた。(ここでは説明を省くが、JavaのGCは凄いと感銘を受けた内容だった。)

なので、Javaのような優秀なGCを使っているエンジニアで今後もGC系を使っていくエンジニアにはこの問題は関係がないだろう。

参照カウンター

参照カウンター方式はガベージコレクションとは違い、自分でオブジェクトの参照数を数えていく方法である。
オブジェクトが生成された時点で参照カウンターは1になり、他のオブジェクトから参照されると2.3.4...のようにカウントアップされていく。
参照が外されるとカウントダウンしていき0になるとオブジェクトのメモリが解放される仕組みである。

ガベージコレクションとは違い、自分で参照カウンターを意識する必要があるためプログラマーの負担が多くなり生産性が悪くなりがちである。
一方でガベージコレクションがいつ走るかわからないような不安定な要素はなく、メモリが即時に開放されるのでメモリを効率的に扱うことができる。

今回はこの参照カウンターについて説明した後に、weakの必要性について記載していく。

参照カウンタープログラムの例

objective-cやSwiftを実際に使っている方は多分この辺までの理解はできていると思う。
理解している方はこのあたりについては飛ばして読んでいただければと思う。(当たり前のことを当たり前に書いているだけである。)

iOS5からARC(Automatic Reference Counting)が導入されたが、ここでは説明のためこの機能がオフだった場合の挙動について記載する。
SwiftでARCをオフにできるかわからないが、ここでは便宜上できる事として説明をする。
ARCがONの場合は呼び出せないが、以下のメソッドが存在することとする。

  • retain・・・参照カウンターを増やして自分のオブジェクトを返すメソッド
  • release・・・自分が保持しているオブジェクトの参照カウンターを一つ下げるメソッド(C言語とかだとfreeにあたる)

この状況でオブジェクトを管理すると以下のようなことが発生する。

Retain.swift
// 二つのオブジェクトを生成する。どちらも参照カウンターは`1`で生成される。
var a1 = A()
var b1 = B()

var a2 = a1.retain // ここでAオブジェクトの参照カウンターが上がり`2`になる。
var b2 = b1        // Bは値を渡しているだけなので、参照カウンターはあがらず`1`のままである。(後述するweak参照である。)

a2.release         // Aオブジェクトの参照カウンターを下げるため、`1`に戻る。
b2.release         // Bオブジェクトの参照カウンターを下げるため、`0`になりメモリが解放される。

a1.call()          // Aオブジェクトはメモリが解放されていないためcallメソッドが呼び出さる。
b1.call()          // Bオブジェクトはメモリが解放されいているためエラーが発生する。
// (objective-cの場合はnilに対してメソッドを呼びだ出しても何も処理されないためエラーにはならないが、本来呼び出されるはずのcallメソッドが呼び出されずバグの原因になる。)

このように各オブジェクトが保持している参照カウンターをインクリメントしていくことで、メモリ管理をしている。
この程度の処理ならば自前で管理できるが、生成したオブジェクトが様々なオブジェクト間で受け渡されると参照カウンターの管理が煩雑になりメモリリリークが発生してしまう。

この欠点を埋めたのがARC(Automatic Reference Counting)である。

ARC(Automatic Reference Counting)の例

ARCではオブジェクトが解放されるタイミングで自動的にreleaseが呼び出されるため、上記のようなプログラム例ではエラーが発生しない。

Retain.swift
var a1 = A()     // オブジェクトを生成する。参照カウンターは`1`で生成される。
var a2 = a1        // ここでAオブジェクトの参照カウンターが`1`上がり`2`になる。

// a2.release      // releaseメソッドを明示的に呼び出すことができない。
a1.call()          // Aオブジェクトはメモリが解放されていないためcallメソッドが呼び出さる。

// プログラム終了と同時に`a1`と`a2`が解放され参照カウンターが2下がり`0`になる。それと同時にメモリが解放される。

このような機構が入ったためプログラムの生産性は一気に向上した。
しかも、ARCはガベージコレクションとは違いコンパイル時にretainreleaseメソッドをプログラムに自動的に埋め込んでくれるためガベージコレクションが走ることもない優れた技術だ。

しかし、ARCはあくまでretainreleaseを自動的に差し込んでくれるだけなのでオブジェクトのメモリ管理は自分で行わなければならない。

相互参照

そこで発生するのが相互参照や循環参照の問題である。
ここでは実際に発生するわかりやすい例が見当たらなかったため、実際に発生するかどうはおいておき概念的なものの理解をするためのプログラムを例に説明する。
実際にはクロージャを使用した場合に多発しやすいので、実際の例は後述のクロージャで説明する。
なのでこ、こではなんとなく相互参照が発生すると問題がありそうだという事を理解してもらえればと思う。

AクラスはBクラスを内部に保持する。

A.swift
class A {
    // Bクラスを保持する。
    var b:B!
}

BクラスはAクラスを内部に保持する。

B.swift
class B {
    // Aクラスを保持する。
    var a:A!
}

実際に呼び出す。

Retain.swift
var a = A()
var b = B()

a.b = b
b.a = a

このような設計をした場合はRetain.swiftが解放されるときにaを解放しても、bオブジェクトがaを保持しているため解放されない。
同様にbオブジェクトでも同じことが発生する。

実際に確認はしていないが、この程度の相互参照の場合は全てのオブジェクトが解放されると思う。
コメントで指摘がありました。この程度でも相互参照が発生します。
しかし、C++で問題となっているひし形継承のような複雑な相互参照が発生すると相互にオブジェクトを保持してしまい永遠にメモリの解放が行われないという問題が発生する。

weakを使う

このような複雑なオブジェクト関係を作らなければこの問題は発生しないが、長い開発期間を持って作成する場合は気付いたら相互参照が発生しているケースが存在する。
その相互参照がくるくる回って、AはBを保持してBはCを保持ししてCはAとBを保持して・・・のような事が発生するかもしれない。

それを解決するのがweakである。
このような問題が発生するのはオーナーシップ(所有権)がはっきりしていない時に発生する。
言い換えると、そのオブジェクトの持ち主が誰かがはっきりしていればこのような問題は発生しない。

例えば先ほどの状態にweakをつけると以下のようになる。
AクラスはBクラスを内部に保持する。

A.swift
class A {
    // Bクラスを弱参照で保持する。
    weak var b:B?
}

BクラスはAクラスを内部に保持する。

B.swift
class B {
    // Aクラスを弱参照で保持する。
    weak var a:A?
}

実際に呼び出す。

Retain.swift
var a = A()
var b = B()

a.b = b
b.a = a

こうすることで、AクラスはBクラスを保持するが渡されたbという変数のオーナーシップは持たない(参照カウンターを上げない)状態が作れる。
この状態になっている場合にbの変数がリリースされた場合はAクラスで保持しているインスタンス変数であるbも解放されnilになる。

Aクラスから見た場合のBクラスはあくまで参照関係で、オーナーシップを持たないことにすることで相互参照の問題がなくなる。(参照先がなくなった場合は別の処理をするようなプログラムを書く必要がある。そのためBクラスはOptionalとして保持している。)

僕はここまでの理解では苦しまなかった。
問題はここからである。

オーナシップを誰が持つべきか?

例えば以下のようなコードを見たことがあるかと思う

TestView.swift
class TestView:UIView {
    @IBOutlet weak var childView: UIView!
}

よく見るとchildViewにはweakが付いているがなぜだろうか?
これはxibやストーリーボードなどからコンポーネントが挿入された場合は、Cocoaフレームワークがオーナーシップを持っているためweakが必要になる。(※本件は私の理解不足で誤った内容になってます。正しく理解したい方はコメント欄を参照願います。)
Cocos2dのSpriteBuildeから挿入された場合も同様である。

このように自分の管理外のオブジェクトがどこから来ているのかを把握していないとweakをつけることが難しいケースがある。

しかし、一方で

TestView.swift
class TestView:UIView {
    weak var childView: UIView!

    func createChildView() {
        childView = UIView()
    }
}

などのようなプログラムを書いてしまうと、createChildViewメソッドを実行してもchildViewは生成と同時に破棄されてしまう。

Main.swift
var testView = TestView()
testView.childView = UIView()

上記のようにしても同様で、オーナーシップが不在のためメモリ確保と解放が同時に発生してしまうのだ。
(実は、シミュレータと実機、iOSのバージョンなどでこの挙動は違う。シミュレーターの場合はweak変数でもしばらくメモリが解放されない場合がある。32bit版と64bit版でも挙動が違った・・・)

もし、TestViewの中のchildViewを外部のプロジェクトがオーナーシップをとる場合は以下のように記述する必要がある。

Main.swift
var testView = TestView()
var childView = UIView()
testView.childView = childView

もちろんだが、ローカル変数のchildViewが破棄されるときにTestViewのchildViewも破棄される。
※通常はこのケースではweakをつけないと思うが、なんでもweakをつけるべきではない事の一例で記載してます。

Delegateに関して

ここまで読んでいただいた方なら理解できると思うが外部のオブジェクトを保持して、外部のオブジェクトに対して通知を行うdelegateに関しては基本的にweakをつける必要がある。

DelegateTest.swift
protocol TestDelegate: class {
    // 何かの処理
}

class TestClass {
    weak var delegate: TestDelegate?
}

delegate処理は外部に対して通知を行う仕組みなので、外部のオブジェクトを受け取って保持してしまうと外部のオブジェクトが破棄されてもTestClassが破棄されるまでその外部のオブジェクトが残ってしまう。
なので、大抵の場合はweakが必須になる。

クロージャに関して

やっと本題。。。
クロージャに関しても同様である。
クロージャは外部のオブジェクトが自分自信を外から呼び出すため、自分自身をweakにしておく必要がある。

呼び出しクラス

TestClass.swift
class TestClass {
    func sendData(onSccess:((Void) -> Void)?) {
        // 何かの非同期通信処理...

        // コールバック
        onSccess?()
    }
}

呼び出されるクラス

TestClass2.swift
class TestClass2 {
    var testClass = TestClass()

    func sendData() {
        // 通知処理を呼び出す。ここの[self weak]がポイントになる。
        testClass.sendData({[weak self] () -> Void in
            if let _self = self {
                // 通知された処理を実行する。
            }
        })
    }
}

ここで[weak self]というキーワードが出てきているかと思う。
これがSwiftでプログラミングを書く上でのポイントになる。

objective-cで書く場合は以下のようになる。

objecive-c.m
__weak typeof(self) self_ = self;
testClass.sendData( ^() {
    // 例で使ってる変数が少しわかりにくい・・・
    TestClass2 *_self = self_
    if (_self) {
        // 通知された処理を実行する。
    }
})

これはTestClass2からTestClassを呼び出して、結果を待ってから処理を実行する例になる。
ポイントはTestClassを呼び出している間にTestClass2が破棄されてしまう可能性があるところである。

TestClassの結果を処理しようとして破棄されたselfに直接アクセスしてしまうと、ヌルポのようなことが発生してしまいコード上で追いにくいスレッド内で例外が発生してしまう。
そのため[weak self]を使うことで一時的にselfを弱参照として、保持しておきコールバックが走った時点でselfが生きていた場合は強参照でオブジェクトの参照を発生させている。(キャプチャーという仕組みらしい。)

Swiftはこの[weak self]というキャプチャーリストという構文があるため、objective-cに比べてスマートにかけるがこの概念はなかなか理解が難しい。

また、ownedなどの概念もあるがこれはweakと違い参照カウンターが0になっても弱参照しているオブジェクトがnilにならないらしい。
なので、今の所使いどころがないと思っている。(すいません。ownedの使い方はまだ勉強してません。)

まとめ

オーナーシップで無い参照変数にはweakを必ずつけましょう!
後々無用なトラブルが発生しにくくなります。