わけあって weak self

Closureを使っていたら、ちょっと予想外の動作になってしまっていました。 超・基本的なことですが、動作確認したのでメモ。

closureの循環参照と weak self

weak self をなぜ使うかというと、循環参照によるメモリーリークを避けるためですよね。

closureを保持するクラスを作って見てみましょう。

下の Personクラスは、生成・消滅した時がわかりやすいように initdeinit にログをいれておきます。 そシンプルに Hello, I am [name]! と表示してくれる normalHello というメソッドを作り、 keepAndDo メソッドで、closureを保持するようにしておきます。

gist.github.com

このPersonクラスをつかって、Closureを保持して実行してみましょう。 strongHello では weak self を使わず、 weakHello では weak self を使っています。

gist.github.com

実行してみると、🦁の deinit だけよばれません。 weak self をよんでいないから self が循環参照をおこして解放されず、メモリーリークの原因になっていますね。

escapingとnonescape

で、ここで escapingnonescape についても復習しましょう。

対象となるclosureがスコープから抜けても存在するときには @escaping が必要になります。

上の keepAndDo メソッドでは @escapingをつけていますが、これを省略するとコンパイルエラーになります。 上の場合は、keepAndDo のスコープから抜けても、(selfで保持しているので)closureが存在するから必要なんですね。

で、似たような処理でnoescapeのものを付け加えてみましょう。

gist.github.com

さきほどの Personクラスに、closureを実行するだけで保持しない justDo メソッドをつけてみました。 その justDo を よぶ noescapeHello には weak self はついていません。

(なお、Swift3からデフォルトは @noescape になったので、最近は @noescape を書くことはないと思います。)

gist.github.com

実行してみると、weak self をつけなくても deinit がよばれています。循環参照は発生していません。

escapingで非同期実行

で、さらにここで非同期実行をかけてみましょう。 それぞれのclosureのなかに、1秒後に自分の名前のログを出す処理をいれておきます。

さらに、 noescapeAndWeakHello として、非同期実行のclosureで weak self つきのメソッドも追加。

gist.github.com

で、これを実行すると……。

gist.github.com

weakで実行した場合、当然解放済みなので非同期実行で名前が表示されないんですが、noescapeだと名前が表示されるのに、noescapeかつweakだと名前が表示されない……。

ログを見ればわかることですが、noescapeの場合、非同期処理の完了と同時にPersonのインスタンスが削除されますが、selfにweakがついていると、非同期処理が実行される前にインスタンスが削除されてしまうんですね。 weak self つけてるんだから当然ですけど。

ステップで一つ一つ確認すれば当然の動作なんだけど、惰性で weak self つけちゃって予想外の動作になっちゃっていました。

なんとなく weak self 、じゃなくてちゃんと理由を考えて使わないとな。 (とおもいつつ、closureの処理が深くなってくると、だんだんわからなくなるんですよね……。)

基本的には closureは nonescapeでとりまわすのが一番事故らない気はしますが、いろいろな事情でclosureをkeepしたいこともあり。難しいところです。