Swift:weak与unowned的奥秘

Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you do not need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.

However, in a few cases ARC requires more information about the relationships between parts of your code in order to manage memory for you.

以上是 Swift 官方教程中,对 Swift 内存管理的解释。通常情况的部分很好理解,Swift 中 ARC 负责绝大部分的内存管理,ARC部分可以参考我的另一篇博客:iOS内存管理初探 – 引用计数、AutoRelease与ARC;但少数情况下,我们需要向ARC提供对象之间的关系来使其正确管理内存。那么少数情况是什么意思呢?这得从Swift中的循环强引用讲起。

Swift 中循环强引用的情景

  • 类实例之间的循环强引用:类实例之间相互强引用了对方
类实例之间的循环引用.png

如图所示的情况中,john指向的对象强引用了unit4A指向的对象,而unit4A指向的对象又强引用了john指向的对象。

  • 闭包引起的循环强引用
闭包循环引用.png

paragraph实例有一个成员asHTML强引用了一个闭包,而这个闭包中又捕获了self,意味着对self的强引用。

在ARC下,引用循环的情况是编译器无法自动解决的,这就是上文提到的少数情况。weak 和 unowned 的存才就是为了给编译器提供更多的信息,来打破循环引用。

利用 weak 和 unowned 杀死循环引用

weak

含义:weak 即弱引用,当把一个实例声明为弱引用时,此实例不会持有这个对象,即不会使对象的引用计数加1。当对象被废弃,其所有的弱引用会被置为 nil。

适用场景

  • 类实例之间的循环强引用:实例之间形成引用循环,且无法确定对象之间生命周期的依赖关系,即无法确定弱引用在某一时刻是否为空时,将可能为空的实例声明为弱引用。由于弱引用可能为 nil,应当声明为可选值。如:
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 }
    weak var tenant: Person?  //公寓的房客可能为空所以声明为weak
    deinit { print("Apartment \(unit) is being deinitialized") }
}

由于 tenant 是弱引用,当 tenant 引用的对象被销毁(如赋值 nil),tenant 被置为空,并且释放对 apartment的强引用,此时 apartment 所指对象就可以正常释放了。

  • 闭包引起的循环强引用:在被捕获的引用可能会变为nil时,在捕获列表中,将闭包内的捕获定义为弱引用。如:
// someClosure 是类成员变量
lazy var someClosure: (Int, String) -> String = {
    //self.delegate 可能在被捕获后变为 nil,所以定义为弱引用,unowned 解释见下文
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // 这里是闭包的函数体
}

由于 self.delegate 指向的是外部对象,生命周期与self无关,所以可能在被捕获后变为nil。(delegate 一般都声明为weak以避免循环引用)

unowned

含义:无主引用,与弱引用一样,当把一个实例声明为无主引用时,此实例不会持有这个对象,即不会使对象的引用计数加1。但与弱引用不同的是,当对象被废弃,其无主引用并不会被置为 nil。

适用场景

  • 类实例之间的循环强引用:实例之间形成引用循环,且可以确定某一实例之外的其他实例有相同或者更长的生命周期时,将此实例声明为无主引用。无主引用总被期望拥有值,当你访问对象被销毁的无主引用时,会触发运行时错误。
class Country {
    let name: String
    var capitalCity: City!  //由于 capitalCity 的生命周期等于 country 的生命周期,可以隐式解析可选属性
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

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

虽然在这个例子中,capitalCity 与 country 的生命周期相同,理论上讲将其中任何一个声明为无主引用都可以打破引用循环,但 capitalCity 与 country 之间有从属关系,所以倾向于将“大”的一方,即 country 声明为无主引用。

  • 闭包引起的循环强引用:在闭包和捕获的实例总是互相引用并且总是同时销毁时,将闭包内的捕获定义为无主引用。
// someClosure 是类成员变量
lazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // 这里是闭包的函数体
}

self 与属于 self 的成员变量 someClosure 生命周期相同,同时销毁,所以声明为无主引用。

unowned(safe) 与 unowned(unsafe)

Automatic Reference Counting

Swift also provides unsafe unowned references for cases where you need to disable runtime safety checks—for example, for performance reasons. As with all unsafe operations, you take on the responsiblity for checking that code for safety.

You indicate an unsafe unowned reference by writing unowned(unsafe). If you try to access an unsafe unowned reference after the instance that it refers to is deallocated, your program will try to access the memory location where the instance used to be, which is an unsafe operation.

在Swift中,写下 unowned 相当于 unowned(safe)。但在官方文档中提到, Swift 还提供了另一种不安全的无主引用 unowned(unsafe) 来禁用运行时的安全检查。运行时的安全检查就是使 unowned(safe) 安全的原因。

unowned(safe):当访问 unowned(safe) 类型的无主引用时,运行时会进行安全检查,如果对象已经废弃,将抛出异常并终止程序。

unowned(unsafe) :unowned(unsafe) 的作用效果实际上相当于 Objective-C 属性标示符中的 assign/unsafeunretained。访问 unowned(unsafe) 类型的无主引用时,运行时的安全检查被禁用,这时会有三种情况:

  • 废弃对象内存还未被覆盖:程序正常运行
  • 废弃对象内存被部分覆盖:奇怪的 crash,不确定的结果
  • 废弃对象内存正好填入新的对象:由于向新的对象发送了调用旧对象方法的消息,会出现 unrecognized selector exceptions

总结

weak unowned
能杀死的强引用循环 实例之间/ 闭包引起 实例之间/ 闭包引起
实例是否能为nil 都允许为nil 其中一个允许为nil,另一个不允许为nil
实例间的生命周期关系 无法确定 一个小于等于另一个

参考文章

The Swift Programming Language (Swift 3.1) : Automatic Reference Counting
What is the difference in Swift between 'unowned(safe)' and 'unowned(unsafe)'?

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

推荐阅读更多精彩内容