はじめに
クロージャーの中で自己参照しているはずなのにも関わらず循環参照(メモリリーク)しないパターンがあったので今回それについてまとめます。
今回はクロージャーを含めた参照関係を3パターンほど確認してみましたので、ソースコードを含めて簡単にまとめていきます。
関連記事
[図解]SwiftでARCのメモリ解放の仕組みを理解するために実施したこと(weak版)
[図解]SwiftでUIAlertControllerが何故循環参照するのかを理解するために実施したこと
環境
Xcode 10.1
Swift 4.2
作成したプロジェクト一式
循環参照しないクロージャーのパターンについて
クロージャーの中でselfを使うと自己参照してメモリリークするというのはよくある話なのですが、以下のソースコードは自己参照してるのに循環参照しません。
//メインスレッドにディスパッチする処理
DispatchQueue.main.async {//ここクロージャー
// UIを更新する処理
print(self)//自己参照???
}
クロージャー内で自己参照してるので循環参照になっちゃいそうな気がします。本当はこう書かないとダメでは?
//メインスレッドにディスパッチする処理
DispatchQueue.main.async {[weak self]//ここクロージャー
print(self)//自己参照解消???
}
・・・と思っていたのですが、これweakにする必要ありません。
何故なら、これDispatchQueue.main.asyncはmainが以下の定義となっていました。
public class var main: DispatchQueue { get }
クラス変数です。つまりシングルトンパターンですね。。。
staticなどのシングルトンインスタンスの場合はARCのリファレンスカウンタが上がらない動作となるようです。
実際にシングルトンインスタンスと循環参照させてみた以下のコードを作って、解放されるか確認したところ、ArcTestを再生成するタイミングできちんと解放してくれます。Memory Debug Graphでもリークとなっていなかったので、staticを参照する場合は強参照にならないようです。
class ArcTest: NSObject {
var c = C.shared //c → C を強参照
/// コンストラクタ
override init(){
super.init()
callC()
}
func callC(){
C.shared.arc = self //arc → ArcTestを強参照
}
deinit { print("ArcTestを解放しました") }
}
//テスト3 スタティックの確認
class C:NSObject{
static let shared = C()
var arc:ArcTest?
private override init(){}//privateで隠して、init呼べないようにしちゃいます。
deinit { print("Cを解放しました") }//staticだから解放されるわけがない。。。念の為。
}
循環参照するクロージャーパターンについて
パターン1
class ArcTest: NSObject {
var a:A?//a → A を強参照
/// コンストラクタ
override init(){
super.init()
callA()
}
func callA(){
a = A{print(self)}//(2)クロージャー → 渡された引数のself(ArcTest)を強参照
a?.clo?()
}
deinit { print("ArcTestを解放しました") }
}
//テスト1
class A:NSObject {
var clo:(()->())?//(3)clo → クロージャーを強参照
init(clo:@escaping ()->()){
super.init()
self.clo = clo
}
deinit { print("Aを解放しました") }
}
class ArcTest: NSObject {
var b:B?//b → B を強参照
/// コンストラクタ
override init(){
super.init()
callB()
}
func callB(){
b = B()
b?.clo?()
}
deinit { print("ArcTestを解放しました") }
}
//テスト2 B内でクロージャーからセルフを参照した場合
class B:NSObject {
var clo:(()->())?//(1)B → クロージャーを強参照
override init(){
super.init()
clo = {
print(self)//(2)クロージャー → Bを強参照で循環参照
}
}
deinit { print("Bを解放しました") }
}
循環参照の図は以下の通り。
ここまでやってみて
ARCはstaticを参照する際はリファレンスカウンタを上げない動きをするということがわかりました。なんでもかんでもクロージャー使う場合は自己参照に気をつけなきゃいけないというわけではないということがわかって良かったです。使用するAPIについてはしっかりと調べてから使うようにしたいですね。
すくなくともこんかいで dispatchについてはweakselfパターンは不要ですね。
ちなみにgrobalスレッドもclassメソッドだったのでシングルトンのためクロージャー内で自己参照してても循環参照にはならない動きをしていました。