截图Xcode版本:Xcode 10.1
如果您在用Swift做iOS开发,且暂时不是很清楚什么时候用weak、什么时候用unowned、或者不是很清楚什么是closure capture list,那么,此文尚值一读。
上面的关键字,都和Swift的内存管理机制ARC(Automatic Reference Counting/自动引用计数 )有关,而且都是在解决Reference Cycle(引用循环)需要用到的关键字。
Swift的官方文档Automatic Reference Counting中并没有对ARC进行定义,但是可以参考Objective-C中关于ARC的定义,因为Objective-C中的ARC和Swift的非常相似(very similar)。
Automatic Reference Counting (ARC) is a compiler feature that provides automatic memory management of Objective-C objects.
从定义可知,ARC是编译器提供的一个特性,用于自动管理内存。
结论就是:在大部分情况下,开发者无需操心内存管理的事情:
In most cases, this means that memory management “just works” in Swift, and you do not need to think about memory management yourself.
不过,剩下的这「小部分」情况,也够大家头大的……
这「小部分」情况是什么呢,就是Reference Cycle。
什么是Reference Cycle、Reference Cycle有什么危害?
因为官方文档举例用了Person和Apartment两个classes,所以这里举个可能不太恰当的例子:
想象一下,房地产商在北京建了一套房子Apartment,然后出租给一个租客Tenant。突然某天,晴天一个霹雳,租客意外挂了,同时房地产商又接了P2P暴雷的接力棒——也暴了。这时候,你把这个Apartment想象成电脑中的一块内存,因为知道这个Apartment存在的两方都被导演安排去领饭盒了,这个Apartment就白白浪费在城市中了,如果陆续出现很多这种情况,这个城市很多房产就浪费掉了——好比如电脑中的内存被浪费掉。
上面的情况,可以把它简单理解为Reference Cycle,它会导致内存浪费——内存浪费到一定程度,你的程序可能会crash,所以要避免。下面用官方文档的图示进一步阐述:
image
▲1. 左边是我们潜在租客(Tenant)john,右边是我们房地产商新建的一个Apartment,起了个很洋气的名字unit4A。可以看到,john还没租到房子——apartment属性为nil;房子unit4A也还没找到租客——tenant属性为nil,大家各不相干。
image
▲2. 第二张图可以看到,apartment属性和tenant属性都有值了,而且中间多了两个strong的箭头,表示他们的关系。可以把它们理解成租客和房东签订了合同,确立了租赁关系(但是这个「合同」是有问题的,会导致Reference Cycle)。
image
▲3. 第三张图,我们看到,租客和房地产商都被导演安排去领饭盒了——都被赋值了nil(上面的两个strong箭头不见了)。不过因为他们之前签的合同没有第三方知道,所以大家都以为这个房子还在住人,导致房子没有流回租赁市场,造成浪费。
以上用了一个不太恰当的比喻描述Reference Cycle。
而在Xcode的debug工具Debug Memory Graph,则是用图片这样描绘的:
image
感觉挺形象的(后面会说明Debug Memory Graph的简单用法)
那怎么解决呢?用weak
这个关键字,继续看图示:
image
▲4. 这张图和图2有个小区别,就是下面的strong箭头变成成了灰色的weak。打个比方,他们重新签订合同,规定租客两个月不交租的话,就失去房子的租赁权,要被回收、再出租。
image
▲5 . 一语中的,租客john真的狗带了(被赋值为nil),同时他对Apartment的strong reference也随之消失。而Apartment指向Person实例的是weak reference,不持有Person实例,所以 tenant重设为nil。房子可以重新出租给其他人。但是,如果这时候房地产商也暴雷倒闭了,就出现以下情况:
image
▲6 .房子现在成为无主孤魂了——房地产商不持有,租客也不持有。所以超级管理员——政府就知道可以回收再利用了。
上面举例说明了类实例之间的Reference Cycle和其「解决」方法——用weak
关键字修饰属性,下面看官方文档的代码:
// 这种写法,会引起Reference Cycle,因为大家都是strong reference,互相持有对方,最后得不到释放。
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
// 修正:Apartment改为这种写法,即可解决Reference Cycle
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
// 具体就是改了这里,tenant属性,用weak关键字修饰
weak var tenant: Person? // 因为tenant有可能会是nil,所以是Optional Type,可以理解为,房子不一定有租客。(weak修饰的,一定是Optional Type)
deinit { print("Apartment \(unit) is being deinitialized") }
}
再举个不恰当的例子:
想象一下,我们有「Customer/客户」和「CreditCard/信用卡」两个类。这种情况和「租客」和「房子」的不同点在于,「租客」和「房子」都可以作为独立的存在,它们的lifetime(生命周期)没有跟对方没有直接的因果关系。而「客户」和「信用卡」的关系则不同:「客户」可以单独存在,「信用卡」不行。「信用卡」被创造出来的前提是——肯定先有「客户」(联想一下现实生活:银行都是在用户申请信用卡之后才制卡的,不可能预先制造一堆卡——因为卡上要印「客户」的名字)。所以,「客户」的lifetime(生命周期)一定是和「信用卡」一样、或者更长的。
怎么表达这种关系呢?Swift中用的是unowned
关键字:
class Customer {
let name: String
var card: CreditCard? // 「客户」不一定有「信用卡」,所以这里是Optional Type
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
// 这里用unowned,因为「客户/Customer」和「信用卡」的lifetime一样,或者比「信用卡」更长
unowned let customer: Customer // 有「信用卡」,就一定有「客户」,所以这里不能用Optional Type(nonoptional)
// 有「客户」才能创建「信用卡」,所以init方法,要传Customer参数
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
这时候的图示如下:
image
▲7 .比起上面「租客」和「房子」的关系,右边「信用卡」这个instance,少了一个strong refrence指向它。
之所以叫unowned,可能是因为「Customer」可以拥有(own)「CreditCard」,但是「CreditCard」不能拥有(does not own )「Customer」(或者是:除了指定「Customer」这个owner外,不可以有其他owner,Who knows?)。
个人总结两者的异同:
weak
和unowned
都可以解决Reference Cycle,所以他们相同的地方:
weak
修饰的属性,只能是变量(var),同时只能是Optional类型,因为在模拟实际情境中,这个属性有可能是没有具体值的。换言之你需要手动检查解包后才能使用——所以朝阳群众说这样更安全;unowned
修饰的属性,不能是Optional类型(一定是nonoptional类型),(想象一样,银行肯定要有了「客户」之后,才能制作该「客户」的「信用卡」);weak
属性,初始化后也可以为nil;unowned
属性,初始化后一定都有值;weak
比unowned
更安全(原因见「不同点」第一条);unowned
比weak
性能好一点点(出处——倒数第二段)下面这张插图,比较直观描绘出strong、weak、unowned在属性声明时的异同(图片来源:ARC and Memory Management in Swift):
image
那么,问题来了,我究竟什么时候用weak,什么时候用unowned?
官方文档给出的答案是:
Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.
对于什么时候用unowned
,When to use strong, weak and unowned reference types in Swift and why一文给出类似的答案:
The rule here is to use it if we can guarantee that the lifecycle of the referenced object is equal or greater than the lifetime of the variable pointing to it. In that case we know for sure that the object will not be deallocated and we can safely use it.
上面用对象的「lifetime/生命周期」来解释,相对抽象,感觉也不好判断,在具体实践中或许可以这样判断:
什么?你现在还像我一样黑人问号?那可以简单点:当你不知道用weak
还是用unowned
的时候,用weak吧。为什么?因为群众说weak
更安全——毕竟安全第一。
补充:用unowened + Implicitly Unwrapped Optional解决Reference Cycle
上面说了两种情况:
官方文档还描述了第三种情况:两个属性都不允许是nil——初始化完成后,一定都要有值。(官方文档举例:「Country/国家」一定会有「capitalCity/首都」,「capitalCity」也一定会有它所在的「Country」)
class Country {
let name: String
var capitalCity: City! // 用Implicitly Unwrapped Optional的方式(就是加个感叹号),表示初始化后属性一定有值,不为nil(备注:还是Optional类型,初始化前的默认值也是nil)
init(name: String, capitalName: String) {
self.name = name
// 其实到这里为止,就算是初始化完成了,因为name赋值了,capitalCity也有默认值nil。所以下面这句不写也不会报错。另外,因为初始化完成,所以可以调用selfe了
// 下面这句,是为了满足实际初始化需求:初始化结束后,capitalCity一定有值
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
// 这里和上面的unowned用法一致
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
// 这样一句代码,就可以创建两个实例了(而且他两的lifetime都一样:同时创建、同时销毁——所以可以用unowned)
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
上述情况,就是用unowened、Implicitly Unwrapped Optional解决Reference Cycle。
Implicitly Unwrapped Optional就是在声明capitalCity这个Optional属性时,加上叹号,用来表示初始化后一定有值(「国家」建立了,就一定要有「首都」啊),并且后面也可以不解包直接访问。
Closures(闭包)和class instance(类实例)之间,也有可能产生Reference Cycle,这种情况用capture list解决。
在讲Closures中的Reference Cycle前,先明确以下几点:
self
的实例,有可能会造成Reference Cycle,要多加注意。in
关键字搭配使用——即使Closures中没有参数、没有返回值;先看看Closures、classes实例之间的Reference Cycle长啥样:
image
▲ 这是官方文档的示意图。可以看到,实例化一个HTMLElement
对象后:asHTML
属性指向closure,而closure因为capture了self
,也指向HTMLElement
对象(self
),最后造成Reference Cycle。
image
再看看Xcode中Debug Memory Graph描绘出来的图示,也很形象,有一个箭头,跑了一圈,又回到了HTMLElement
对象自身。
而在代码中,表现是这样的:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
// 如果没有写capture list(方括号内加若干属性),默认是strong reference的。
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
// 这时候只要创建HTMLElement实例,就会Reference Cycle
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
paragraph = nil // 赋值为nil,也不会调用deinit()销毁对象
而解决办法,就是上面说的Capture List:
class HTMLElement {
let name: String
let text: String?
// 在closure里面,用Capture List,将默认的Strong Reference,声明为不增加Reference Count的unowned self(当然,用weak self也有一样的效果,下面说明具体区别)
// 注意,用Capture List,后面就一定要用in
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
看上述代码,用[unowned self]
,把原来默认的strong reference手动改为unowned referenc,即可解决问题。
而关于用weak还是unowned,和class实例之间的Reference Cycle类似。你能确保closure和它capture回来的对象一直引用对方(初始化后一直有值,不可能为nil)、并且会同时销毁,就用unowned
;如果closure capture回来对象,有可能在某一时刻会变成nil(有可能为nil),就用weak
。
什么?也还是不明白?那就不负责任地说一句:用weak吧~
Debug Memory Graph是Xcode 8开始有的一个新工具,将内存中的对象可视化。致力于回答一个问题:
Why does this object exist?
这个工具可以很方便地帮你检查出项目中可能存在的内存问题,也是检查是否有Reference Cycle的神器,具体应用可看如下图示:
image
WWDC2016: Visual Debugging with Xcode 24:40有详细介绍Debug Memory Graph
When to use strong, weak and unowned reference types in Swift and why
strong, weak, unowned - Reference Counting in Swift
Memory Management in Swift: Understanding Strong, Weak and Unowned References