Swift ARC (引用&循环引用的造成及解决方案)

ARC简介

ARC负责跟踪和管理应用程序的内存状态,一般情况下不需要考虑内存管理问题,当一个实例不再需要时,ARC会自动释放该实例占用的内存,ARC仅仅适用于类的实例,例如结构体,枚举是值类型,它们不通过引用传递和存储,所以ARC不会管理他们。

ARC是如何工作的?

每次创建一个类的实例的时候,内存会开辟一块空间去存放该实例以及它的存储属性的值,当ARC将该实例释放掉之后,就无法再去调用该实例的属性或者函数,如果坚持调用的话,则可能会出现崩溃的问题。
为了确保实例仍然在需要时保持存活,ARC会跟踪当前引用的实例的每个属性,只要至少有一个对该实例有强引用,ARC就不会释放该实例。

举例说明

class Person {
  let name: String
  init(name: String) {
    self.name = name
    print("\(name) 初始化")
  }
  deinit {
    print("\(name) 被释放")
  }
}

let person1: Person?
let person2: Person?
let person3: Person?

我们定义了三个Person,但是Person都是可选(optional)类型,所以它们在内存中并不存在,他们的值也是nil,并且也不会引用Person实例

person1 = Person(name: "Hellolad")  // Hellolad 被初始化

当我们把Person(name: "Hellolad")赋值给person1后,person1不再为nil,person1这时对Person有了一个强引用,这时ARC就起作用了,它跟踪到了该实例,并且保证该实例不会被释放

person2 = person1
person3 = person1

当我们把person1赋值给了person2 person3,这两个实例没有被初始化,只是它们强引用了和person1同样的person实例

person1 = nil
person2 = nil

当我们把这个两个实例置为nil之后,deinit函数并不会执行,因为ARC知道还有person3在引用着person1,所以该实例仍然不会被释放

person3 = nil // Hellolad 被释放

当我们把person3也置为nil的时候,person1才能被释放掉,并打印 Hellolad 被释放

循环引用

当两个类互相引用彼此的时候,就会造成循环引用。

class Person {
  let name: String
  var apartment: Apartment?
  init(name: String) {
    self.name = name
  }
  deinit {
    print("\(name) 被释放")
  }
}

class Apartment {
  let unit: String
  init(unit: String) {
    self.unit = unit
  }
  var tenant: Person?
  deinit {
    print("Apartment \(unit) 被释放")
  }
}

上面的代码我们故意创建一个可以造成循环引用的例子,每一个Person都可以拥有一个公寓,每个公寓也都可以拥有一个人,他们的引用关系如下图:

图片: The Swift Programming Language AutomaticReferenceCounting

图片来自官网

var hellolad: Person?
var loveApartment: Apartment?

hellolad = Person(name: "Hellolad")
loveApartment = Apartment(unit: "502")

我们创建了一个人和爱情公寓的两个实例,他们现在都强引用着各自的实例,但并没有强引用对方,因为我们还有给他们的apartmenttenant赋值。

hellolad?.apartment = loveApartment
loveApartment?.tenant = hellolad

然后我们把他们的各自的实例,赋值对应到他们各自的实例的属性apartmenttenant,此时他们的引用关系如下图:

图片: The Swift Programming Language AutomaticReferenceCounting

图2.png

hellolad = nil
loveApartment = nil

当我们把两个实例置为nil的时候,ARC会把他们的引用计数置为0,这是没有错的,但是控制台却没有打印任何释放实例的信息?
此时我们再看他们的引用情况:

图片: The Swift Programming Language AutomaticReferenceCounting

图3.png

我们发现他们自已已经移除了对PersonApartment的互相引用,所以ARC依然在追踪他们的属性的引用计数.

解决循环引用的两种方案

Swift在类实例的属性相互引用造成的循环引用中,提供了两种解决方案, 弱引用(weak)和无主引用(unowned),weakunowned可以使一个实例能和另一个实例保持互相引用,但却不是强引用。两者的区别weak在OC里和weak是一样的,unowned和OC里的unsafe_unretainedunowned设置以后它的原理的引用如果被释放了,它依然会保持着一个对被释放的的实例的引用,而weak就不一样了,如果它引用的对象被置为nil那么它也会自动的指向nil(所以被weak的对象需要是Optional)

weak var apartment: Apartment?
weak var tenant: Person?
// Hellolad 被释放
// Apartment 502 被释放

他们的关系图:

图片: The Swift Programming Language AutomaticReferenceCounting

图4.png

官方建议:如果能够确定在访问时不会已被释放的话,尽量使用 unowned ,如果存在被释放的可能,那就选择用 weak 。

闭包的循环引用(Closure/Block)

我们上面知道了如果我们在两个实例里互相引用了对方,会造成循环引用,而我们使用闭包捕获了self之后,也可能会造成循环引用,我们来看Swift是如何发生这种问题,以及应该怎样去解决这样的问题?

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) 被释放")
  }
}

HTMLElement我们定义了三个属性,其中asHTML是一个懒加载的闭包属性,并且返回()-String的一个函数。我们想要打印出来像这种样式的html语句:<h1>Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad</h1>
我们来实践一下:

let heading = HTMLElement(name: "h1")
let defaultText = "Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
heading = nil
// 打印: <h1>Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad</h1>
// h1 被释放

我们为heading.asHTML赋值,并且打印他,发现h1已经被释放,因为它并没有走asHTML的存储属性的get函数,所以没有捕获到self,因此并不会造成循环引用

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "Hello lad!!")
print(paragraph?.asHTML())
paragraph = nil
// 打印:"<p>Hello lad!!</p>"

只打印了"<p>Hello lad!!</p>"但是p确并没有被释放,它走了该asHTML存储属性的get函数,该闭包已经造成了一个循环引用。

图片: The Swift Programming Language AutomaticReferenceCounting

图5.png

解决闭包的循环引用

我们知道了闭包造成的循环引用,我们依然需要用weakunwoned来打破循环强引用,我们知道weak是当被修饰的属性被置为nil之后,ARC就会帮助我们销毁该引用,并将retainCount记为0。然而我们的闭包持有了self,而self又持有了闭包,所以self是不可能为nil的,如果为nil,那我们调用该asHTML将会永远crash,所以我们最合适的方式是使用unowned

lazy var asHTML: () -> String = {
  [unowned self] in
  if let text = self.text {
    return "<\(self.name)>\(text)</\(self.name)>"
  } else {
    return "<\(self.name)/>"
  }
}

他们之间的引用关系如下:

图片: The Swift Programming Language AutomaticReferenceCounting

图6.png

这样的话 闭包内的self对于闭包来说就是一个无主的。我们打印出来看:

paragraph = nil
// p 被释放

参考文献&图片来源:
1. The Swift Programming Language AutomaticReferenceCounting
2. What Is Automatic Reference Counting (ARC)

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

推荐阅读更多精彩内容