ARC概述

和4.2+版本的Xcode对OC的支持一样,Swift也是使用ARC来管理内存,文档是这么描述的:

Swift uses Automatic Reference Counting(ARC) to track and manage your app’s memory usage. In most case, this means that memory management “just works” in Swift, and you do not need to think about memory management yourself.

P.S:这段话来自于《The Swift Programming Language》,若没有特别说明,本文所涉及的「Swift文档」「官方文档」均指的是它;本文是《The Swift Programming Language》的《Automatic Reference Counting》章节的学习笔记。

P.S:有个问题,如何理解「In most case」,哪些情况下需要自己管理内存呢?

ARC是如何工作的

有过OC开发经验的人都对ARC有了一定程度的了解,来回顾一下吧。

每次创建一个类的实例,ARC就会分配一个内存块,用来存储这个实例的相关信息。这个内存块保存着实例的类型以及这个实例相关的属性的值。

当实例不再被使用时,ARC释放这个实例使用的内存,使这块内存可作它用。这保证了类实例不再被使用时,它们不会占用内存空间。

但是,如果ARC释放了仍在使用的实例,那么你就不能再访问这个实例的属性或者调用它的方法。如果你仍然试图访问这个实例,应用极有可能会崩溃。

为了保证不会发生上述的情况,ARC跟踪与类的实例相关的属性、常量以及变量的数量。只要有一个有效的引用,ARC都不会释放这个实例。

为了让这变成现实,只要你将一个类的实例赋值给一个属性/常量/变量,这个属性/常量/变量就是这个实例的强引用(strong reference)。之所以称之为「强」引用,是因为它强持有这个实例,并且只要这个强引用还存在,就不能销毁实例。

类实例之间的强引用环

类实例之间的强引用环介绍

我们有可能会写出这样的代码,一个类的实例永远不会有0个强引用。具体来说,可能会发生这样的情况,两个类实例(譬如instanceA和instanceB)彼此保持对方的强引用,因为instanceA被instanceB所强持有,所以instanceA被释放的前提是instanceB被释放,同样,因为instanceB被instanceA所强持有,所以instanceB被释放的前提是instanceA被释放,这在逻辑上形成了一个死循环,业内称之为「强引用环」。

解决类实例之间的强引用环

Swift提供两种方法来解决强引用环:「弱引用」(weak references)和「无主引用」(unowned references)。

P.S:在OC中,解决强引用环的手段同样是「弱引用」,却没有这里提到的「无主引用」(unowned references)。

「弱引用」和「无主引」用允许引用环中的一个实例引用另外一个实例,但不是强引用。因此实例可以互相引用但是不会产生强引用环。

如何选用「弱引用」和「无主引用」呢?

  • 对于生命周期中引用会变为nil的实例,使用弱引用;
  • 对于初始化时赋值之后引用再也不会赋值为nil的实例,使用无主引用;

P.S:实在看不出「无主引用」相对于「弱引用」有啥特别的地方。

弱引用

弱引用不会增加实例的引用计数,因此不会阻止ARC销毁被引用的实例。这种特性使得引用不会变成强引用环。声明属性或者变量的时候,关键字weak表明引用为弱引用。关于弱引用:

  • 「弱引用」只能声明为变量类型,因为运行时它的值可能改变,「弱引用」绝对不能声明为常量;
  • 因为「弱引用」可以没有值,所以声明「弱引用」的时候必须是可选类型的;
  • 当「弱引用」所引用的实例的「强引用」次数为0时,ARC会将该实例销毁,并将所指向它的所有「弱引用」赋值为nil

无主引用

弱引用相似,无主引用也不强持有实例。但是和弱引用不同的是,无主引用默认始终有值。因此,无主引用只能定义为非可选类型(non-optional type)。在属性、变量前添加unowned关键字,可以声明一个无主引用

因为是非可选类型,因此当使用无主引用的时候,不需要解包(unwrapping)可以直接访问。不过非可选类型变量不能赋值为nil,因此当实例被销毁的时候,ARC无法将引用赋值为nil

注意:
当实例被销毁后,试图访问该实例的无主引用会触发运行时错误。使用无主引用时请确保引用始终指向一个未销毁的实例。

虽然无主引用弱引用的区别非常明显,但对其存在的意义仍然非常迷惑。举个栗子吧!

接下来的例子定义了两个类,Customer和CreditCard,模拟了银行客户和客户的信用卡。每个类都一个属性,存储另外一个类的实例。这样的关系可能会产生强引用环

在这个模型中,消费者不一定有信用卡(简单起见,假设消费者最多只有一张信用卡),但是每张信用卡一定对应一个消费者。鉴于这种关系,Customer类有一个可选类型属性card,而CreditCard类的customer属性则是非可选类型的。如下:

class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { println("\(name) is being deinitialized") }
}
 
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { println("Card #\(number) is being deinitialized") }
}

P.S:unowned既可以修饰常量也修饰变量,但似乎更多的使用场景是被用来修饰常量。

下面的代码定义了一个叫john的可选类型Customer变量,用来保存某个特定消费者的引用。因为是可变类型,该变量的初始值为nil:

var john: Customer?

现在创建一个Customer实例,然后用它来初始化CreditCard实例,并把刚创建出来的CreditCard实例赋值给Customer的card属性:

john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

我们来看看此时的引用关系:

Customer实例持有CreditCard实例的强引用,而CreditCard实例则持有Customer实例的无主引用

因为customer的无主引用,当破坏john变量持有的强引用时,就没有Customer实例的强引用了:

此时Customer实例被销毁。然后,CreditCard实例的强引用也不复存在,因此CreditCard实例也被销毁:

john = nil
// prints "John Appleseed is being deinitialized"
// prints "Card #1234567890123456 is being deinitialized"

上例说明,john变量赋值为nil后,Customer实例和CreditCard实例的deinitializer方法都打印了“deinitialized”信息。

通过这个示例,能够更深刻理解到「弱引用」和「无主引用」在解决强引用环之间的区别了:

  • 两个属性的值都可能是nil,并有可能产生强引用环,这种场景下适合使用弱引用;
  • 一个属性可以是nil,另外一个属性不允许是nil,并有可能产生强引用环,这种场景下适合使用无主引用;(有点从属的关系)

P.S:个人认为将「unowned」翻译为「无主引用」不是很好;根据我的理解,unowned有种「you own me, but I don’t own you」的从属关系。到目前来看,没有unowned也是可以的,weak完全可以全方位代替它,但是在某些场合,它比weak表达的逻辑更清晰,只是最好不要用「无主引用」来翻译它吧。(2015年8月补充)

无主引用和隐式解包的可选属性

上文介绍了「两个类实例之间互相引用导致可能造成强引用环」的场景以及相应的解决方案。

然而,还存在另外一种场景,两个类实例互相引用,在初始化完成后,二者谁都不能为nil。使用「强引用」+「弱引用」?不行,因为「弱引用」必须是optional,这就允许引用为nil了;使用「var optional强引用」+「无主引用」?同样不行;使用「强引用」+「强引用」?更不行;采用「无主引用」+「无主引用」?似乎挺别扭的!

此时一般采用的模式是:「隐式解包的可选型强引用」+「无主引用」。

在这种模式下,在初始化完成后我们可以立即访问这两个变量(而不需要可选展开),同时又避免了引用环。’self’ used before all stored properties are initialized

下面的例子顶一个了两个类,Country和City,都有一个属性用来保存另外的类的实例。在这个模型里,每个国家都有首都,每个城市都隶属于一个国家。所以,类Country有一个capitalCity属性,类City有一个country属性:

class Country {
let name: String
let capitalCity: City!
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
}
}

P.S:这段代码来自于Swift手册,但不晓得怎么回事儿,怎么也无法通过编译(或许是Xcode的问题吧),无奈把第三行的let改为var才可以。

City的初始化函数有一个Country实例参数,并且用country属性来存储这个实例。这样就实现了上面说的关系。

Country的初始化函数调用了City的初始化函数。但是,只有Country的实例完全初始化完后(在Two-Phase Initialization),Country的初始化函数才能把self传给City的初始化函数。

为满足这种需求,通过在类型结尾处加感叹号(City!),我们声明Country的capitalCity属性为隐式解包的可选类型属性。就是说,capitalCity属性的默认值是nil,不需要展开它的值就可以直接访问。

因为capitalCity默认值是nil,一旦Country的实例在初始化时给name属性赋值后,整个初始化过程就完成了。这代表只要赋值name属性后,Country的初始化函数就能引用并传递隐式的 self。所以,当Country的初始化函数在赋值capitalCity时,它也可以将self作为参数传递给City的初始化函数。

综上所述,你可以在一条语句中同时创建Country和City的实例,却不会产生强引用环,并且不需要使用感叹号来展开它的可选值就可以直接访问capitalCity:

let china = Country(name: "中国", capitalName: "北京")
let shanghai = City(name: "上海", country: china)
println("\(china.name)的首都是\(china.capitalCity.name)")
println("\(shanghai.name)是\(shanghai.country.name)的一个城市")
 
/*输出:
中国的首都是北京
上海是中国的一个城市
*/

闭包之间的强引用环

闭包之间的强引用环介绍

前面我们看到了强引用环是如何产生的,还知道了如何引入弱引用和无主引用来打破引用环。

将一个闭包赋值给类实例的某个属性,并且这个闭包使用了实例,这样也会产生强引用环。这个闭包可能访问了实例的某个属性,例如self.someProperty,或者调用了实例的某个方法,例如self.someMethod。这两种情况都导致了闭包使用self,从而产生了「强引用环」。

因为诸如类这样的闭包是引用类型,导致了强引用环。当你把一个闭包赋值给某个属性时,你也把一个引用赋值给了这个闭包。实质上,这个之前描述的问题是一样的 — 两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个是类实例,另一个是闭包。

在OC中,也存在block与类实例之间的「强引用环」问题,OC的解决方案通常也是「弱引用」(经典的weakSelf)。

不同于OC,Swift提供了一种更优雅的方法来解决这个问题,我们称之为「闭包占用列表」(closuer capture list)。

解决闭包之间的强引用环

在定义闭包时同时定义「capture list」作为闭包的一部分,可以解决闭包和类实例之间的「强引用环」问题。「capture list」定义了闭包内占有一个或者多个引用类型的规则,和解决两个类实例间的强引用环一样,声明每个占有的引用为弱引用或无主引用,而不是强引用。根据代码关系来决定使用弱引用还是无主引用。

注意:Swift有如下约束:只要在闭包内使用self的成员,就要用self.someProperty或者self.someMethod(而非只是somePropertysomeMethod)。这可以提醒你可能会不小心就占有了self。

定义Capture List

「capture list」中的每个元素都是由weak或者unowned关键字和实例的引用(如 self或someInstance)组成,每一对都在花括号中,通过逗号分开。

「capture list」放置在闭包参数列表和返回值类型之前,如下:

lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}

如果闭包没有指定参数列表或者返回类型(可以通过上下文推断),那么占有列表放在闭包开始的地方,跟着是关键字in,如下:

lazy var someClosure: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}

「弱引用」和「无主引用」

当闭包和占有的实例总是互相引用时并且总是同时销毁时,将闭包内的占有定义为无主引用。

相反的,当占有引用有时可能会是nil时,将闭包内的占有定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。利用这个特性,我们可以在闭包内检查他们是否存在。

注意:如果占有的引用绝对不会置为nil,应该用「无主引用」,而不是「弱引用」。

举个栗子:

class HTMLElement {
 
let name: String
let text: String?
 
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 {
println("\(name) is being deinitialized")
}
}

Swift引用计数器的更多相关文章

  1. OC-内存管理-基本原理与引用计数器

    基本原理 1. 什么是内存管理 移动设备的内存极其有限,每个app所能占用的内存是有限制的 当app所占用的内存较多时,系统会发出内存警告,这时得回收一些不需要再使用的内存空间.比如回收一些不需要使用 ...

  2. php 调试工具及学习PHP垃圾回收机制了解引用计数器的概念

    php代码工具:Xdebug  与分析工具 WinCacheGrind Xdebug之函数大全: string xdebug_call_class()返回当前被调用的函数或方法所属的类的类名 stri ...

  3. OC语法6——内存管理之引用计数器(retain,release)

    OC内存管理: 一.引用计数器: Java有垃圾回收机制(Garbage Collection,GC).也就是说当我们创建对象后,不需要考虑回收内存的事,Java的垃圾回收机制会自动销毁该对象,回收它 ...

  4. Objective-C:MRC手动释放对象内存举例(引用计数器)

    手机内存下的类的设计练习: 设计Book类, 1.三个成员变量:    title(书名)author(作者).price(价格) 2.不使用@property,自己完成存取方法(set方法,get方 ...

  5. 学习PHP垃圾回收机制了解引用计数器的概念

    php变量存在一个叫"zval"的变量容器中,"zval"变量容器包括含变量的类型和值,还包括额外的两个字节信息,分别是“is_ref”表示变量是否属于引用,“ ...

  6. Unity 游戏框架搭建 (二十二) 简易引用计数器

    引用计数是一个很好用的技术概念,不要被这个名字吓到了.首先来讲讲引用计数是干嘛的. 引用计数使用场景 有一间黑色的屋子,里边有一盏灯.当第一个人进屋的时候灯会打开,之后的人进来则不用再次打开了,因为已 ...

  7. php 内核变量 引用计数器写时复制

    写时复制,是一个解决内存复用的方法,就是你在php语言层,如$d=$c=$b=$a='value';把$a赋给另一个或多个变量,这时这个变量都只占用一个内存块,当其中一个变量值改变时,才会开辟另一个内 ...

  8. netty 引用计数器 ,垃圾回收

    netty 引用计数器 ,垃圾回收 https://blog.csdn.net/u013851082/article/details/72170065 Netty之有效规避内存泄漏 https://w ...

  9. Linux内核引用计数器kref结构

    1.前言 struct kref结构体是一个引用计数器,它被嵌套进其它的结构体中,记录所嵌套结构的引用计数.引用计数用于检测内核中有多少地方使用了某个对象,每当内核的一个部分需要某个对象所包含的信息时 ...

随机推荐

  1. lstm作的第一首诗,纪念

    黄獐春风见破黛,十道奇昌犹自劳. 开领秦都色偏早,未知长柳是来恩. 争时欲下花木湿,早打红筵枝上香. 酣质矫胶麦已暮,丝窗瑞佩满含龙. 感觉有点意思哈,花木对应枝上,还有点对仗的意味. 于是接着又弄了 ...

  2. serialVersionUID的作用以及如何用idea自动生成实体类的serialVersionUID

    转载:http://blog.csdn.net/liuzongl2012/article/details/45168585 serialVersionUID的作用: 通过判断实体类的serialVer ...

  3. 微信小程序日期定位弹出框遮挡问题

    只需要用bindtap绑定一个点击后的操作(隐藏键盘): wx.hideKeyboard()

  4. 邻接表的使用及和vector的比較

    这几天碰到一些对建边要求挺高的题目.而vector不好建边,所以学习了邻接表.. 以下是我对邻接表的一些看法. 邻接表的储存方式 邻接表就是就是每一个节点的一个链表,而且是头插法建的链表,这里我们首先 ...

  5. WeX5开发指南

    WeX5入门.UI2开发.App开发.服务端开发.扩展资料学习. 1 新手入门 1.1 运行WeX5的demo(视频) 1.2 App开发.调试.打包部署完整过程(视频) 1.3 创建第一个应用(视频 ...

  6. 【转载】C#扫盲之:带你掌握C#的扩展方法、以及探讨扩展方法的本质、注意事项

    1.为什么需要扩展方法 .NET3.5给我们提供了扩展方法的概念,它的功能是在不修改要添加类型的原有结构时,允许你为类或结构添加新方法. 思考:那么究竟为什么需要扩展方法呢,为什么不直接修改原有类型呢 ...

  7. Springmvc返回信息乱码解决

    恩...基本上所有的配置信息都弄上了,但是还是乱码,最后在方法上面添加了下面的参数,就完美解决了: @RequestMapping(value="/action.action",m ...

  8. caffe学习--Lenet5的应用和原理、实现----ubuntu16.04.2+caffe+mnist+train+test

    Lenet5的应用和原理.实现 ----------------------------------------------ubuntu16.04.2------------------------- ...

  9. 深度解析开发项目之 02 - 使用VTMagic实现左右滑动的列表页

    深度解析开发项目之 02 - 使用VTMagic实现左右滑动的列表页 实现效果: 01 - 导入头文件 02 - 遵守代理协议 03 - 声明控制器的属性 04 - 设置声明属性的frame 05 - ...

  10. 对canvas arc()中counterclockwise参数的一些误解

    一直没有很细心地去研究CanvasRenderingContext2D对象的arc方法,对它的认识比较模糊,导致犯了一些错误,特发此文,以纠正之前的错误理解. arc()方法定义如下: arc() 方 ...