Swift Reference Cycle中的weak,unowned,Closure Capture List

截图Xcode版本:Xcode 10.1

如果您在用Swift做iOS开发,且暂时不是很清楚什么时候用weak、什么时候用unowned、或者不是很清楚什么是closure capture list,那么,此文尚值一读。

TL;DR(太长不看版)

  • weak还是用unowned,和对象的lifetime(生命周期)有关;
  • 如果两个对象的生命周期完全和对方没关系(其中一方什么时候赋值为nil,对对方都没影响),请用weak(来解决Reference Cycle);
  • 如果你有十足信心确保:其中一个对象销毁,另一个对象也要跟着销毁,这时候,可以(谨慎)用unowned(来解决Reference Cycle);
  • closure capture list,是在closures(闭包)内,把capture(捕抓)到的对象、值,放到一个方括号中的语法。在方括号(capture list)中,可以利用weak、unowned关键字把默认的strong reference 改为非strong reference,从而解决closures和类实例(class instance)之间的Reference Cycle;
  • Xcode 8 推出的工具Debug Memory Graph可以在App运行时十分方便定位到产生Reference Cycle的代码。

ARC定义

上面的关键字,都和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。

用weak解决Reference Cycle

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 关键字

那怎么解决呢?用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") }
}

用unowned解决Reference Cycle

再举个不恰当的例子:

想象一下,我们有「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

个人总结两者的异同:

  • 相同点

    weakunowned都可以解决Reference Cycle,所以他们相同的地方:

    • 都不会对object进行reference count(引用计数)加1的操作
    • 都可以解决reference cycle这个问题(这句好像有点废)
  • 不同点

    • weak修饰的属性,只能是变量(var),同时只能是Optional类型,因为在模拟实际情境中,这个属性有可能是没有具体值的。换言之你需要手动检查解包后才能使用——所以朝阳群众说这样更安全;
    • unowned修饰的属性,不能是Optional类型(一定是nonoptional类型),(想象一样,银行肯定要有了「客户」之后,才能制作该「客户」的「信用卡」);
    • weak属性,初始化后也可以为nil;
    • unowned属性,初始化后一定都有值;
    • weakunowned更安全(原因见「不同点」第一条);
    • unownedweak性能好一点点(出处——倒数第二段)

下面这张插图,比较直观描绘出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.

对于什么时候用unownedWhen 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/生命周期」来解释,相对抽象,感觉也不好判断,在具体实践中或许可以这样判断:

  • 当两个属性在实际情况中都允许是nil的时候(「Person」中的「apartment」,「Apartment」中的「tenant」,初始化后,都可以为nil):用weak;
  • 当一个属性允许是nil(「Customer」中的属性「card」),另一个属性不允许是nil(「CreditCard」中的「customer」,「CreditCard」初始化成功后,属性customer一定要有值):就用unowned。

什么?你现在还像我一样黑人问号?那可以简单点:当你不知道用weak还是用unowned的时候,用weak吧。为什么?因为群众说weak更安全——毕竟安全第一。

补充:用unowened + Implicitly Unwrapped Optional解决Reference Cycle

上面说了两种情况:

  • 两个属性同时允许是nil;
  • 一个属性允许是nil,另一个不允许是nil。

官方文档还描述了第三种情况:两个属性都不允许是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)")

上述情况,就是用unowenedImplicitly Unwrapped Optional解决Reference Cycle。

Implicitly Unwrapped Optional就是在声明capitalCity这个Optional属性时,加上叹号,用来表示初始化后一定有值(「国家」建立了,就一定要有「首都」啊),并且后面也可以不解包直接访问。

用capture list解决Reference Cycle

Closures(闭包)和class instance(类实例)之间,也有可能产生Reference Cycle,这种情况用capture list解决。

在讲Closures中的Reference Cycle前,先明确以下几点:

  • Closures是Reference Type——所以才有可能产生Reference Cycle

  • 在Closures内,使用Closures外的常量、变量,这种行为被定义为「capture」,有以下几种语法表现:

    // 如果什么都不写,直接使用。默认是strong类型的capture(想象一下,这时候就有一个粗粗的箭头指向self)
    // 下面这句,意思就是把title实例capture到closure里来用(为什么强制写self,下面解释)
    myFunction { print(self.title) }                    // implicit strong capture
    
    // 把capture的对象放在方括号,是显式地声明capture行为,也是strong类型的capture
    myFunction { [self] in print(self.title) }          // explicit strong capture
    
    // 显式地声明capture回来的实例,是weak类型的reference
    // 因为weak reference只能是optional类型,所以使用时要解包处理(感叹号强制解包)
    myFunction { [weak self] in print(self!.title) }    // weak capture
    
    // 显式地声明capture回来的实例是unowned类型的reference
    myFunction { [unowned self] in print(self.title) }  // unowned capture
    
    • 上面closures的第一种写法,在closure内,使用外面的title,Swift强制要加上self,否则编译报错。原因是为了提醒用户,这里「capture」了self的实例,有可能会造成Reference Cycle,要多加注意。
    • 方括号内,可以放多个值;
    • Closures内方括号放若干个值,这种语法,叫做「Capture List」;
    • 如果显式地把「Capture List」写出来,就一定要和in关键字搭配使用——即使Closures中没有参数、没有返回值;
    • 对于Value Type,显式地用方括号capture回来的值,会copy一份到closures里面(是不能修改的let常量),这时候和原来外面的值就没关系了;如果不是写在「Capture List」里,closures内外就共享一个值;
    • 而对于Reference Type,无论是否显式地写「Capture List」,指向的都是同一个Reference;「Capture List」的作用,是用于声明是weak,还是unowned类型的Reference。
    • Closures、classes实例之间的Reference Cycle,就是用这种方法(Capture List)来解决的。

先看看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

Debug Memory Graph是Xcode 8开始有的一个新工具,将内存中的对象可视化。致力于回答一个问题:

Why does this object exist?

这个工具可以很方便地帮你检查出项目中可能存在的内存问题,也是检查是否有Reference Cycle的神器,具体应用可看如下图示:

image

WWDC2016: Visual Debugging with Xcode 24:40有详细介绍Debug Memory Graph

Reference

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容