Swift 循环引用

简介

Swift 使用 Automatic Reference Counting (ARC) 管理应用内存的使用,ARC自动释放那些不在使用的对象,然而在一些场景下ARC需要更多的对象之间的引用信息来管理内存.

ARC 如何工作

每当你创建一个实例instance对象时,ARC分配一块儿内存用来存储instance对象信息包括对象类型,以及属性的值.
此外,当instance对象不在使用的时候,ARC释放instance对象所占的内存,以便释放的内存可在利用.然而,
instance对象被ARC释放后,将不在允许访问该instance对象的属性或者方法,如果你尝试访问,结果就会使APP crash
为了确保正在使用的instance对象,不被释放. ARC追踪分配给instance对象的属性 property 常量 变量 即 引用计数.只要instance对象被引用着,就不会被释放.

ARC 的作用
下面一个Person类对象 有一个name 常量属性 一个初始化方法并赋值给name属性,一个析构方法

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

定义三个Person? 类型的变量 reference1 reference2 reference3默认值为nil

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

创建Person类的实例对象 reference1 强引用Person instance

reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"

reference2 reference3 强引用Person instance

reference2 = reference1
reference3 = reference1

通过赋值nil给 reference1 reference2 使得Person instance引用变为1,ARC将不会释放 Person instance

reference1 = nil
reference2 = nil

当最后一个强引用设置为nil的时候,Person instance执行了析构函数

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

对象间的循环引用

在上面的例子🌰中,ARC能过追踪Person instance的引用计数,进行内存管理. 然而,我们很容易写出instance对象不存在强引用情况的代码,发生在两个class instances直接彼此强引用.(各位对方的属性)称为引用循环

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") }
}

定义Person Apartment变量 并初始化

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
初始化后 john unit4引用关系图

接下来给person 入住公寓, Apartment记录person

// 由于john是可选型, 访问的时候需要解包
john!.apartment = unit4A
unit4A!.tenant = john
Person 入住后 Apartment记录后.png
john = nil
unit4A = nil

可以看到Person跟Apartment之间的强引用环,因此,当你打破john对Person 跟unit4对Apartment的强引用时,Person和Apartment之间的闭环仍然存在,此时john unit4不会被ARC回收.(造成内存泄漏)

解决

Swift提供了两种方式 在属性 类声明前加 weak或者 unowned ,weak或者 unowned引用允许一个instance非强引用令一个instance,来避免出现强循环.
那什么时候用weak什么时候用unowned呢? weak允许使用在生命周期较短的那一方,unowned稍后再讲. so 在Person 跟Apartment这个场景中, Apartment 的生命周期肯定是比Person要长的.

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
weak 修饰后的引用关系图.png

可以看到john 强引用Apartment Apartment弱引用john

john = nil
// Prints "John Appleseed is being deinitialized"
john打破强引用后

同样

unit4A = nil
// Prints "Apartment 4A is being deinitialized"
unit4打破强引用后

In systems that use garbage collection, weak pointers are sometimes used to implement a simple caching mechanism because objects with no strong references are deallocated only when memory pressure triggers garbage collection. However, with ARC, values are deallocated as soon as their last strong reference is removed, making weak references unsuitable for such a purpose.
在使用垃圾收集的系统中,弱指针有时用于实现简单的缓存机制,因为只有在内存压力触发垃圾收集时才释放没有强引用的对象。然而,使用ARC,值在其最后一个强引用被删除后立即被释放,这使得弱引用不适合用于此目的。

unowned
weak一样,unowned也不会对它引用的实例保持强控制。但是,与weak不同的是,当其他实例具有相同的生命周期期或更长的生命周期时,将使用unowned。通过在属性或变量声明前放置unowned关键字,可以指示一个unowned引用。

一个unowned应该总是有一个值。因此,ARC从不将unowned引用的值设置为nil,这意味着unowned引用是使用非可选类型定义的。

只有在确定引用始终引用未释放的实例时,才使用unowned。如果在释放实例之后尝试访问一个unowned的值,将会得到一个运行时错误。

接下来的例子🌰中,Customer客户 CreditCard信用卡 每个人都有可能有一张信用卡,也有可能没有信用卡. 但是一张信用卡必定有一个对应的客户. 那么Customer跟CreditCard之间必定存在一个强引用循环.此时使用unowned避免循环引用

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") }
}

创建一个Customer Instance 并设置CreditCard

john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john CreditCard创建后引用关系

因为没有对Customer实例的更强引用,所以john被释放了。在此之后,就不再有对CreditCard实例的强引用,它也被释放

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

上面的示例展示了如何使用安全的unowned引用。Swift还为需要禁用运行时安全检查(例如出于性能原因)的情况提供了不安全的unowned引用。与所有不安全的操作一样,您将负责检查代码的安全性。通过编写unowned(不安全)来指示一个不安全的unowned引用。如果您试图在它引用的实例被释放后访问一个不安全的unowned引用,那么您的程序将尝试访问实例曾经所在的内存位置,这是一个不安全的操作。

Unowned和隐式展开的可选属性
上面关于weakunowned引用的示例涵盖了两种更常见的场景,需要打破强引用循环。

Person和Apartment的例子显示了这样一种情况,两个属性都被允许为nil,有可能导致强引用循环。此场景最好使用弱引用来解决。

Customer和CreditCard示例显示了一种情况,其中一个属性允许为nil,而另一个属性不能为nil,这两种属性都有可能导致强引用循环。此场景最好使用unowned引用来解决。

然而,还有第三种情况,在这种情况下,两个属性都应该始终有一个值,并且一旦初始化完成,任何一个属性都不应该为nil。在这个场景中,将一个类上的unowned属性与另一个类上的隐式展开的可选属性相结合是很有用的。

这使得初始化完成后可以直接访问这两个属性(没有可选的展开),同时仍然避免了引用循环。本节将向你展示如何建立这样的关系。

下面的示例定义了两个类,Country和City,每个类都将另一个类的实例存储为属性。在这个数据模型中,每个国家必须始终有一个首都城市,并且每个城市必须始终属于一个国家。为了表示这一点,Country class有一个capital - City property,而City class有一个Country property:

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
    }
}

要设置这两个类之间的相互依赖关系,City的初始化器接受一个Country实例,并将该实例存储在其Country属性中。

City的初始化器从Country的初始化器中调用。但是,Country的初始化器不能将self传递给City初始化器,直到一个新的Country实例被完全初始化,如两阶段初始化中所述。

为了满足这一要求,你可以将Country的capitalCity属性声明为一个隐式展开的可选属性,(City!)。这意味着capitalCity属性的默认值为nil,与任何其他可选属性一样,但是不需要像隐式展开Optionals中描述的那样展开它的值就可以访问它。

因为capitalCity有一个默认的空值,所以只要Country实例在其初始化器中设置了name属性,就会认为新Country实例已经完全初始化。这意味着国家参考和通过隐式初始化器可以开始自我财产一旦该国名称属性设置。因此Country初始化器可以将self作为一个参数传递给City初始化设置City的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"

闭包强引用

当你将一个闭包作为对象的属性时,同时闭包内又访问了对象内的属性 或者方法时.这时候闭包会捕获对象形成引用闭环.

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 {
        print("\(name) is being deinitialized")
    }

}

例如,可以将asHTML属性设置为闭包,如果text属性为nil,则该闭包默认为某些文本,以防止表示返回空HTML

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>"

sHTML属性被声明为惰性属性,因为只有当元素实际需要作为某个HTML输出目标的字符串值呈现时才需要它。asHTML是一个惰性属性,这意味着您可以在缺省闭包中引用self,因为在初始化完成且self已知存在之前,惰性属性不会被访问。

HTMLElement类创建和打印一个新实例

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
访问asHTML()闭包后引用关系

即使闭包内使用self多次,只强引用HTMLElement对象一次

当你打破paragraph跟HTMLElement对象的强引用后 paragraph = nil ,会发现HTMLElement析构方法并没有执行.(内存泄漏)

解决

捕获列表中的每一项都是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
}

Weak and Unowned References
当闭包和它捕获的实例总是相互引用,并且总是同时释放时。此时将闭包中的捕获定义为一个unowned引用

相反,当捕获的引用可能在将来的某个时刻变为nil时,将捕获定义为weak引用。weak引用始终是可选的类型,当它们引用的实例被释放时,将自动变为nil。这使你能够检查它们是否存在于闭包中

如果捕获的引用永远不会变为nil,则应该始终将其捕获为unowned引用,而不是weak引用。
so,HTMLElement将适合使用unowned引用

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实例 paragraph

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
unowned 修饰后

paragraph 被释放

paragraph = nil
// Prints "p is being deinitialized"

参考文章

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