"WEAK, STRONG, UNOWNED, 老天爷!" - SWIFT中的引用关系说明

这篇文章为http://krakendev.io/blog/weak-and-unowned-references-in-swift的个人翻译版本。
原作者:Hector Matos
原文章发布日期:2015-06-25


我发现自己写代码的时候经常担心强引用循环(retain cycles)的出现。我觉得这个和其他问题一样比较常见。不知道你是什么情况,我反正总是听见"我什么时候要用关键词weak?'unowned'这坨东西到底是啥玩意儿?"这类声音。我们发现的问题是我们知道在swift代码中要去用strong,weak和unowned说明符来避免强引用循环,但是我们不大了解具体用哪一个。好在我知道它们是啥,还知道啥时候去用他们!希望这篇文章能教会你知道什么时候,并且在哪里用这3个说明符。

咱们开始吧

ARC

ARC是自动内存管理Apple版本的一个编译时特性(compile time feature)。全称是Automatic Reference Counting。意思是对于一个对象来说,只有在没有任何强引用指向它时,该对象占用的内存才会被回收。

STRONG - 强引用

从什么是强引用说起。它本质上是一个普通的引用(指针或者其他有相同意思的东西),但是它特殊在能够通过将该引用指向对象(object)的保留计数(retain count)增加1来保护这个对象不被ARC回收。实质上,哪怕任何一个东西的一个强引用指向了这个对象,这个对象就不会被回收。记住这点,待会儿讲强引用循环和相关东西的时候会用到。
强引用在swift中几乎随处可见。实际上声明一个属性(property)的时候默认就是一个强引用。通常在关系层级是线性的时候用强引用问题不大。当强引用从父层级流向子层级的时候,这个强引用的使用总是没问题。
这有个强引用的例子。

class Kraken {  
    let tentacle=Tentacle() //对子层级的强引用。
}
class Tentacle {
    let sucker=Sucker()    //对子层级的强引用。
}
class Sucker{}

*/Kraken的意思是海妖,Tentacle的意思是触手,sucker的意思是吸盘...译者注/*

例子中是一个线性的关系层级。Kraken有一个指向Tentacle实例的强引用,Tentacle实例又有一个指向Sucker实例的强引用。引用关系的流向从父层级(Kraken)一直向下流到子层级(Sucker)。
在animation block里引用层级也是类似的:

UIView.animateWithDuration(0.3) {
    self.view.alpha=0.0
}

因为animateWithDuration是UIView的一个静态方法,这里的闭包是父层级,self是子层级。
如果子层级想引用父层级怎么办?这就是我们要用弱引用和unowned引用的地方。

WEAK AND UNOWNED REFERENCES - 弱引用和UNOWNED引用

WEAK - 弱引用

弱引用就是一个保护不了其所指对象不被ARC回收的指针。强引用能让它对象的保留计数增加1,弱引用不能
swift中,所有的弱引用都是非常量的可选类型(non-constant Optionals)(想一下var和let的关系),因为在没有其他强引用指向的时候,这个引用能,并且会被改变成nil。
例如下面的代码就不能通过编译:

class Kraken {
    weak let tentacle = Tentacle() //let是一个常量。所有的weak变量都必须是可变(mutable)的。
}

因为tentacle是一个let常量。Let由于规范限制使得其在运行时不能够被改变。因为弱引用变量(weak variables)在没有任何强引用指向它们时是会被改变成nil的,所以swift编译器要求你将弱引用变量声明成var。
那些会出现潜在的强引用循环的地方就是使用弱引用变量的关键之处。强引用循环发生在两个对象彼此之间都用强引用指向对方的情况下,ARC不会对其中任何一个实例发出正确的释放信号代码(release message code),因为这两个实例正彼此保护着对方。这有个来自Apple的简洁图片,非常明了的展示了这点:

下面是一个能展示强引用循环的很棒的例子,其中用到了NSNotification API(还是比较新的API)。看看下面的代码吧:

class Kraken {
    var notificationObserver: ((NSNotification) -> Void)? 
    init() {notificationObserver = NSNotificationCenter.defaultCenter().addObserverForName("humanEnteredKrakensLair", object: nil, queue: NSOperationQueue.mainQueue()) { notification in
            self.eatHuman()
           }
    }
    deinit {
            if notificationObserver != nil {
                NSNotificationCenter.defaultCenter.removeObserver(notificationObserver)
            }
    }
}

到这儿我们就搞出了一个强引用循环。你看,swift里的闭包与Objective-C里的blocks极像。如果一个变量是在闭包外面声明的,在闭包里面引用这个变量就会产生出另一个强引用。此种情况下仅有的例外就是使用值类型的变量,比如swift里的Ints,Strings,Arrays和Dictionaries。
这里NSNotificationCenter保留了一个闭包,当你调用eatHuman()方法时这个闭包以强引用的方式捕获了self。问题是:我们直到deinit的时候才清空这个闭包,但是deinit永远不会被ARC调用,因为这个闭包有一个对Kraken实例的强引用!
NSTimersNSThread的地方也会出现这种情况。
解决方法是在闭包的捕获列表(capture list)里使用一个对self的弱引用。这就打破了强引用循环。到了这里,我们的对象引用关系图就变成了这样:

把self变成weak不会给self的保留计数加1,这就能让ARC在正确的时间将其合理的销毁。
要在闭包里使用weak和unowned变量的话,需要在闭包体内用[]语法。例如:

let closure = { [weak self] in
    self?.doSomething() //记住,所有的weak变量都是可选类型。
}

为什么weak self会在方括号里?这看起来很怪!Swift中我们看见方括号就会想到数组。你猜怎么着?你可以在闭包里指定多个待捕获的值!比如:

let closure = { [weak self, unowned krakenInstance] in //瞧这个捕获了多个值的数组
    self?.doSomething() //weak变量是可选类型
    krakenInstance.eatMoreHumans() //unowned 变量不是可选类型
}

看起来就像数组多了吧?现在你就知道了为什么捕获值是写在方括号里的。好,用我们现在所学到的,在上面notification代码的闭包捕获列表中加上[weak self]就可以解决强引用循环的问题:

NSNotificationCenter.defaultCenter().addObserverForName("humanEnteredKrakensLair", object: nil, queue: NSOperationQueue.mainQueue()) { [weak self] notification in //使用捕获列表消除了强引用循环!
    self?.eatHuman() //self现在是一个可选类型了!
}

用到weak和unowned变量的另外一个地方就是使用协议(protocol)在多个class间去实现委托(delegation)的情况,因为swift中class是引用类型。结构体(structs)和enum(枚举)也能遵循协议,但是它们是值类型。如果一个父类带上一个子类使用委托,像这样:

class Kraken: LossOfLimbDelegate {
    let tentacle = Tentacle()
    init() {
        tentacle.delegate = self
    }
    func limbHasBeenLost() {
        startCrying()
    }
}
protocol LossOfLimbDelegate {
    func limbHasBeenLost()
}
class Tentacle {
    var delegate: LossOfLimbDelegate?
    func cutOffTentacle() {
        delegate?.limbHasBeenLost()
    }
}

那么我们就需要用weak变量。在这个例子里Tentacle以它所拥有的代理属性(delegate property)的形式持有一个对Kraken的强引用,同时Kraken在它的tentacle属性中也有一个对Tentacle的强引用。我们在代理声明之前加上一个weak说明符来解决:

weak var delegate: LossOfLimbDelegate?

你说什么?编译不通过?好吧,因为非class类型的协议不能被标识为weak。
此时,我们得用一个唯类协议(class protocol)来使得代理属性能够标识成weak。让我们的协议继承:class

protocol LossOfLimbDelegate: class { //Protocol 现在继承了class
    func limbHasBeenLost()
}

什么时候不用:class? Apple的文档里说:

当一个协议需求所定义的行为(behavior)能够确保或要求遵循这个协议的类型是引用类型而非值类型的时候,使用唯类协议。

基本上,如果你自己代码的引用层级和我上面写的一样的话,你就加上:class。对于使用结构体或者枚举的情况,就不需要:class了,因为结构体和枚举是值类型,class是引用类型。

UNOWNED

弱引用和unowned引用本质上是一样的。Unowned引用并不增加它所引用对象的保留计数。然而swift语言中unowned引用的额外的优点是它为非可选类型。这使得它用起来更方便,不用再去引入可选绑定(optional binding)。这和隐式可选类型(Implicity Unwarpped Optionals)没什么区别。
到这里就有点儿乱了。弱引用和unowned引用都不增加保留计数。它们都用来解决强引用循环的问题。那么我们什么时候用它们?Apple的文档说:

当一个引用在其生命周期中变为nil时依然合理,就把这个引用定义为弱引用。相反,如果你事先知道一个引用在被设置好了之后不会再变成nil,就把它定义成unowned引用。

你知道答案了:就和隐式可选类型一样,如果你能确保这个引用在被用到的时候肯定不是nil的话,就用unowned,如果不确保,就得用弱引用。
下面是一个典型的例子,一个class的闭包中捕获的self不会变成nil,这就生成了一个强引用循环:

class RetainCycle {
    var closure: (() -> Void)!
    var string = "Hello"
    init() {
        closure = {
            self.string = "Hello, World!"
        }
    }
}
//初始化class,并激活强引用循环。
let retainCycleInstance = RetainCycle()
retainCycleInstance.closure() //此时我们可以确保闭包中捕获的self不会再是nil了。此后的任何代码(尤其是改变self的引用的代码)都需要判断一下unowned是否在这儿还起作用。

上面的例子里,闭包以强引用的形式捕获了self,同时self通过自己的闭包属性也保留了一个对该 闭包的强引用,这就造出了强引用循环。简单的给闭包加一个[unowned self]就能打破这个循环:

closure = { [unowned self] in
     self.string = "Hello, World!"
}

因为我们在初始化RetainCycle类之后立即调用了闭包,我们就可以认为self不会再是nil了。

结论

强引用循环很不好。但是认真的写代码,考虑清楚自己的引用层级,合理的选用weak和unowned引用就可以避免内存泄露和内存遗弃。希望这篇文章会帮到你。
祝码农们编程愉快!

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

推荐阅读更多精彩内容