【官方文档翻译】Swift4.0.3 ☞ Automatic Reference Counting

原文链接

自动引用计数(Automatic Reference Counting)

Swift 使用 自动引用计数器(ARC)去追踪和管理你应用内存的使用。大多数情况下,这意味着在Swift中内存管理“自动生效”,你不需要自己管理内存。ARC会在类对象不再使用的时候自动释放内存。

然而,在少数情况下 ARC在为你管理内存时 需要你提供代码间的关系等更多的信息。这一章阐述了这些情况并且向你展示了如何使用ARC管理你的应用所有内存。在Swift中使用ARC非常接近OC中的过渡到ARC

ARC如何工作(How ARC Works)

每当你创建一个类的新的对象,ARC分配一块内存空间去存储这个对象的信息。这块内存持有实例类型的信息,以及与该实例相关联的任何存储属性的值。

此外,当一个对象不再需要时,ARC会释放掉对象占用的内存用于其他用途。这就确保了当类实例不再使用时不会占据内存空间。

然而,如果如果ARC销毁了一个仍然有用的实例,就不能再访问该实例的属性以及调用该实例的方法。如果你尝试访问它,会使你的应用崩溃。

为了确保实例在它们仍会被使用的情况下不会被释放,ARC追踪当前每个实例被多少属性、常量、变量被引用。实例只要有一个有效的引用,ARC就不会释放这个实例ARC tracks how many properties, constants, and variables are currently referring to each class instance)。

使这成为可能,每当你把类实例赋给属性、常量、或者变量时,需要强引用这个实例。之所以称为强引用是它真实持有了实例,并且只要这个强引用还在,就不允许实例被释放。

ARC 示例(ARC in Action)

这里有一个ARC如何工作的例子。在这个例子中,使用一个简单的Person类,这个类定义了一个常量name属性。

  class Person {
     let name: String
     init(name: String) {
         self.name = name
         print("\(name) is being initialized")
     }
     deinit {
         print("\(name) is being deinitialized")
     }
 }

Person类有一个构造方法,设置了这个实例的name属性并且打印了一条信息指示出初始化在进行。这个Person类还有一个析构方法,当实例销毁的时候打印了一条信息。

下面这个代码块定义了三个Person?类型的变量,使用它们在接下来的代码中对一个新的Person实例实现多重引用。因为这些变量时可选类型(Person?,不是Person),它们被自动初始化为nil,并且当前没有引用Person实例

var reference1:Person?
var reference2:Person?
var reference3:Person?

你现在可以创建一个Person实例并且把它赋给这三个变量中的一个。

reference1 = Person(name:"John Appleseed")

注意当调用Person的构造方法时打印了"John Appleseed is being initialized",说明初始化已经完成。

因为创建的Person实例赋值给了reference1变量,所以现在reference1强引用着Person实例。因为有一个强引用,ARC确保这个实例在内存中,不会被销毁。

如果你把Person实例赋值给更多的变量,就会建立更多的强应用:

reference2 = reference1
reference3 = reference1

现在有三个强引用引用着这个实例。

如果你打断其中的两个强引用(包括最先强引用)通过赋值nil给两个变量,仍然还有一个强引用,此时,这个Person实例不会被销毁。

reference1 = nil
reference2 = nil

ARC在最后一个强引用打断之前,不会释放Person实例,如果最后一个强引用被打断,说明你不再使用Person实例。

reference3 = nil
// Prints "John Appleseed is being deinitialized"

两个类实例之间的循环引用 (Strong Reference Cycles Between Class Instances)

在上面的例子中,ARC追踪你创建的Person实例的引用数量,并且当Person实例不在被引用时销毁掉。

然而,在编码的时候,有可能遇到一个类的实例永远不会有强引用为0的情况。这种情况可能发生在两个类实例彼此强引用对方,导致了互相持有。这被称作循环引用

你可以通过定义两个类的一些关系例如weak或者unowned引用代替强引用。这个过程描述在解决两个类实例间的循环引用。然而,在你学习如何解决循环引用之前,理解循环引用是如何引起的是很有用的。

这里有一个怎样偶然的引起循环引用的例子。这个例子定义了一个Person类和Apartment类

class Person {
   let name: String
   init(name: String) { self.name = name }
   var apartment: Apartment ?
   deinit { print("\(name) is being deinitialzed") }
}
class Apartment {
   let unit: String
   init(unit: String) { self.unit = unit }
   var tenant: Person?
   deinit { print("Apartment \(unit) is being deinitialized") }
}

每个Person实例有一个String类型的name属性和一个可选的被初始化为nil的apartment属性。这个apartment属性是可选的,因为一个人可能不会一直拥有一个公寓。

类似的,每个Apartment实例有一个Stirng类型的unit属性和一个可选的被初始化为nil的tenant属性。这个tenant属性是可选的,因为一个公寓不一定总有租客。

两个类都定义了析构函数,打印了类的实例被销毁的信息。这可以确保你看到是否Person和Apartment像你期望的那样被销毁。

下面这个代码块定义了两个可选类型的变量分别叫做john和unit4A,下面设置了具体的Apartment和Person实例。两个变量都被初始化为nil。

var john: Person?
var unit4A: Apartment?

现在可以创建具体的Person实例和Apartment实例并且赋给john和unit4A变量:

john = Person(name: "John Appleaseed")
unit4A = Apartment(unit: "4A")

这里展示了强引用是如何创建和赋值这两个实例的。john变量现在强引用着Person实例,unit4A变量强引用着Apartment实例。

你现在可以把这两个实例联系在一起,让人拥有一套公寓,让公寓有一个租客。注意到感叹号被用于解绑并且获取存储在john和unit4A中的可选变量,便于给他们的属性设置值。

这里展示了如何将两个实例联系到一起。


遗憾的是,连接这两个实例制造了它们之间的循环引用。Person实例现在强引用着Apartment实例,并且Apartment实例强引用着Person实例,因此,当你打断了john和unit4A持有的强引用,引用计数没有归零,这些实例不会被ARC销毁。

john = nil
unit4A = nil

注意到当你设置这两个变量的值为nil时,两个类的实例都没有调用析构函数。循环引用阻止了Person实例和Apartment实例被销毁,在应用里引起了内存泄露。

这里展示了当你给这些变量设置nil时的强引用关系。

在Person实例和Apartment实例之间仍然保持着强引用,没有办法打断。

解决类实例间的循环强引用(Resolving Strong Reference Cycles Between Class Instances)

Swift提供两种方式去解决循环强引用,通过作用在类的属性上的关键字:weakunowned

Weak 和 unowned引用能够使一个实例在循环引用中用不是强引用的方式引用另一个实例。之后实例互相引用时就不会造成循环强引用。

当引用的另一个实例有更短的生命周期时使用weak引用--就是,另一个实例会先被销毁。在上面的Apartment例子里,在公寓的生命周期里,会存在一个时间点,没有租客。所以在这种情况下使用weak引用是一种合适的做法。相反的,使用unowned引用,当引用的实例的生命周期大于等于自己的生命周期

Weak 引用 (Weak References)
Weak引用不会对它持有的实例强引用,也不会阻止ARC释放它引用的实例。这种行为阻止了成为循环强引用。你通过在一个属性或者变量声明之前放置weak关键字来表示这是一个若引用

因为弱引用没有对他引用的实例strong hold,所以它引用的实例可以被销毁即使有弱引用的存在。因此,ARC自动设置一个弱引用为nil 当这个实例弱引用的实例被销毁的时候。同时,因为弱引用的实例需要在运行时被ARC改为nil,他们总是被定义为可选类型的变量,而不是常量

你可以检查弱引用是否存在一个值,像其他的可选值一样,你将永远不会引用一个不存在的无效实例。

注意
当ARC设置弱引用为nil时,属性的观察者不会被调用(Property observers aren’t called when ARC sets a weak reference to nil.)(我理解的是不会走KVC的方法)

下面这个例子和上面的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: Stirng) { 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实例现在弱引用Perosn实例。这就意味着当你通过设置john变量的值为nil来打断john的强引用时,就没有其他的强引用来引用Person实例了。

john = nil
// Prints "John Appleseed is being deinitialized"

因为没有更多的强引用去引用Person实例,所以Person实例被销毁,同时将tenant属性被赋值为nil:

Apartment实例唯一剩下的强引用来源于unit4A变量,如果你打断这个强引用,就没有别的强引用去引用这个实例了

unit4A = nil
// Prints "Apartment 4A is being deinitialized"

因为没有更多的强引用引用Apartment实例,它也被销毁了。

注意
在使用垃圾回收的系统中,weak指针有时被用于实现简单的缓存机制,因为没有被强引用的对象被销毁只有当内存达到峰值时才会触发垃圾回收。然而,在ARC中,当最后一个强引用被移除时会立即被销毁,所以使用weak不适合做这件事。

Unowned引用 (Unowned References)

像weak引用一样,unowned引用不会对它引用的实例strong hold。与weak引用不一样的,unowned引用被用于引用的实例的生命周期大于等于自身你通过在一个属性声明前放置关键字unowned来表示unowned引用。

一个unowned引用被认为一直有一个值,因此,ARC不会设置unowned引用的值为nil,这意味着unowned引用被定义为非可选类型。

关键
使用一个unowned引用,仅仅当你确定你引用的实例不会被释放。
如果你尝试访问一个被销毁的unowned引用的实例,你会触发运行时错误。

下面这个例子定义了两个类,Customer和CreditCard,模拟了银行的顾客和这个顾客可能拥有的引用卡。这两个类实例都存储了另一个类实例作为属性。这种关系有潜在的可能制造循环强引用。

Customer和CreditCard之间的关系与Apartment和Person之间的关系有轻微的不同。在这个情形中,一个顾客可能拥有或者不拥有一张信用卡,但是一张信用卡会一直关联一个顾客。一个信用卡实例不会脱离于它引用的顾客实例单独存活。表达这个关系,Customer类有一个可选的card属性,但是creditCard类有一个unowned(非可选)customer属性。

此外,一个新的CreditCard实例只能通过一个number和一个customer实例去构造,这一点也确保了当Creditcard实例创建时,一直拥有一个与它关联的customer实例。

因为一张信用卡会一直有一个顾客,所以定义他的customer属性使用unowned引用来避免循环强引用。

class Customer {
     let name: Stirng
     var card: CreditCard?
     init(name: String) { self.name = name }
     deinit { print("\(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 { print("Card #\(number) is being deinitialized") }
}

注意
CreditCard类的属性number被定义为UInt64而不是Int,确保number属性的容量足够大去存储一个16位卡的数字,在32位或者64位系统中。

下面的代码块定义了一个可选的Customer变量john,将被用来存储一个具体的customer实例。这个变量有一个nil初始值。

var john: Customer?

你现在可以创建一个Customer实例,并且使用它去初始化并且赋给一个新的CreditCard实例作为这个Customer实例的card属性:

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

下面展示了这两个实例的引用关系:

Customer实例现在有一个强引用引用着CreditCard实例,CreditCard实例有一个unowned引用,引用着Customer实例。

因为使用了unowned customer 引用,当你打断john的强引用,就没有额外的强引用引用着Customer实例了

因为没有更多的强引用引用着Customer实例,它被销毁了,随着它被销毁,也没有更多的强引用引用着CreditCard实例,所以它也会被销毁。

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

上面的代码块展示了当john变量被设置为nil时,Customer实例和CreditCard实例都调用了对应的析构函数。都被销毁了。

注意
上面的例子展示了如何安全的使用unowned引用,Swift也提供了不安全的unowned引用为了你需要忽视runtime安全检查的情况--例如由于性能的原因。像所有不安全的操作一样,你需要自己保证代码的安全
你通过写unowned(unsafe)来指明这是一个不安全的unowned引用。如果你尝试访问一个已经被释放的实例,你的项目将会尝试去访问一块曾经使用过的内存空间,这可能是一个不安全的操作。

Unowned引用 和 隐式释放可选属性(Unowned References and Implicitly Unwrapped Optional Properties)

上面的例子覆盖了很常见的两种情形去避免循环强引用。

Person和Apartment例子展示了两个属性都允许被赋值为nil的情形,这种情景最好使用weak去解决循环强引用问题。

Customer和CreditCard例子展示了其中一个属性可以被赋值为nil,另一个属性不能被赋值为nil的情形,这种场景最好使用unowned引用解决循环强引用问题。

然而,还有第三种情况,两种属性都一直拥有值,一旦它们被创建都不会被赋值为nil。在这个场景中,要在一个类的属性中使用unowned同时在另一个类中使用 implicitly unwrapped optional。

这样确保了两个属性一旦被创建就可以被直接访问(不用解绑),这样做仍然避免了循环引用问题。这一节向你展示了如何建立这样的关系。

下面的例子定义了两个类,Country 和 City,每个实例存储了另一个类实例作为自己的一个属性。在这个数据模型中,每个国家必须一直有一个首都城市,并且每个城市必须一直有一个所属的国家。为了表示这个关系,Counrty类有一个capitalCity属性,City类有一个country
属性:

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

未完待续。。。

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

推荐阅读更多精彩内容