Swift使用自动引用计数(ARC)来跟踪并管理应用使用的内存。大部分情况下,这意味着在Swift语言中,内存管理"仍然工作",不需要自己去考虑内存管理的事情。当实例不再被使用时,ARC会自动释放这些类的实例所占用的内存。然而,在少数情况下,为了自动的管理内存空间,ARC需要了解关于你的代码片段之间关系的更多信息。本章描述了这些情况,并向大家展示如何打开ARC来管理应用的所有内存空间。

注意:引用计数只应用在类的实例。结构体(Structure)和枚举类型是值类型,并非引用类型,不是以引用的方式来存储和传递的。
 
  How ARC Works
  每次创建一个类的实例,ARC就会分配一个内存块,用来存储这个实例的相关信息。这个内存块保存着实例的类型,以及这个实例相关的属性的值。当实例不再被使用时,ARC释放这个实例使用的内存,使这块内存可作它用。这保证了类实例不再被使用时,它们不会占用内存空间。但是,如果ARC释放了仍在使用的实例,那么你就不能再访问这个实例的属性或者调用它的方法。如果你仍然试图访问这个实例,应用极有可能会崩溃。为了保证不会发生上述的情况,ARC跟踪与类的实例相关的属性、常量以及变量的数量。只要有一个有效的引用,ARC都不会释放这个实例。
 
  为了让这变成现实,只要你将一个类的实例赋值给一个属性或者常量或者变量,这个属性、常量或者变量就是这个实例的强引用(strong reference)。之所以称之为“强”引用,是因为它强持有这个实例,并且只要这个强引用还存在,就不能销毁实例。
 
下面的例子展示了ARC是如何工作的。本例定义了一个简单的类,类名是Person,并定义了一个名为name的常量属性
class Person {
let name: String
init(name: String) {
self.name = name
println("\(name) is being initialized")
} deinit {
println("\(name) is being deinitialized")
}
}
接下来的代码片段定义了三个Person?类型的变量,这些变量用来创建多个引用,这些引用都引用紧跟着的代码所创建的Person对象。因为这些变量都是可选类型(Person?,而非Person),因此他们都被自动初始化为nil,并且当前并没有引用一个Person的实例。
var reference1: Person?
var reference2: Person?
var reference3: Person?

现在我们创建一个新的Person实例,并且将它赋值给上述三个变量中的一个:

reference1 = Person(name: "John Appleseed")
// prints "Jonh Appleseed is being initialized"
因为Person的实例赋值给了变量reference1,所以reference1是Person实例的强引用。又因为至少有这一个强引用,ARC就保证这个实例会保存在内存重而不会被销毁。
 
如果将这个Person实例赋值给另外的两个变量,那么将建立另外两个指向这个实例的强引用:
reference2 = reference1
reference3 = reference2
现在,这一个Person实例有三个强引用。
 
如果你通过赋值nil给两个变量来破坏其中的两个强引用(包括原始的引用),只剩下一个强引用,这个Person实例也不会被销毁:
reference1 = nil
reference2 = nil

直到第三个也是最后一个强引用被破坏,ARC才会销毁Person的实例,这时,有一点非常明确,你无法继续使用Person实例:

referenece3 = nil
// 打印 “John Appleseed is being deinitialized”
  类实例之间的强引用循环 
 
 在两个类实例彼此保持对方的强引用,使得每个实例都使对方保持有效时会发生这种情况。我们称之为强引用循环。
 下面的例子展示了一个强引用环是如何在不经意之间产生的。例子定义了两个类,分别叫Person和Apartment,这两个类建模了一座公寓以及它的居民:
 

class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { println("\(name) is being deinitialized") }
} class Apartment {
let unit: Int
init(unit: Int) { self.unit= unit }
var tenant: Person?
deinit { println("Apartment #\(number) is being deinitialized") }
}
每个Person实例拥有一个String类型的name属性以及一个被初始化为nil的apartment可选属性。apartment属性是可选的,因为一个人并不一定拥有一座公寓。
 
类似的,每个Apartment实例拥有一个Int类型的number属性以及一个初始化为nil的tenant可选属性。tenant属性是可选的,因为一个公寓并不一定有居民。
 
这两个类也都定义了初始化函数,打印消息表明这个类的实例正在被初始化。这使你能够看到Person和Apartment的实例是否像预期的那样被销毁了。
 
下面的代码片段定义了两个可选类型变量,john和number73,分别被赋值为特定的Apartment和Person的实例。得益于可选类型的优点,这两个变量初始值均为nil:

var john: Person?
var unit4A: Apartment?

现在,你可以创建特定的Person实例以及Apartment实例,并赋值给john和number73:

jhon = Person(name: "John Appleseed")
unit4A = Apartments(number: 4A)

下面的图表明了在创建以及赋值这两个实例后强引用的关系。john拥有一个Person实例的强引用,unit4A拥有一个Apartment实例的强引用:

现在你可以将两个实例关联起来,一个人拥有一所公寓,一个公寓也拥有一个房客。注意:用感叹号(!)来展开并访问可选类型的变量,只有这样这些变量才能被赋值:

john!.apartment = unit4A
unit4A!.tenant = john

实例关联起来后,强引用关系如下图所示

关联这俩实例生成了一个强循环引用,Person实例和Apartment实例各持有一个对方的强引用。因此,即使你破坏john和number73所持有的强引用,引用计数也不会变为0,因此ARC不会销毁这两个实例

john = nil
unit4A = nil

当上面两个变量赋值为nil时,没有调用任何一个析构方法。强引用阻止了Person和Apartment实例的销毁,进一步导致内存泄漏。

  避免强引用循环

  Swift提供两种方法避免强引用循环:弱引用和非持有引用。

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

  弱引用

  弱引用不会增加实例的引用计数,因此不会阻止ARC销毁被引用的实例,声明属性或者变量的时候,关键字weak表明引用为弱引用。弱引用只能声明为变量类型,因为运行时它的值可能改变。弱引用绝对不能声明为常量

  因为弱引用可以没有值,所以声明弱引用的时候必须是可选类型的。在Swift语言中,推荐用可选类型来作为可能没有值的引用的类型。

下面的例子和之前的Person和Apartment例子相似,除了一个重要的区别。这一次,我们声明Apartment的tenant属性为弱引用:

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?
deinit { print("Apartment \(unit) is being deinitialized") }
}

然后创建两个变量(john和unit4A)的强引用,并关联这两个实例:

var john: Person?
var unit4A: Apartment? john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A") john!.apartment = unit4A
unit4A!.tenant = john

下面是引用的关系图:

Person的实例仍然是Apartment实例的强引用,但是Apartment实例则是Person实例的弱引用。这意味着当破坏john变量所持有的强引用后,不再存在任何Person实例的强引用:

既然不存在Person实例的强引用,那么该实例就会被销毁:

   非持有引用

和弱引用相似,非持有引用也不强持有实例。但是和弱引用不同的是,非持有引用默认始终有值。因此,非持有引用只能定义为非可选类型(non-optional type)。在属性、变量前添加unowned关键字,可以声明一个非持有引用。
 
因为是非可选类型,因此当使用非持有引用的时候,不需要展开,可以直接访问。不过非可选类型变量不能赋值为nil,因此当实例被销毁的时候,ARC无法将引用赋值为nil。
 

class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
} deinit { println("\(name) is being deinitialized")
} class CreditCard {
let number: Int
unowned let customer: Customer
init(number: Int, customer: Customer) {
self.number = number
self.customer = customer
} deinit { println("Card #\(number) is being deinitialized")
}
下面的代码定义了一个叫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!)

此时的引用关系如下图所示

因为john对CreditCard实例是非持有引用,当破坏john变量持有的强引用时,就没有Customer实例的强引用了

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

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

  非持有引用以及隐式展开的可选属性

Person和Apartment的例子说明了下面的场景:两个属性的值都可能是nil,并有可能产生强引用环。这种场景下适合使用弱引用。
 
Customer和CreditCard的例子则说明了另外的场景:一个属性可以是nil,另外一个属性不允许是nil,并有可能产生强引用环。这种场景下适合使用无主引用。
 
但是,存在第三种场景:两个属性都必须有值,且初始化完成后不能为nil。这种场景下,则要一个类用无主引用属性,另一个类用隐式展开的可选属性。这样,在初始化完成后我们可以立即访问这两个变量(而不需要可选展开)

下面的例子定义了两个类,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
}
}
City的初始化函数有一个Country实例参数,并且用country属性来存储这个实例。这样就实现了上面说的关系。
 
Country的初始化函数调用了City的初始化函数。但是,只有Country的实例完全初始化完后(在Two-Phase Initialization),Country的初始化函数才能把self传给City的初始化函数。
 
为满足这种需求,通过在类型结尾处加感叹号(City!),我们声明Country的capitalCity属性为隐式展开的可选类型属性。就是说,capitalCity属性的默认值是nil,不需要展开它的值(在Implicity Unwrapped Optionals中描述)就可以直接访问。
 
因为capitalCity默认值是nil,一旦Country的实例在初始化时给name属性赋值后,整个初始化过程就完成了。这代表只要赋值name属性后,Country的初始化函数就能引用并传递隐式的self。所以,当Country的初始化函数在赋值capitalCity时,它也可以将self作为参数传递给City的初始化函数。
 
综上所述,你可以在一条语句中同时创建Country和City的实例,却不会产生强引用环,并且不需要使用感叹号来展开它的可选值就可以直接访问capitalCity:
var country = Country(name: "Canada", capitalName: "Ottawa")
println("\(country.name)'s captial city is called \(country.capitalCity.name)")
// 打印"Canada's capital city is called Ottawa"

在上面的例子中,使用隐式展开的可选值满足了两个类的初始化函数的要求。初始化完成后,capitalCity属性就可以做为非可选值类型使用,却不会产生强引用环。

  闭包的强引用循环

将一个闭包赋值给类实例的某个属性,并且这个闭包使用了实例,这样也会产生强引用环。这个闭包可能访问了实例的某个属性,例如self.someProperty,或者调用了实例的某个方法,例如self.someMethod。这两种情况都导致了闭包使用self,从而产生了抢引用环。
 
因为诸如类这样的闭包是引用类型,导致了强引用环。当你把一个闭包赋值给某个属性时,你也把一个引用赋值给了这个闭包。实质上,这个之前描述的问题是一样的-两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个是类实例,另一个是闭包。
 
Swift提供了一种优雅的方法来解决这个问题,我们称之为闭包捕获列表(closuer capture list)。
 
下面的例子将会告诉你当一个闭包引用了self后是如何产生一个强引用循环的。
class HTMLElement { 

    let name: String
let text: String?   lazy var asHTML: () -> String = {
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")
} }
HTMLElement定义了一个name属性来表示这个元素的名称,例如代表段落的"p",或者代表换行的"br";以及一个可选属性text,用来设置HTML元素的文本。
 
除了上面的两个属性,HTMLElement还定义了一个lazy属性asHTML。这个属性引用了一个闭包,将name和text组合成HTML字符串片段。该属性是() -> String类型,就是“没有参数,返回String的函数”。
 
默认将闭包赋值给了asHTML属性,这个闭包返回一个代表HTML标签的字符串。如果text值存在,该标签就包含可选值text;或者不包含文本。对于段落,根据text是"some text"还是nil,闭包会返回"<p>some text</p>"或者"<p />"。
 
可以像实例方法那样去命名、使用asHTML。然而,因为asHTML终究是闭包而不是实例方法,如果你像改变特定元素的HTML处理的话,可以用定制的闭包来取代默认值。

闭包使用了self(引用了self.name和self.text),因此闭包占有了self,这意味着闭包又反过来持有了HTMLElement实例的强引用。这样就产生了强引用环

  避免闭包产生的强引用循环

在定义闭包时同时定义捕获列表作为闭包的一部分,可以解决闭包和类实例之间的强引用环。捕获列表定义了闭包内占有一个或者多个引用类型的规则。和解决两个类实例间的强引用环一样,声明每个占有的引用为弱引用或非持有引用,而不是强引用。根据代码关系来决定使用弱引用还是非持有引用。
 
注意:Swift有如下约束:只要在闭包内使用self的成员,就要用self.someProperty或者self.someMethod(而非只是someProperty或someMethod)。这可以提醒你可能会不小心就占有了self。

  定义捕获列表

捕获列表中的每个元素都是由weak或者unowned关键字和实例的引用(如self或someInstance)组成。每一对都在花括号中,通过逗号分开。
 
捕获列表放置在闭包参数列表和返回类型之前:
lazy var someClosure: (Int, String) -> String = {
[unowned self] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}

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

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

前面提到的HTMLElement例子中,非持有引用是正确的解决强引用的方法。这样编码HTMLElement类来避免强引用环:

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")
} }
上面的HTMLElement实现和之前的实现相同,只是多了占有列表。这里,占有列表是[unowned self],代表“用无主引用而不是强引用来占有self”。
 
和之前一样,我们可以创建并打印HTMLElement实例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
println(paragraph!.asTHML())
// 打印"<p>hello, world</p>"

引用关系如下图

这一次,闭包以无主引用的形式占有self,并不会持有HTMLElement实例的强引用。如果赋值paragraph为nil,HTMLElement实例将会被销毁,并能看到它的deinitializer打印的消息。  

paragraph = nil
// 打印"p is being deinitialized"
 
 

【Swift学习】Swift编程之旅---ARC(二十)的更多相关文章

  1. JAVA之旅(二十九)——文件递归,File结束练习,Properties,Properties存取配置文件,load,Properties的小练习

    JAVA之旅(二十九)--文件递归,File结束练习,Properties,Properties存取配置文件,load,Properties的小练习 我们继续学习File 一.文件递归 我们可以来实现 ...

  2. JAVA之旅(二十六)——装饰设计模式,继承和装饰的区别,LineNumberReader,自定义LineNumberReader,字节流读取操作,I/O复制图片

    JAVA之旅(二十六)--装饰设计模式,继承和装饰的区别,LineNumberReader,自定义LineNumberReader,字节流读取操作,I/O复制图片 一.装饰设计模式 其实我们自定义re ...

  3. JAVA之旅(二十二)——Map概述,子类对象特点,共性方法,keySet,entrySet,Map小练习

    JAVA之旅(二十二)--Map概述,子类对象特点,共性方法,keySet,entrySet,Map小练习 继续坚持下去吧,各位骚年们! 事实上,我们的数据结构,只剩下这个Map的知识点了,平时开发中 ...

  4. JAVA之旅(二十八)——File概述,创建,删除,判断文件存在,创建文件夹,判断是否为文件/文件夹,获取信息,文件列表,文件过滤

    JAVA之旅(二十八)--File概述,创建,删除,判断文件存在,创建文件夹,判断是否为文件/文件夹,获取信息,文件列表,文件过滤 我们可以继续了,今天说下File 一.File概述 文件的操作是非常 ...

  5. JAVA之旅(二十五)——文件复制,字符流的缓冲区,BufferedWriter,BufferedReader,通过缓冲区复制文件,readLine工作原理,自定义readLine

    JAVA之旅(二十五)--文件复制,字符流的缓冲区,BufferedWriter,BufferedReader,通过缓冲区复制文件,readLine工作原理,自定义readLine 我们继续IO上个篇 ...

  6. JAVA之旅(二十四)——I/O流,字符流,FileWriter,IOException,文件续写,FileReader,小练习

    JAVA之旅(二十四)--I/O流,字符流,FileWriter,IOException,文件续写,FileReader,小练习 JAVA之旅林林总总也是写了二十多篇了,我们今天终于是接触到了I/O了 ...

  7. JAVA之旅(二十)—HashSet,自定义存储对象,TreeSet,二叉树,实现Comparator方式排序,TreeSet小练习

    JAVA之旅(二十)-HashSet,自定义存储对象,TreeSet,二叉树,实现Comparator方式排序,TreeSet小练习 我们继续说一下集合框架 Set:元素是无序(存入和取出的顺序不一定 ...

  8. Swift学习——Swift基础具体解释(一)

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/zhenyu5211314/article/details/34807025 注:由于基础部分在Swi ...

  9. 【Swift学习】Swift编程之旅---枚举(十二)

    枚举为一组相关的值定义一个共同的类型,并允许您在代码中的以类型安全的方式中使用这些值,在 Swift 中,枚举类型是一等(first-class)类型.它们采用了很多传统上只被类所支持的特征,例如计算 ...

随机推荐

  1. 微信公共平台开发-(.net实现)1--成为开发者

    刚换了个新环境,哎这都快一个月了,还没适应过来,还是怀念老地方呀.老板让开发一个基于微信平台的开发项目,而且是用net实现.当时就蒙了,微信就用了一会个人赶脚不好,所以果断不用,现在让开发,而且查了一 ...

  2. css position全解析

    1.position:static 所有的元素的默认定位都是position:static,这意味着元素没有被定位,而且在文档中出现在它应该在的位置. 2.position:relative 如果设定 ...

  3. 如何在遍历中使用 iterator/reverse_iterator 删除元素

    如何在遍历中使用 iterator/reverse_iterator 删除元素 罗朝辉 (http://www.cnblogs.com/kesalin/) 本文遵循“署名-非商业用途-保持一致”创作公 ...

  4. 自定义 Azure Table storage 查询过滤条件

    本文是在Azure Table storage 基本用法一文的基础上,介绍如何自定义 Azure Table storage 的查询过滤条件.如果您还不太清楚 Azure Table storage ...

  5. 使用aggregate在MongoDB中查找重复的数据记录

    我们知道,MongoDB属于文档型数据库,其存储的文档类型都是JSON对象.正是由于这一特性,我们在Node.js中会经常使用MongoDB进行数据的存取.但由于Node.js是异步执行的,这就导致我 ...

  6. web系统架构设计中需要知道的点(前端篇)

    上周没写东西,这周写点互联网系统开发中需要了解的技术点,每个点都可以发散出去,连接更多的知识点,打算做个逐步细化的记录. 一个应用的整个生命周期中(生,老,病,死)都需要有一个整体规划. 前期 评估需 ...

  7. ui-router带参数的ui-sref配置

    ui-router带参数的ui-sref配置 路由 .state('app.user_edit', { url:'user/userid/:userid', templateUrl: 'compone ...

  8. TypeScript实例

    interface Person { firstName: string, lastName: string } function greeter(person: Person) { return p ...

  9. andriod adt和andriod sdk

    今天搭建appium的环境,没有太明白andriod adt和andriod sdk分别是什么东西,经过与开发沟通,大致了解如下,这里记录一下,免得过几天就搞忘了. andriod adt是一个插件, ...

  10. 关于js中值的比较规则问题

    上一篇文章提到了javascript中可变值与不可变值,如果你不知道什么是可变值和不可变值,可以先去看看那篇文章,再回来看这篇,因为这篇文章是基于可变值与不可变值讲解的. 那我就默认你知道什么是可变值 ...