Swiftのクロージャが循環参照をつくりメモリリークする例を理解するためにサンプルコードを書いてみました。

Xcode 8.2.1のPlaygroundで動作を確認しています。

1. クロージャが強参照(strong reference)することによる循環参照

class CalorieCalculator {
    let weight: Float

    init(weight: Float) {
        self.weight = weight
    }

    deinit {
        print("deinit: CalorieCalculator")
    }

    // 遅延格納型プロパティ(lazy)
    lazy var golf: (Float) -> Float = self.calorie(mets: 3.5)
    lazy var walk: (Float) -> Float = self.calorie(mets: 4.0)
    lazy var swim: (Float) -> Float = self.calorie(mets: 8.0)

    func calorie(mets: Float) -> (Float) -> Float {
        return {
            // クロージャからselfに対する強参照(strong reference)がある
            (hour:Float)->Float in return (mets*hour*self.weight)
        }
    }
}

do {
    let calcOfBob = CalorieCalculator(weight: 61.5)
    let calcOfAlice = CalorieCalculator(weight: 52.3)

    print("Bob / swim : \(calcOfBob.swim(1.0)) kcal")
    print("Bob / golf : \(calcOfBob.golf(8.0)) kcal")
    print("Bob / walk : \(calcOfBob.walk(8.0)) kcal")
}

【 ポイント 】

  1. クロージャの内部でselfを強参照でキャプチャすることで循環参照が発生している。するとインスタンスが解放されずメモリリークする。

2. 明示的なキャプチャリストにより循環参照の回避

class CalorieCalculator {
    let weight: Float

    init(weight: Float) {
        self.weight = weight
    }

    deinit {
        print("deinit: CalorieCalculator")
    }

    lazy var golf: (Float) -> Float = self.calorie(mets: 3.5)
    lazy var walk: (Float) -> Float = self.calorie(mets: 4.0)
    lazy var swim: (Float) -> Float = self.calorie(mets: 8.0)

    func calorie(mets: Float) -> (Float) -> Float {
        return {
            // 値型をキャプチャリストに明示的に指定
            [weight](hour:Float)->Float in return (mets*hour*weight)
        }
    }
}

do {
    let calcOfBob = CalorieCalculator(weight: 61.5)
    let calcOfAlice = CalorieCalculator(weight: 52.3)

    print("Bob / swim : \(calcOfBob.swim(1.0)) kcal")
    print("Bob / golf : \(calcOfBob.golf(8.0)) kcal")
    print("Bob / walk : \(calcOfBob.walk(8.0)) kcal")
}

【 ポイント 】

  1. CalarieCaluculatorクラスのインスタンス self をクロージャの内部から参照していたことが諸悪(循環参照)の根源
  2. だから値型(Float)のweightを明示的にキャプチャしてselfへの参照を解消
  3. 循環参照がなくなりメモリリークが解消!

3. 非所有参照 (unowned reference) による循環参照の回避

class CalorieCalculator {
    let weight: Float

    init(weight: Float) {
        self.weight = weight
    }

    deinit {
        print("deinit: CalorieCalculator")
    }

    lazy var golf: (Float) -> Float = self.calorie(mets: 3.5)
    lazy var walk: (Float) -> Float = self.calorie(mets: 4.0)
    lazy var swim: (Float) -> Float = self.calorie(mets: 8.0)


    func calorie(mets: Float) -> (Float) -> Float {
        return {
            [unowned self](hour:Float) -> Float in
                return (mets * hour * self.weight)
        }
    }
}

do {
    let calcOfBob = CalorieCalculator(weight: 61.5)
    let calcOfAlice = CalorieCalculator(weight: 52.3)

    print("Bob / swim : \(calcOfBob.swim(1.0)) kcal")
    print("Bob / golf : \(calcOfBob.golf(8.0)) kcal")
    print("Bob / walk : \(calcOfBob.walk(8.0)) kcal")
}

【 ポイント 】

  1. self を [unowned self] と明示的に非所有参照でキャプチャすることで循環参照を回避している
  2. この例ではクロージャが実行されるときにselfが解放されていることはないため実行時エラーは発生しない
  3. もしクロージャの実行時にselfが解放されていれば実行時エラーが発生する

4. 弱参照(weak reference)による循環参照の回避

class CalorieCalculator {
    let weight: Float

    init(weight: Float) {
        self.weight = weight
    }

    deinit {
        print("deinit: CalorieCalculator")
    }

    lazy var golf: (Float) -> Float = self.calorie(mets: 3.5)
    lazy var walk: (Float) -> Float = self.calorie(mets: 4.0)
    lazy var swim: (Float) -> Float = self.calorie(mets: 8.0)    

    func calorie(mets: Float) -> (Float) -> Float {
        return {
            [weak self](hour: Float) -> Float in
                if let weakSelf = self {
                    return mets * hour * weakSelf.weight
                } else {
                    return 0
                }
        }
    }
}

do {
    let calcOfBob = CalorieCalculator(weight: 61.5)
    let calcOfAlice = CalorieCalculator(weight: 52.3)

    print("Bob / swim : \(calcOfBob.swim(1.0)) kcal")
    print("Bob / golf : \(calcOfBob.golf(8.0)) kcal")
    print("Bob / walk : \(calcOfBob.walk(5.0)) kcal")
}

【 ポイント 】

  1. 弱参照(weak reference)をつかうことで循環参照を回避している
  2. もしselfが解放されたとしてもゼロ化される(nilがセットされる)ため安全なコードである
  3. ゼロ化の実行コストとnilのときのコードの分岐が冗長である(安全のためのコスト)
  4. selfがnilのときに関数calorie()が0を返すことが妥当かは残課題