IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Swift基础入门知识学习(19)-自动引用计数(自动参考计数ARC)-讲给你懂 -> 正文阅读

[移动开发]Swift基础入门知识学习(19)-自动引用计数(自动参考计数ARC)-讲给你懂

TED演讲的8个秘诀:学习18分钟高效表达-重点笔记

Swift基础入门知识学习(18)-构造过程(建构过程及解构过程)-讲给你懂


理解难度
★★★★★
实用程度
★☆☆☆☆

Swift 使用自动引用计数(ARC, Automatic Reference Counting)机制来追踪与管理记忆体使用状况,所以大部分情况下,你不需要自己管理,Swift 会自动释放掉不需要的记忆体。

引用计数只应用在类(也就是参考类型)的实例。结构体与枚举为值类型,也不是通过引用的方式储存与传递。

自动引用计数的运作方式

当一个类实例被指派值(给一个属性、常量或变量)的时候,会建立一个该实例的强引用(strong reference),同时会将引用计数(reference counting)加 1 ,强引用表示会将这个实例保留住,只要强引用还在(也就是引用计数不为 0 ),储存这个实例的记忆体就不会被释放掉。

下面介绍一下 ARC 运作的方式:

// 定义一个类 SomeOne
class SomeOne {
    let name: String
    init (name: String) {
        self.name = name
    }
}

// 先宣告三个可选 SomeOne 的变数 会被自动初始化为 nil
// 这三个变数目前都尚未有实例的引用
var reference1: SomeOne?
var reference2: SomeOne?
var reference3: SomeOne?

// 先生成一个实例 并指派给其中一个变数 reference1
reference1 = SomeOne(name: "Jess")

// 目前这个实例就有了一个强引用 引用计数为 1
// 所以 ARC 会保留住这个实例使用的记忆体

// 接着再指派给另外两个变数
reference2 = reference1
reference3 = reference1

// 这时这个实例多了 2 个强引用 总共为 3 个强引用
// 也就是目前的引用计数为 3

// 接着将其中两个变数指派为 nil 断开他们的强引用
reference1 = nil
reference2 = nil

// 目前还有 1 个强引用 引用计数为 1
// 所以 ARC 仍然会保留住记忆体

// 最后将第三个变数也指派为 nil 断开强引用
reference3 = nil

// 这时这个实例已经没有强引用了 引用计数为 0
// ARC 就会将记忆体释放掉

类实例间的强引用循环

ARC 在大部分时间都可以运作顺利,但在有些情况下会造成强引用永远不会归零,进而发生记忆体泄漏(memory leak)的问题。

下面是一个例子,两个类彼此都拥有对方强引用的属性,一个实例要释放记忆体前,必须先释放对方强引用,而对方要释放前也是要原本实例先释放,进而产生强引用循环:

// 定义一个类 Person
// 有一个属性为可选 Apartment 类型 因为人不一定住在公寓内
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
}

// 定义一个类 Apartment
// 有一个属性为可选 Person 类型 因为公寓不一定有住户
class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
}

// 宣告一个变数为可选 Person 类型 并生成一个实例
var joe: Person? = Person(name: "Joe")
// 生成实例后 其内的 apartment 属性没有指派值 初始化为 nil
// 目前这个实例的强引用有 1 个 引用计数为 1

// 宣告一个变数为可选 Apartment 类型 并生成一个实例
var oneUnit: Apartment? = Apartment(unit: "5A")
// 生成实例后 其内的 tenant 属性没有指派值 初始化为 nil
// 目前这个实例的强引用有 1 个 引用计数为 1

接着把这两个不同的实例联系起来,如下:

joe!.apartment = oneUnit
oneUnit!.tenant = joe
// 这时这两个实例 各别都有 2 个强引用

//如果此时将 2 个变数断开强引用
joe = nil
oneUnit = nil

// 这时这 2 个实例 各别仍还是有 1 个指向对方的强引用
// 也就造成记忆体无法释放

前面提过,上面的代码中,在实例后的惊叹号(!)指的是将一个可选类型强制解析,也就是隐式解析可选类型。

解决实例间的强引用循环

Swift 提供了两种办法来解决强引用循环,分别是弱引用(weak reference)及无主引用(unowned reference)。

这两种引用也能引用实例,但因为不是强引用,所以不会保留住实例的引用(也就是这个实例的引用计数不会增加)。

而两者的差别在于,如果一个引用这个实例的变数在生命周期中,可能会为nil时,就使用弱引用,而在初始化之后不会再变为nil的则是使用无主引用。

弱引用

弱引用(weak reference)也能引用实例,但不会保留住引用的实例(所以这个实例的引用计数不会增加)。而一个引用这个实例的变数在生命周期中可能没有值(为nil)时,就使用弱引用。

弱引用必须宣告为变数,表示可以被修改,同时也必须是可选类型(optional),因为可能没有值(为nil)。

弱引用使用weak关键字来定义,下面将前面强引用循环的例子改写,将类Apartment内的属性tenant改为弱引用(因为公寓可能有时没有住户,即有时会没有值,适合使用弱引用):

class AnotherPerson {
    let name: String
    init(name: String) { self.name = name }
    var apartment: AnotherApartment?
}

class AnotherApartment {
    let unit: String
    init(unit: String) { self.unit = unit }

    // 将这个属性定义为弱引用 使用 weak 关键字
    weak var tenant: AnotherPerson?
}

var joe2: AnotherPerson? = AnotherPerson(name: "Joe")
var oneUnit2: AnotherApartment? = AnotherApartment(unit: "5A")
joe2!.apartment = oneUnit2

// 因为是弱引用
// 所以这个指派为实例的属性 不会增加 joe2 引用的实例的引用计数
oneUnit2!.tenant = joe2

// 当断开这个变数的强引用时 目前该实例的引用计数会减为 0
// 所以会将这个实例释放
// 而所有指向这个实例的弱引用 都会被设为 nil
joe2 = nil

// 随着上面的 joe2 被释放
// 目前 oneUnit2 引用的实例的引用计数减为 1
// 下面再将原本的强引用断开 引用计数减为 0 则也会将此实例释放
oneUnit2 = nil

无主引用

与弱引用一样,无主引用(unowned reference)不会保留住引用的实例(所以这个实例的引用计数不会增加),但不同的是,无主引用会被视为永远有值,所以需要被定义为非可选类型,而因此可以直接存取,不需要强制解析(即加上惊叹号!)。

无主引用使用unowned关键字来定义,下面例子将介绍一个使用者类Customer与信用卡类CreditCard之间的关系,使用者不一定有信用卡,但当产生出信用卡时,这信用卡一定属于某个使用者:

// 定义一个类 Customer
class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
}

// 定义一个类 CreditCard
class CreditCard {
    let number: Int

    // 定义一个无主引用 非可选类型 因为一定会有使用者(一定有值)
    unowned let customer: Customer

    init(number: Int, customer: Customer) {
        self.number = number
        self.customer = customer
    }
}

// 宣告一个可选 Customer 的变数
var jess: Customer? = Customer(name: "Jess")

// 接着生成一个 CreditCard 实例并指派给 jess 的 card 属性
jess!.card = CreditCard(number: 123456789, customer: jess!)
// 这个 CreditCard 实例的 customer 属性 则使用无主引用指向 jess

// 现在 jess 指向的实例 引用计数为 1 (即 jess 这个变数强引用指向的)
// jess 内的属性 card 指向的实例 引用计数也为 1
// (即这个 card 属性强引用指向的)

// 而 CreditCard 实例的 customer 属性 因为是无主引用指向 jess
// 所以不会增加引用计数

// 这时将 jess 指向的实例强引用断开
jess = nil
// 这时这个实例的引用计数为 0 则实例会被释放
// 指向 CreditCard 实例的强引用也会随之断开
// 因此也就被释放了

无主引用和隐式解析可选类型

除了上面的两种情况,还有一种情况为,两个互相引用实例的属性都必须有值,且初始化后永远不为nil,这时要在其中一个类使用无主引用,另一个类使用隐式解析可选类型。

当初始化完成后,这两个属性都能被直接存取(不需要强制解析,即不用加上惊叹号 !),且也避免了强引用循环。

下面的例子分别定义了类Country及类City,每个类都有一个储存对方类实例的属性,也就是每个国家(Country)都有一个首都(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
    }
}

var country = Country(name: "Japan", capitalName: "Tokyo")

上面的代码中,为了建立两个类的依赖关系,City的建构函数有一个Country实例的参数,并储存为country属性。Country的建构函数则呼叫了City的建构函数。

以Country来说:

在构造器中要使用self代表自己本身,必须要在自己初始化完成后才行。而Country的建构函数中,在name设置完值后就完成了初始化,所以可以将self(即本身)当做参数传给City的建构函数。
而因为Country将属性capitalCity定义为隐式解析可选类型,所以不需要使用可选绑定(optional binding)或强制解析(即在后面加惊叹号 !)去引用City,可以直接存取。

以City来说:

City在country属性使用了无主引用,因为一个城市一定属于一个国家,所以一定有值,且不会增加Country实例的引用计数。(其实就如同前面例子Customer之于CreditCard的关系)

以上的意义在于可以通过一条语句同时生成Country跟City的实例,而不会产生强引用循环,并且capitalCity属性可以被直接存取,不需要被强制解析(即加上惊叹号!)。

闭包的强引用循环

除了前面提到的类实例之间可能产生强引用循环,当将一个闭包(closure,也就是匿名函数)设置给一个类实例的属性时,这个闭包函数内存取了这个实例的某个属性,或是呼叫了实例的一个方法,都会导致闭包捕获(capture)了self,进而产生了强引用循环。

闭包所捕获的引用会被自动视为强引用。这个强引用循环的产生,是因为闭包也是引用类型,当把闭包设置给一个属性时,实际上是设置了闭包的引用。

下面是一个闭包与类实例的强引用循环的例子:

// 定义一个代表 HTML 元素的类 HTMLElement
class HTMLElement {
    let name: String
    let text: String?

    // 定义为 lazy 属性 表示只有当初始化完成以及 self 确实存在后
    // 才能存取这个属性
    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
    }

}

// 宣告为可选 HTMLElement 类型 以便后面设为 nil
var paragraph: HTMLElement?
      = HTMLElement(name: "p", text: "Hello, world")

// 初始化完成后 就可以存取这个属性
print(paragraph!.asHTML())

// 这时 paragraph 指向的实例的引用计数为 2
// 一个是自己 一个是闭包

// 而这个实例也有一个强引用指向闭包

// 这时将变数指向的强引用断开 引用计数减为 1
// 引用仍然不会被释放 造成强引用循环
paragraph = nil

闭包中虽然多次使用了self,但只捕获了 1 个强引用(也就是引用计数只算 1 次)

解决闭包的强引用循环

在定义闭包时,同时定义捕获列表(capture list)作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的强引用循环。捕获列表中必须定义每个闭包中捕获的引用为弱引用或无主引用(依其相互关系来决定)。

定义捕获列表

捕获列表(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
    // 闭包内执行的代码
}

// 或是省略闭包定义的参数或返回类型 让他们可以通过上下文自动推断
// 这时将捕获列表放在关键字 in 的前面
lazy var someClosure: () -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // 闭包内执行的代码
}

下面则是将前面的例子HTMLElement中的闭包加上捕获列表,便可以避免强引用循环:

class NewHTMLElement {
    let name: String
    let text: String?

    lazy var asHTML: Void -> String = {
        // 这边使用无主引用 unowned
        [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
    }

}

这个部分能看懂就看,觉得不容易理解,就先浏览过有个概念也就够了。

Swift基础入门知识学习(18)-构造过程(建构过程及解构过程)-讲给你懂

高效阅读-事半功倍读书法-重点笔记-不长,都是干货

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-12-13 12:56:35  更:2021-12-13 12:59:11 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 9:02:59-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码