弱引用?强引用?未持有?额滴神啊-- Swift 引用计数指导
ARC
ARC 苹果版本的自动内存管理的编译时间特性。它代表了自动引用计数(Automatic Reference Counting)。也就是对于一个对象来说,只有在引用计数为0的情况下内存才会被释放。
Strong(强引用)
让我们从什么是强引用说起。它实质上就是普通的引用(指针等等),但是它的特殊之处在于它能够通过使对象的引用计数+1来保护对象,避免引用对象被ARC机制销毁。本质上来讲,任何对象只要有强引用,它就不会被销毁掉。记住这点对我接下来要讲的引用循环等其他知识来说很重要。
强引用在swift中无处不在。事实上,当你声明一个属性时,它就默认是一个强引用。一般来说,当对象之间的关系为线性时,使用强引用是安全的。当对象之间的强引用是从父层级流向子层级时,用强引用通常也是ok的。
下面是一些强引用的例子
1
2
3
4
5
6
7
8
9
|
class Kraken { let tentacle = Tentacle() //strong reference to child. } class Tentacle { let sucker = Sucker() //strong reference to child } class Sucker {} |
示例代码展示了线性关系。Kraken有一个指向Tentacle实例对象的强引用,而Tentacle又有一个指向Sucker实例对象的强引用。引用关系从父层级(Kraken)一直流到子层级(Sucker)。
同样的,在动画blocks中,引用关系也类似:
1
2
3
|
UIView.animateWithDuration(0.3) { self.view.alpha = 0.0 } |
由于animateWithDuration是UIView的一个静态方法,这里的闭包作为父层级,self作为子层级。
那么当一个子层级想引用父层级会怎么样呢?这里我们就要用到 weak 和 unowned 引用了。
Weak 和 unowned 引用
weak
weak 引用并不能保护所引用的对象被ARC机制销毁。强引用能使被引用对象的引用计数+1,而弱引用不会。此外,若弱引用的对象被销毁后,弱引用的指针会被清空。这样保证了当你调用一个弱引用对象时,你能得到一个对象或者nil.
在swift中,所有的弱引用都是非常量的可选类型(对比 var 和 let) ,因为当没有强引用对象引用的的时候,弱引用对象能够并且会变成nil。
例如,这样的代码不会通过编译
1
2
3
|
class Kraken { weak let tentacle = Tentacle() //let is a constant! All weak variables MUST be mutable. } |
因为tentacle是一个let常量。Let常量在运行的时候不能被改变。因为弱引用变量在没有被强引用的条件下会变成nil,所以Swift 编译器要求你必须用var来定义弱引用对象。
值得注意的地方是,使用弱引用变量能够避免你出现可能的引用循环。当两个对象相互强引用的时候会出现一个引用循环。如果2个对象相互引用对方,ARC就不能给这两个对象发出合适的释放信息,因为这两个对象彼此相互依存。下图是从苹果官方简洁的图片,它很好的解释了这种情况:
一个比较恰当的例子就是通知APIs,看一下下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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等。
在这里,当你调用eatHuman( ) 时,NSNotificationCenter就保留了一个闭包以强引用方式捕获self。经验告诉我们,你应该在deinit方法中清除通知监听对象。这段代码的问题在于我们没有清除掉block直到deinit.但是deinit 永远都不会被ARC机制调用,因为闭包对Kraken实例有强引用。
另外在NSTimers和NSThread也可能会出现这种情况。
解决这种情况的方法就是在闭包的捕获列表中使用对self的弱引用。这样就能够打破强引用循环。那么,我们的对象引用图就会像这样:
把self变成weak不会让self 的引用计数+1,因此ARC机制就能在合适的时间释放掉对象。
想要在闭包使用 weak 和 unowned 变量,你应该用[]把它们括起来。如:
1
2
3
|
let closure = { [weak self] in self?.doSomething() //Remember, all weak variables are Optionals! } |
在上面的代码中,为什么要把 weak self 要放在方括号内?看上去简直秀逗了!在Swift中,我们看到方括号就会想到数组。你猜怎么着?你可以在在闭包内定义多个捕获值!例如:
1
2
3
4
|
let closure = { [weak self, unowned krakenInstance] in //Look at that sweet Array of capture values. self?.doSomething() //weak variables are Optionals! krakenInstance.eatMoreHumans() //unowned variables are not. } |
这样看上去更像是数组了,对吧?现在你知道为什么把捕获值放在方括号里面了吧。那么用我们已了解的东西,通过在闭包捕获列表中加上[weak self],我们就可以解决之前那段有引用循环的通知代码。
1
2
3
|
NSNotificationCenter.defaultCenter().addObserverForName( "humanEnteredKrakensLair" , object: nil, queue: NSOperationQueue.mainQueue()) { [weak self] notification in //The retain cycle is fixed by using capture lists! self?.eatHuman() //self is now an optional! } |
其他我们用到weak和unowned变量的情况是当你使用协议在多个类之间实现代理时,因为Swift中类使用的是reference semantics。在Swift中,结构体和枚举同样能够遵循协议,但是它们用的是value semantics。如果像这样一个父类带上一个子类使用委托使用了代理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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以代理属性的形式对Kraken有着一个强引用,而Kraken在它的Tentacle属性中对Tentacle也有一个强引用。我们通过在代理声明前面加上weak来解决这个问题:
1
|
weak var delegate: LossOfLimbDelegate? |
是不是发现这样写不能通过编译?不能通过编译的原因是非 class类型的协议不能被标识成weak。这里,我们必须让协议继承:class,从而使用一个类协议将代理属性标记为weak。
1
2
3
|
protocol LossOfLimbDelegate: class { //The protocol now inherits class func limbHasBeenLost() } |
我们什么时候用 :class,通过苹果官方文档:
“Use a class-only protocol when the behavior defined by that protocol’s requirements assumes or requires that a conforming type has reference semantics rather than value semantics.”
本质上来讲,当你有着跟我上述代码一样的引用关系,你就用:class。在结构体和枚举的情况下,没有必要用:class,因为结构体和枚举是value semantics,而类是 reference semantics.
UNOWNED
weak引用和unowned引用有些类似但不完全相同。Unowned 引用,像weak引用一样,不会增加对象的引用计数。然而,在Swift里,一个unowned引用有着非可选类型的优点。这样相比于借助和使用optional binding更易于管理。这和隐式可选类型(Implicity Unwarpped Optionals)区别不大。此外,unowned引用是non-zeroing(非零的) ,这表示着当一个对象被销毁时,它指引的对象不会清零。也就是说使用unowned引用在某些情况下可能导致 dangling pointers(野指针url)。你是不是跟我一样想起了用Objective -C的时候, unowned引用映射到了 unsafe_unretained引用。 http://www.krakendev.io/when-to-use-implicitly-unwrapped-optionals/
看到这里是不是有点蛋疼了。既然Weak和unowned引用都不会增加引用计数,它们都能用于解除引用循环。那么我们该在什么使用它们呢?根据苹果文档:
“Use a weak reference whenever it is valid for that reference to become nil at some point during its lifetime. Conversely, use an unowned reference when you know that the reference will never be nil once it has been set during initialization.”
翻译:在引用对象的生命周期内,如果它可能为nil,那么就用weak引用。反之,当你知道引用对象在初始化后永远都不会为nil就用unowned.
现在你就知道了:就像是implicitly unwrapped optional(隐式可选类型),如果你能保证在使用过程中引用对象不会为nil,用unowned 。如果不能,那么就用weak
下面就是个很好的例子。Class 里面的闭包捕获了self,self永远不会为nil。
1
2
3
4
5
6
7
8
9
10
11
12
|
class RetainCycle { var closure: (() -> Void)! var string = "Hello" init() { closure = { self.string = "Hello, World!" } } } //Initialize the class and activate the retain cycle. let retainCycleInstance = RetainCycle() retainCycleInstance.closure() //At this point we can guarantee the captured self inside the closure will not be nil. Any further code after this (especially code that alters self's reference) needs to be judged on whether or not unowned still works here. |
在这种情况下,闭包用强引用形式捕获了self,而self也通过闭包属性保留了一个对闭包的强引用,这就出现了引用循环。只要给闭包添加[unowned self] 就能打破引用循环:
1
2
3
|
closure = { [unowned self] in self.string = "Hello, World!" } |
在这个例子中,由于我们在初始化RetainCycle类后立即调用了闭包,所以我们可以认为self永远不会为nil。
苹果在unowned references也说明了这点:
“Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.”
如果你知道你引用的对象会在正确的时机释放掉,且它们是相互依存的,而你不想写一些多余的代码来清空你的引用指针,那么你就应该使用unowned引用而不是weak引用。
像下面这种懒加载在闭包中使用self就是一个使用unowned的很好例子:
1
2
3
4
5
6
|
class Kraken { let petName = "Krakey-poo" lazy var businessCardName: () -> String = { [unowned self] in return "Mr. Kraken AKA " + self.petName } } |
我们需要用unowned self 来避免引用循环。Kraken 和 businessCardName在它们的生命周期内都相互持有对方。它们相互持有,因此总是被同时销毁,满足使用unowned 的条件。
然而,不要把下面的懒加载变量与闭包混淆:
1
2
3
4
5
6
|
class Kraken { let petName = "Krakey-poo" lazy var businessCardName: String = { return "Mr. Kraken AKA " + self.petName }() } |
在懒加载变量中调用closure时,由于没有retain closure,所以不需要加 unowned self。变量只是简单的把闭包的结果assign 给了自己,闭包在使用后就被立即销毁了。下面的截图很好的证明了这点。(截图是厚着脸皮评论区Алексей的拷贝)
总结
引用循环很坑爹!但是谨慎码代码,理清你的引用逻辑,通过使用weak 和 unowned 能够很好的避免内存循环和 内存泄露。我希望这篇指导手册能够帮到你们。
弱引用?强引用?未持有?额滴神啊-- Swift 引用计数指导的更多相关文章
- Java的四种引用——强引用、软引用、弱引用、虚引用
目录 强引用 软引用 弱引用 虚引用 强引用 拥有强引用的对象永远不会被GC,可以根据引用的get方法获取到被引用对象 软引用 在内存充足的额时候,拥有软引用的对象不会被GC:即将内存溢出的时候,会对 ...
- Java对象引用/JVM分级引用——强引用、软引用、弱引用、虚引用
无论是通过引用计数法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判断对象是否存活都与“引用”有关, 相关资料:如何判断对象是否存活/死去 那么引用究竟是什么?让我们一起来看一下 ...
- java的4种引用 强软弱虚
<img src="https://pic4.zhimg.com/d643d9ab5c933ac475cfa23063bed137_b.png" data-r ...
- java的四种引用:强软弱虚
简介 在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象.也就是说,只有对象处于(reachable)可达状态,程序才能使用它. 从JDK 1.2版本开始,对象的引 ...
- Swift引用计数器
ARC概述 和4.2+版本的Xcode对OC的支持一样,Swift也是使用ARC来管理内存,文档是这么描述的: Swift uses Automatic Reference Counting(ARC) ...
- SaaS架构(一) 弱后端强前端的尝试和问题
最近在公司项目组内部沙龙的时候,提出一个"弱后端强前端"的概念,其实已经在项目内部新的服务有做试点,我们整个SaaS系统,后端主要是JAVA构建,前端是Angular构建.&quo ...
- 使用 EPPlus 封装的 excel 表格导入功能 (二) delegate 委托 --永远滴神
使用 EPPlus 封装的 excel 表格导入功能 (二) delegate 委托 --永远滴神 前言 接上一篇 使用 EPPlus 封装的 excel 表格导入功能 (一) 前一篇的是大概能用但是 ...
- node-ejs-mongodb结合的项目案例-----引用mongoose和未引用mongoose模块
本项目个人尝试了2种方法,一个是直接用mongod,一个是引用mongod里的mongoose. nodejs-ejs-mogondb- nodej+ejs模板,通过mogondb数据查询数据实现简单 ...
- const constptr 和引用的盲点(未解决)
#include<iostream> //const 和 引用的值必须初始化 //等号左侧是const或者const和引用,右侧可以是数字,普通变量-等号左侧是const和指针,右侧必须是 ...
随机推荐
- BZOJ4699 : 树上的最短路
这道题主要是要解决以下两个问题: 问题1: 给定一个点$x$,如何取出所有经过它的下水道? 一条下水道经过$x$等价于它起点在$x$的子树里面且终点不在$x$的子树里面,或者两端点的lca就是$x$. ...
- BZOJ3515 : EvenPaths
首先拓扑排序,并将障碍点按拓扑序平均分成两半. 那么一条$0$到$1$的路径一定是形如: $0$->前一半点->后一半点->第一个后一半障碍点->后一半点->$1$. 对 ...
- Spring In Action(第三版)读书笔记
第一章 Spring之旅 POJO: plain old java object 简单的java对象 DI:Dependency Injection 依赖注入 构造器注入:构造时作为构造器参数传入 p ...
- BZOJ3442: 学习小组
Description [背景] 坑校准备鼓励学生参加学习小组. [描述] 共有n个学生,m个学习小组,每个学生有一定的喜好,只愿意参加其中的一些学习小组,但是校领导为学生考虑,规定一个学生最 ...
- C#文字样式
[字体] 中文名称 英文名称宋体 SimSun黑体 SimHei微软雅黑 Microsoft YaHei微软正黑体 Microsoft JhengHei新宋体 NSimSun新细明体 PMin ...
- Nginx_查看并发连接数
通过查看Nginx的并发连接,我们可以更清除的知道网站的负载情况.Nginx并发查看有两种方法(之所以这么说,是因为笔者只知道两种),一种是通过 web界面,一种是通过命令,web查看要比命令查看显示 ...
- android-数据存储之远程服务器存储
一.如何编码实现客户端与服务器端的交互 <一>JDK内置原生API HttpUrlConnection <二>Android内置的包装API HttpClient浏览器 < ...
- 记录JVM内存模型,参数含义和优化
一.JVM内存模型 (图片来自网络) 根据Java虚拟机规范,JVM将内存划分为: New(年轻代) Tenured(年老代) Perm (永久代) 其中New和Tenured属于堆内存,堆内存会从J ...
- 云计算仿真软件Cloudsim介绍以及类的功能介绍
一·云计算的介绍 云计算仿真软件,称为CloudSim.它是在离散事件模拟包SimJava上开发的函数库,可在Windows和Linux系统上跨平台运行,CloudSim继承了GridSim的编程模型 ...
- rman恢复误删除的一张表(不完全恢复)
恢复误删除的一张表可以使用很多方法,如日志挖掘.闪回等,rman恢复(不完全恢复)肯定不是最好的,也不建议用, 现在我们只是演示一下这种恢复. 1 RMAN备份数据库 2创建测试表 3查看此时的SCN ...