处理对象reference cycle的三种方式

泊学高清学习视频
泊阅开发者文档
我们在上一段视频里了解了reference cycle的成因。这次,我们来了解具体的解决方案。Reference cycle的解决方式,根据class member是否允许为nil,有着不同的处理方式。我们来分别看一下它们:

class member允许为nil时 - weak reference

在我们之前的例子里,Apartment和Person中引起reference cycle的数据成员都允许为nil,对于这种情况,我们可以像下面这样使用weak reference来解决reference cycle:

weak var name: Type

对于一个weak reference来说:

  • 当其指向类对象时,并不会添加类对象的引用计数;

  • 当其指向的类对象不存在时,ARC会自动把weak reference设置为nil;


“由于weak reference的特定,它只能被定义成var。”
——特别提示


了解了weak reference特性之后,我们可以把Apartment修改成这样:

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

    // omit for simplicity...
}

var mars: Person? = Person(name: "Mars")
var apt11: Apartment? = Apartment(unit: "11", owner: mars!)

mars!.apartment = apt11
apt11!.tenant = mars

mars = nil
apt11 = nil

之后,当我们把mars设置为nil的时候,Apartment和Person的关系就变成了这样:
bo-reading-mars-nil@2x.jpg

由于已经没有strong reference指向mars,于是mars就被ARC释放了。接下来,我们把apt11设置成nil:
bo-reading-apt11-nil@2x.jpg

这时,也没有任何strong reference指向apt11了,它也会被ARC释放。这就是当引起reference cycle的两个member允许为nil时,我们使用的解决方案。


“weak reference用于解决成员允许为nil的reference cycle。”
——特别提示


当有一个member不能为nil时 - unowned

来看另外一个应用场景。我们把Person改成下面这样:

class Person {
    let name: String
    var apartment: Apartment?
    var property: Apartment?
     
     // omit for simplicity...   
}

由于不是每个人名下都有房产,因此property允许为nil,我们把它定义为Apartment optional。接下来,把Apartment的代码改成这样:

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

    init(unit: String, owner: Person) {
        self.unit = unit
        self.owner = owner
        print("Apartment \(unit) is being initialized.")
    }

     // omit for simplicity...   
}

由于每个Apartment一定会有房东,不能为nil,因此,我们把它定义为Person。

接下来,当我们再次创建mars和apt11的时候:

var mars: Person? = Person(name: "Mars")
var apt11: Apartment? = Apartment(unit: "11", owner: mars!)

mars!.apartment = apt11
apt11!.tenant = mars

mars = nil
apt11 = nil

我们不难发现,添加的owner又引入了一个reference cycle。
bo-reading-owner-ref-cycle@2x.jpg

在上面这个图里,即使我们把mars和apt11设置为nil:
bo-reading-owner-ref-cycle1@2x.jpg

owner这个strong reference会让mars存活,进而apartment会让apt11存活。这就形成了一个新的reference cycl。

解决这种类型的reference cycle也很简单,我们把用下面的方式:

unowned let name: Type

把owner设置为一个unowned reference就可以了

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

    // Omit for simplicity...
}

和strong reference相比,unowned reference只有一个特别:不会引起对象引用计数的变化。因此,当我们把mars和apt11设置为nil时,对象的关系就会变成这样:
bo-reading-owner-ref-cycle2@2x.jpg

此时,已经没有strong reference引用mars,它会先被ARC释放掉,之后,引用apt11的apartment也不存在了,apt11也会被ARC清除。这就是reference cycle中一方不能为nil时的解决方案。


“unowned reference用于解决成员不允许为nil的reference cycle。”
——特别提示


当两个member都不允许为nil时
这种情况相对复杂一些,我们要让两个类成员互相配合处理这个问题。为了简单说明这种情况,我们重新定义两个类:

class Country {
    let name: String
    
    init(name: String) {
        self.name = name
    }
}

class City {
    let name: String
    
    init(name: String) {
        self.name = name
    }
}

起初,它们很简单,Country代表国家,City代表城市,各自的init()函数用来构建对象。接下来,我们希望添加下面这样的语义:Country要有一个member表示首都,City要有一个country表示城市的归属。我们先做第一步的修改:

class Country {
    let name: String
    var capital: City

    init(name: String) {
        self.name = name
    }
}

class City {
    let name: String
    let country: Country

    init(name: String) {
        self.name = name
    }
}

在我们的例子里,一个国家不可能没有首都,一个城市也不可能没有国家归属,因此,它们都只能是普通的类对象,而不能是一个Optional。接下来,我们来处理它们的初始化问题。首先,我们按照一般的方式处理City:

class City {
    let name: String
    let country: Country
    
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

然后,处理Country的init():

class Country {
    let name: String
    var capital: City
    
    init(name: String, capitalName: String) {
        self.name = name
        // Syntax Error!!!
        self.capital = City(name: capitalName, country: self)
    }
}

在Country的init()里,我们把正在创建的Country对象传递给了City的init(),这样做,只在语义上是正确的,语法上,Swift认为构建City时,Country还没有完成初始化(self.captical还没有确定的值),因此,它不允许我们在这个时候把self传递给Country。
bo-reading-uninit-self@2x.jpg

要想构建City时,让Swift认为Country已经构造完,唯一的做法就是captical有一个默认值nil。至此,对于Capital,我们有了两个看似冲突的需求:

  • 对Country的用户来说,不能让他们知道capital是一个optional;

  • 对Country的设计者来说,它必须像Optional一样有一个默认的nil;

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

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

至此,两个彼此关联的类就可以正常的构建和初始化了。我们分别定义一个变量:

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

cn = nil
bj = nil

经历过之前的多个例子之后,我们很快就可以发现,当cn和bj为nil时,cn.captical和其内建的City仍旧会保持彼此“存活”在内存里,而解决这个问题的办法其实之间我们已经处理过了,把captical定义为unowned就可以了。


“unowned reference和implicitly unwrapped optional配合在一起,用于解决引起reference cycle的两个成员都不允许为nil的情况。”
——特别提示


接下来

在这段视频里,我们了解了处理类对象reference cycle的3种不同的方式。在下一段视频里,我们会发现,使用Closure会带来类似的问题。我们也会向大家介绍对应的处理办法


泊学
166 声望27 粉丝

现如今的开发者,早已不是一句我是“某某程序员”可以说清的了。全栈,渐渐从一个概念,变成了对更好工作的核心需求。