72
74

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swift その3Advent Calendar 2015

Day 13

クロージャのメモリ管理について

Last updated at Posted at 2015-12-17

クロージャを使うときは必要な時に適切なメモリ管理を実施する必要があります。
そこでクロージャのメモリ管理に関する事柄をまとめました。

目次

  1. クロージャが原因となるメモリリークについて
  2. キャプチャのメモリ管理について
  3. @noescape属性について

1. クロージャが原因となるメモリリークについて

ARCの性質上、循環参照が発生するとメモリリークが起こります。

// 例1. 循環参照によるメモリリークが発生する極端な例
class MyClass {
    var mine: MyClass!
    
    init() {
        mine = self // 自分で自分の参照を保持する(循環参照が発生!)
    }
    
    deinit {
        print("deinit")
    }
}

var hoge: MyClass? = MyClass() // 参照カウンタ+1 = 1, コンストラクタ内で参照カウンタ+1 = 2
hoge = nil  // 参照カウンタ-1 = 1, 参照カウンタが1残っているのでdeinitは呼ばれない

// まだhogeはメモリ上に残っているが、プログラマがアクセスする手段がない→メモリリーク

これは極端な例ですが、クロージャを使うと意図せず上記のような状態になることがあります。
例えばこんな場合です。

// 例2. クロージャによる循環参照が発生する例
class MyClass {
    var title = "Title"
    
    lazy var myClosure: () -> Void = { // 実行時にselfが暗黙的に強参照としてキャプチャされる
        print(self.title)
    }
    
    deinit {
        print("deinit")
    }
}


var c: MyClass? = MyClass() // 参照カウンタ+1 = 1
c?.myClosure() // ここでselfが強参照としてキャプチャされる、参照カウンタ+1 = 2
c = nil        // 参照カウンタ-1 = 2, 参照カウンタが1残っているのでdeinitは呼ばれない

クラスが持つクロージャプロパティでselfを参照しているため、暗黙的にselfが強参照としてキャプチャされます。
そのため コード上では見えませんがselfが参照されたままになるので 循環参照になっています。

対処方法

weakまたはunownedとしてキャプチャするように指定します。

// 例3. クロージャによる循環参照に対処する
class MyClass {
    var title = "Title"
    
    lazy var myClosure: () -> Void = { [weak self] in // selfを明示的にweakとしてキャプチャする
        print(self?.title)
    }
    
    deinit {
        print("deinit")
    }
}


var c: MyClass? = MyClass() // 参照カウンタ+1 = 1
c?.myClosure() // ここでselfがweakとしてキャプチャされる、参照カウンタ+0 = 1
c = nil        // 参照カウンタ-1 = 0, deinitが呼ばれる

self以外でも循環参照は起こる

これまでの例は自己を自己で強参照する循環参照でしたが、
2つのクラス間で互いを強参照しあう循環参照も発生します。

// 例4. クロージャで循環参照
class MyClassA {
    let title: String = "MyClassA"
    
    lazy var myClosure: () -> Void = {
        print(self.title)
    }
    
    deinit {
        print("deinit: \(self.title)")
    }
}

class MyClassB {
    var title: String = "MyClassB"
    
    lazy var myClosure: () -> String = {
        return self.title
    }
    
    deinit {
        print("deinit: \(self.title)")
    }
}

var a: MyClassA? =  MyClassA()
var b: MyClassB? =  MyClassB()
        
if let b = b {
    a?.myClosure = { print(b.title) }
}
if let a = a {
    b?.myClosure = { return a.title }
}
        
a = nil // deinitが呼ばれない
b = nil // deinitが呼ばれない

この場合もクロージャのキャプチャリストでweak(またはunowned)を指定することで対処できます。

var a: MyClassA? =  MyClassA()
var b: MyClassB? =  MyClassB()
        
if let b = b {
    a?.myClosure = { [weak b] in print(b?.title) }
}
if let a = a {
    b?.myClosure = { return a.title }
}
        
a = nil // deinitが呼ばれる
b = nil // deinitが呼ばれる

循環参照にならないケース

クロージャが属しているスコープから抜ける場合

クロージャがスコープから抜ければキャプチャされている参照も解放されるので循環参照になりません。

class MyClass {
    var title = "Title"
    
    func testMethod(closure: () -> Void) {
        closure()
    }
    
    func myMethod() {
        testMethod {  // selfはstrongキャプチャされるけど、スコープから抜けたら解放されるので問題無し
            print(self.title)    
        }
    }
    
    deinit {
        print("deinit")
    }
}

var c: MyClass? = MyClass()
c?.myMethod()
c = nil  // deinitが呼ばれる
optional型の変数をキャプチャする場合(※特殊なケース)

@haranicle さんのポストに循環参照になりそうでならない例が載っていました。

筋肉SwiftプログラマーになるためのARCトレーニング 筋肉クイズ1

var heading:HTMLElement? = HTMLElement(name: "h1") // (1)
let defaultText = "some default text"
heading!.asHTML = { // (2)
    return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
}
print(heading!.asHTML())

// <h1>some default text</h1>とprintされる

heading = nil // (3)

// ★問題★ ここではどうなるでしょう?
// (A) h1 is being deinitializedとprintされる
// (B) なにもprintされない

headingが強参照としてキャプチャされて循環参照になりそうな雰囲気ですが、
答えは(A)となりdeinitが呼び出されます。
ここでは見た目上の参照カウンタの動きだけ見てみます。

  参照カウンタ 補足
(1) 1 変数headingが作成されたので参照カウンタ+1
(2) 1 なぜか参照カウンタが増加しない
(3) 0 nilを代入したので参照カウンタ-1、deinitが呼び出される!

ここで以下のようにスコープで囲って、最後のnil代入を消した場合を見てみます。

do {
    var heading:HTMLElement? = HTMLElement(name: "h1") // (1)
    let defaultText = "some default text"
    heading!.asHTML = { // (2)
        return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
    }
    print(heading!.asHTML())
} // (3)

スコープから外れた時も参照カウンタが減るはずなので、こうなるはずです。

  参照カウンタ 説明
(1) 1 変数headingが作成されたので参照カウンタ+1
(2) 1 なぜか参照カウンタが増加しない
(3) 0 スコープから外れたので参照カウンタ-1、deinitが呼び出されるハズ

でも実際はこうです。

  参照カウンタ 説明
(1) 1 変数headingが作成されたので参照カウンタ+1
(2) 1 なぜか参照カウンタが増加しない
(3) 1 スコープから外れてもなぜか参照カウンタが減少しない

明示的にnilを代入した場合は循環参照にならずメモリリークしないのに、
スコープに任せるとメモリリークします。なんでこうなるのか次章で説明します。

2. キャプチャのメモリ管理について

キャプチャのメモリ管理はstrong, weak, unownedの3種類だけですが、
strongに特定の条件が組み合わさると先ほどの特殊なケースのように分かりづらい挙動になります。

各々がどのような条件の時に採用されるのかまとめてみます。
また、特殊なケースについては挙動を説明してみます。

strong

  • キャプチャリストに何も指定せず、キャプチャされるのが定数の場合
  • キャプチャリストに変数/定数を指定した場合
let element: HTMLElement? = HTMLElement(name: "h1")

// キャプチャリストに何も指定しない
element?.asHTML = {
    return element!.name
}

// キャプチャリストに定数を指定
element?.asHTML = { [element] in
    return element!.name
}

weak

  • キャプチャリストにweakを付加した変数/定数を指定した場合
let element: HTMLElement? = HTMLElement(name: "h1")

// キャプチャリストにweakを付加した定数を指定
element?.asHTML = { [weak element] in
    return element!.name
}

unowned

  • キャプチャリストにunownedを付加した変数/定数を指定した場合
let element: HTMLElement? = HTMLElement(name: "h1")

// キャプチャリストにunownedを付加した定数を指定
element?.asHTML = { [unowned element] in
    return element!.name
}

strong + box

  • キャプチャリストに何も指定せず、キャプチャされるのが変数(var)の場合
var element: HTMLElement? = HTMLElement(name: "h1") // var!

// キャプチャリストに何も指定しない
element?.asHTML = {
    return element!.name
}

変数はSwiftが自動的にボックス化して作成します。
そのためボックス化された変数がキャプチャされることになり、
ボックス化された変数に対してARCが機能することになります。
値型の場合は例え参照型の定数/変数を内包していてもボックス内の参照カウンタは変化しません。
参照型の場合はそのまま変数の参照カウンタが増加します。

参照型Xを持つOptional変数を作ると、参照カウンタはボックス用とX用の2つ分作られるわけです。

以上を踏まえて、前章最後のコードと参照カウンタの動きを見てみます。

var heading:HTMLElement? = HTMLElement(name: "h1") // (1)
let defaultText = "some default text"
heading!.asHTML = { // (2)
    return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
}
print(heading!.asHTML())

// <h1>some default text</h1>とprintされる

heading = nil // (3)

// ★問題★ ここではどうなるでしょう?
// (A) h1 is being deinitializedとprintされる
// (B) なにもprintされない

変数なのでボックス化されます。
そして参照型HTMLElementを持つOptional変数なので参照カウンタはBoxとHTMLElementの2つ作られます。
それぞれの参照カウンタの動きはこうなります。

  Boxの参照カウンタ HTMLElementの参照カウンタ 説明
(1) 1 1 変数headingがボックス化されて作成されたので両方の参照カウンタ+1
(2) 2 1 ボックスとしてキャプチャされるのでBOXの参照カウンタ+1
(3) 2 0 nilを代入したのでHTMLElementの参照カウンタ-1
(3)' 1 0 HTMLElementのdeinitが実行されてキャプチャも消えるのでBOXの参照カウンタ-1
(4) 0 0 スコープから外れた時にBoxの参照カウンタ-1

続いてスコープで囲った場合も見てみます。

do {
    var heading:HTMLElement? = HTMLElement(name: "h1") // (1)
    let defaultText = "some default text"
    heading!.asHTML = { // (2)
        return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
    }
    print(heading!.asHTML())
} // (3)
  Boxの参照カウンタ HTMLElementの参照カウンタ 説明
(1) 1 1 変数headingが作成されたので参照カウンタ+1
(2) 2 1 ボックスとしてキャプチャされるのでBOXの参照カウンタ+1
(3) 1 1 スコープから外れたのでBoxの参照カウンタ-1

というわけで循環参照したりしなかったりする場合があります。
なかなかなさそうなケースですが、知ってて損はないかなと思います。

3. @noescape属性について

クロージャ引数に付加できる属性で、クロージャが同期的に呼び出されることを保証することができます。
この属性が指定されたクロージャは(スコープ外に)エスケープしない、という意味です。

selfが不要になる

この属性が指定されたクロージャは(コンパイラが)selfの存在を保証する必要がなくなります。
そのためクロージャ内でselfを書かなくて良くなります。
ARCの関与がなくなるからかパフォーマンスも上がるようです。

func myFunction(closure: () -> Void) {
    closure()
}

func myFunctionNoEscape(@noescape closure: () -> Void) {
    closure()
}

class MyClass {
    var count = 0
    
	func myMethod() {
	    myFunction {
	        self.count += 1 // selfが必要
	    }
	    
	    myFunctionNoEscape {
	       count += 1   // selfは不要
       }
	}
}

また副次的な効果として循環参照を起こす可能性のある記述が
コンパイルエラーとなるのでミス防止にもなります。

func myFunction(closure: () -> Void) {
    closure()
}

func myFunctionNoEscape(@noescape closure: () -> Void) {
    // コンパイルエラー: 定数・変数への代入は不可
    let hoge = closure
    
    // コンパイルエラー: @noescapeが指定されていないクロージャ型への変換は不可
    myFunction(closure)
    
    // OK: @noescapeが指定されている引数へ渡すことは可能
    myFunctionNoEscape2(closure)
}

func myFunctionNoEscape2(@noescape closure: () -> Void) {
    closure()
}
72
74
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
74

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?