Reference Cycle 出现 & 解决

2016/10/12 iOS

关于 Retaining Cycle,Swift 和 Objective-C 在问题发生和解决方式上本质相同,今天做一个状况分类和介绍对应的解决方式。

问题发生

由于 Swift 使用 ARC - Automatic Reference Counting 来管理内存,而 ARC 又是依靠 reference count 来管理类对象的生命周期(struct 和 enum 为值类型),所以当两个对象互相引用时就可能产生 Reference Cycle 问题。

和带有 Garbage Collection 的语言不同,当一个对象的 reference count 为0时, Swift 会立即删除该对象。

常见的问题场景分为两大类:两个类对象成员间互相引用和使用 Closure 时,下面会给出在这些场景下对应情况的解决方案。

解决方案

两个类对象间引用

1. Class member 均允许为 nil

  • 解决方案:Weak reference
    • 将其中一个 Class member 的引用标记为 weak 即可
  • e.g: Apartment & Person
    • Apartment 可以没有 tenants ;Person 也可以没有 Apartment
class Person {
    let name: String
    var apartment: Apartment? 
}

class Apartment {
    let unit: String
    weak var tenant: Person?
}

Weak Reference

对于一个 weak reference 来说:

  • 当其指向类对象时,并不会添加类对象的引用计数;
  • 当其指向的类对象不存在时,ARC 会自动把 weak reference 设置为 nil ;

由于 weak reference 的特定,它只能被定义成 var 。

2. Class member 只有一个允许为 nil

  • 解决方案:Unowned reference
    • 将不允许为 nil 的 Class member 标记为 unowned 即可
  • e.g: Apartment & Person
    • Apartment 必须有 owner ;Person 可以没有 Property
class Person {
    let name: String
    var property: Apartment?  
}

class Apartment {
    let unit: String
    unowned let owner: Person
}
Unowned Reference

和strong reference相比,unowned reference只有一个特别:不会引起对象引用计数的变化。

3. Class member 均不允许为 nil

  • 解决方案: Unowned reference + Implicitly unwrapped optional
  • e.g: Country & City
    • Country 必须有一个 Capital;City 必须属于一个 Country
class City {
    let name: String
    unowned let country: Country
    
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

class Country {
    let name: String
    // Implicitly Unwrapped Optional
    var capital: City! // default to nil
    
    init(name: String, capitalName: String) {
        self.name = name
        // PAY ATTENTION!
        self.capital = City(name: capitalName, country: self)
    }
}

调用时:

var cn: Country? = Country(name: "China", capitalName: "Beijing")
var bj: City? = City(name: "Beijing", country: cn!)
Implicitly unwrapped optional

上面的场景下,如果用一般的方式构建,则在 Country 的 init() 里,我们把正在创建的 Country 对象传递给了 City 的 init() 。

这样做只在语义上是正确的;语法上,Swift 认为构建 City 时,Country 还没有完成初始化( self.captical 还没有确定的值),因此,它不允许我们在这个时候把 self 传递给 country ,这就陷入了一种“死循环”的困境。

因此要想构建 City 时,必须让 Swift 认为 Country 已经构造完来打破僵局,而唯一的做法就是 captical 有一个默认值 nil 。因此对于 Capital ,有了两个看似冲突的需求:

  • 对 Country 的用户来说,不能让他们知道 capital 是一个 optional ;
  • 对 Country 的设计者来说,它必须像 Optional 一样有一个默认的 nil 。

而解决这种冲突唯一的办法,就是把 capital 定义为一个 Implicitly Unwrapped Optional 。

此时问题又转化到了第 2 条的状况中,因此 City 的 country 属性也要标为 unowned 。

类对象与 Closure 间引用

当某个类中含有 closure ,并且此 closure 引用了这个类。

解决方案

使用 Capture List ,e.g:

class HTMLElment {
    let name: String
    let text: String?
    // Pay Attention to 'Lazy'
    // Make sure name & text is initialized
    lazy var asHTML: Void -> String = {
    	// Capture list
    	[unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(self.text)</\(self.name)>"
        }
        else {
            return "<\(self.name)>"
        }
    }

    // Omit for simplicity...
}

有 2 点需要注意:

  • lazy 可以确保一个成员只在类对象被完整初始化过之后,才能使用;
  • 就算在 closure 内部多次使用 self ,closure 对 self 的捕获仅发生 1 次(引用计数只加 1 )。

Capture List

本质上来说,closure 作为一个引用类型,解决 reference cycle 的方式和解决类对象之间的 reference cycle 是一样的:

如果引起 reference cycle 的”捕获”不能为 nil ,就把它定义为 unowned ,否则定义为 weak 。而指定“捕获”方式的地方,叫做 closure 的 capture list

而实际在上面的例子中,捕获的 self 不能为 nil (不然 closure 本身也不存在),所以只能定义为 unowned 。

语法要点
  • 如果 closure 带有完整的类型描述,capture list 必须写在参数列表前面;
  • 如果我们要在 capture list 里添加多个成员,用逗号把它们分隔开。

Search

    Post Directory