24、【Swift】自动引用计数

  • Swift 使用自动引用计数(ARC)机制来追踪和管理你的 App 的内存
  • 当这些实例不在需要时,ARC会自动释放类实例所占用的内存。

引用计数只应用于类的实例。结构体和枚举是值类型,不是引用类型,没有通过引用存储和传递。

自动引用计数的工作机制

  • 创建一个类的实例,ARC 分配一大块内存来存储这个实例的信息
    • 类型信息
    • 存储属性值的信息
  • 当实例不需要时,ARC 会释放该实例所占用的内存
  • 如果 ARC 释放了正在使用的实例内存,那么它将不会访问实例的属性,或者调用实例的方法
    • 如果你试图访问该实例,你的app很可能会崩溃
  • ARC 会跟踪和计算当前实例被多少属性,常量和变量所引用。
    • 只要存在对该类实例的引用,ARC 将不会释放该实例。
    • 无论你将实例分配给属性,常量或变量,它们都会创建该实例的强引用
      • 称之为“强”引用,是因它将实例保持住,只要强引用在,实例是不允许被销毁的

自动引用计数实践

  • 场景:实例内存的分配和释放操作

  • 展示了自动引用计数的工作机制

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}
  • 由于可选类型的变量会被自动初始化为一个 nil 值,目前还不会引用到 Person 类的实例。
var reference1: Person?
var reference2: Person?
var reference3: Person?
reference1 = Person(name: "John Appleseed")
// prints "John Appleseed is being initialized"
  • 现在就有了一个从reference1 到该实例的强引用

  • 实例又会多出两个强引用:

reference2 = reference1
reference3 = reference1
  • 给其中两个变量赋值 nil 的方式断开两个强引用
reference1 = nil
reference2 = nil
reference3 = nil
// prints "John Appleseed is being deinitialized"

类实例之间的循环强引用

  • 场景:某个类永远不会变成零强引用 = 永远不会销毁

  • 循环引用:两个类实例,彼此持有对方的强引用

  • 循环引用解决:

    • 定义类之间的关系为弱引用( weak )或无主引用(unowned )来代替强引用
  • 循环引用例子:

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 }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}
  • apartment 属性是可选项,因为一个人并不总是拥有公寓
  • tenant 属性是可选的,因为一栋公寓并不总是有居民
  • 反初始化时输出信息,Person 和 Apartment 的实例是否像预期的那样被释放
  • 两个变量都被初始化为 nil ,这正是可选项的优点:
var john: Person?
var unit4A: Apartment?
  • 创建特定的 Person 和 Apartment 实例并将其赋值给 john 和unit4A 变量
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
  • John 变量对Person 实例有一个强引用, unit4A 变量对 Apartment 实例有一个强引用
image
  • 感叹号( ! )是用来展开和访问可选变量 john 和 unit4A 里的实例的
john!.apartment = unit4A
unit4A!.tenant = john
  • 两个实例联系在一起之后,强引用的关系如图
image
  • Person 实例现在有了一个指向Apartment 实例的强引用

  • Apartment 实例也有了一个指向 Person 实例的强引用

  • 断开 john 和 unit4A 变量持有的强引用,引用计数并不会降零,实例也不会被 ARC 释放

john = nil
unit4A = nil
  • 没有任何一个反初始化器被调用

  • 强引用关系如下图:


    image

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

  • 对于生命周期中会变为 nil 的实例使用弱引用
  • 对于初始化赋值后再也不会被赋值为 nil 的实例,使用无主引用
  • 上面的 Apartment 例子中,在它的声明周期中,有时是”没有居民”的/可选的,因此适合使用弱引用来解决循环强引用。

弱引用

  • 不会对引用实例强引用,不会阻止 ARC 释放
  • 语法:声明属性或者变量时,在前面加上 weak 关键字
  • 置 nil 操作:ARC 会在被弱引用的实例被释放,自动地设置弱引用为 nil (由于弱引用需要允许它们的值为 nil ,它们一定得是可选类型)
  • 检查弱引用的值是否存在,就像其他可选项的值一样,并且你将永远不会遇到“野指针”

在 ARC 给弱引用设置 nil 时不会调用属性观察者。

  • 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") }
}
var john: Person?
var unit4A: Apartment?
 
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
 
john!.apartment = unit4A
unit4A!.tenant = john
image
  • Person 实例对 Apartment 实例强引用, Apartment 实例对Person 实例是引用
  • 当你断开 john 变量所保持的强引用时,再也没有指向 Person 实例的强引用
  • 实例会被释放:
john = nil
// prints "John Appleseed is being deinitialized"
  • Person 实例对 Apartment 实例强引用, Apartment 实例对Person 实例是引用
  • 当你断开 john 变量所保持的强引用时,再也没有指向 Person 实例的强引用
  • 实例会被释放:
john = nil
// prints "John Appleseed is being deinitialized"
  • 没有强引用到 Person 它被释放掉了,并且 tenant 属性被设置为 nil
image
unit4A = nil
// prints "Apartment 4A is being deinitialized"
  • 没有指向 Apartment 实例的强引用,该实例也会被释放:
image

注意

在使用垃圾回收机制的系统中,由于没有强引用的对象会在内存有压力时触发垃圾回收而被释放,弱指针有时用来实现简单的缓存机制。总之,对于 ARC 来说,一旦最后的强引用被移除,值就会被释放,这样的话弱引用就不再适合这类用法了。

无主引用

  • 使用场景:非可选类型
  • 优点:不需要在使用它的时候将它展开
  • 缺点:ARC 无法在实例被释放后将无主引用设为 nil(因非可选类型变量不允许赋值为 nil)

如果你试图在实例的被释放后访问无主引用,那么你将触发运行时错误。

  • Customer 和 CreditCard ,模拟了银行客户和客户的信用卡

  • 一个客户可能有或者没有信用卡,但是一张信用卡总是关联着一个客户

    • 新 CreditCard 实例只有通过 number 值和 customer 实例到CreditCard 的初始化器才能创建。
    • 确保 CreditCard 实例创建时总有 customer 实例
class Customer {
    let name: String
    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 属性的存储量在32位和64位系统上都能足够容纳16位的卡号

var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
  • 关联了两个实例后的图示关系:
image
  • 断开 john 变量持有的强引用时,再也没有指向CreditCard 实例的强引用,该实例也随之被释放了
image
john = nil
// prints "John Appleseed is being deinitialized"
// prints "Card #1234567890123456 is being deinitialized"

无主可选引用

  • 场景:(非可选)无主引用不能为 nil,无主可选引用可为 nil (但不会自动置 nil)

  • 使用无主可选引用时,需保证引用合法对象或 nil

  • 追踪学校特定部门提供的课程

class Department {
    var name: String
    var courses: [Course]
    init(name: String) {
        self.name = name
        self.courses = []
    }
}
 
class Course {
    var name: String
    unowned var department: Department
    unowned var nextCourse: Course?
    init(name: String, in department: Department) {
        self.name = name
        self.department = department
        self.nextCourse = nil
    }
}
  • Course 有两个无主引用,一个是到部门,另一个是下一门学生要上的课程

  • 每一门课程都是某些部门的一部分,所以 department 不是可选的

  • 课程并不包含推荐的后续课程, nextCourse 是可选的

let department = Department(name: "Horticulture")// 创建了一个部门以及它的三个课程
 
let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)
 // 初级和中级课程都有一个建议的后续课程存放在它们的 nextCourse 属性中
intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]
  • nextCourse 维护了一个无主可选引用,指向了学生在完成本课程后应该继续的课程
image
  • unowned 不能自动置nil,所以还是需要保证 nextCourse 指向了一个没有被释放的课程
  • 从 department.courses 删除课程时,同样要移除其他课程对这个课程的引用

上述可选值的类型是 Optional ,也就是 Swift 标准库中的枚举。

总之,可选项是值类型不能被标记为unowned 这个规则中的例外。

包裹了类的可选项并不使用引用计数,所以你不需要对可选项维持强引用。

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

  • 循环引用-解决
    • 弱引用来解决:两个属性的值都允许为 nil
      • Person 和 Apartment
    • 无主引用解决:一个属性的值允许为 nil ,而另一个属性的值不允许为 nil
      • Customer 和 CreditCard
    • 一个类用无主属性,另一个类用隐式展开的可选属性:
      • 两个属性都必须有值,并且初始化完成后永远不会为 nil
  • 两个类, Country 和 City
    • 每个国家必须有首都,每个城市必须属于一个国家
class Country {
    let name: String
    var capitalCity: City! // 有一个默认值 nil,保证 Country 的实例完全初始化完后, Country 的初始化器才能把 self 传给 City 的初始化器
    init(name: String, capitalName: String) {
        self.name = name // 一旦 name 属性被赋值后, Country 的初始化器就能引用并传递隐式的 self
        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
    }
}
  • 以上的意义:通过一条语句同时创建 Country 和 City 的实例,而不产生循环强引用, capitalCity 的属性能被直接访问,二不需要可选绑定、强制展开
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// prints "Canada's capital city is called Ottawa"
  • 用隐式展开的可选属性的意义
    • 满足了两段式类初始化器的需求
    • capitalCity 属性初始化完成后,能像非可选项一样使用,同时还避免了循环强引用

闭包的循环强引用

  • 闭包循环引用:比属性循环引用,多了变量捕获

  • 场景:闭包赋值给实例属性,闭包又捕获这个实例

  • 闭包捕获实例场景:

    • 闭包函数体访问了实例某个属性,比如self.someProperty
    • 闭包调用了一个实例的方法,例如self.someMethod()
  • 闭包循环引用本质:闭包和类,都是引用类型

  • 用一种简单的模型表示 HTML 中的一个单独的元素:

class HTMLElement {
    
    let name: String // 元素的名称,如表示标题元素的 "h1" 、代表段落元素的 "p" 、或者代表换行元素的 "br" 
    let text: String?// 可选的属性 text ,它可以用来设置和展现 HTML  元素的文本
    
  // 这个属性引用了一个将 name 和 text 组合成 HTML  字符串片段的闭包
    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 {
        print("\(name) is being deinitialized")
    }
    
}
  • 由于 asHTML 是闭包,用自定义的闭包来取代默认值
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// prints "<h1>some default text</h1>"

asHTML 声明为 lazy 属性,因为只有当元素确实需要处理为 HTML 输出的字符串时,才需要使用asHTML 。

实际上 asHTML 是延迟加载属性,意味在默认闭包可用 self ,因只有当初始化完成以及 self 确实存在后,才能访问延迟加载属性。

  • 用 HTMLElement 类创建实例并打印消息
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// prints"hello, world"
  • HTMLElement 类产生了和 asHTML 默认值的闭包之间的循环强引用
image
  • asHTML 属性持有闭包的强引用
  • 闭包在其闭包体内使用了 self (引用了 self.name 和 self.text ),因此闭包捕获了 self ,这意味着闭包又反过来持有了 HTMLElement 实例的强引用

尽管闭包多次引用了 self ,它只捕获 HTMLElement 实例的一个强引用。

paragraph = nil
// 实例和它的闭包都不会被释放,也是因为循环强引用
  • HTMLElement 的反初始化器中的消息并没有被打印,证明了 HTMLElement 实例并没有被销毁

解决闭包的循环强引用

  • 场景:通过弱/无主引用,标记闭包捕获列表的变量,解决闭包和实例的循环引用

建议显式使用 self.someProperty 或者 self.someMethod (而不只是someProperty 或 someMethod ),有助于提醒捕获了 self ,避免循环引用

定义捕获列表( closuer capture list )

语法:

  • weak / unowned + 类实例的引用(如 self ) / 初始化过的变量(如 delegate = self.delegate! )

  • 中括号包裹,逗号隔开

  • 放形式参数和返回值的前边

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 ,应该用无主引用而不是弱引用。
  • 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 {
        print("\(name) is being deinitialized")
    }
    
}
  • 创建并打印 HTMLElement 实例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// prints "<p>hello, world</p>"
  • 使用捕获列表后引用关系
image
  • 闭包以无主引用的形式捕获 self ,并不会持有 HTMLElement 实例的强引用
paragraph = nil
// prints "p is being deinitialized"
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,812评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,626评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,144评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,052评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,925评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,035评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,461评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,150评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,413评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,501评论 2 307
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,277评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,159评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,528评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,868评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,143评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,407评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,615评论 2 335

推荐阅读更多精彩内容