美文网首页Swift Advance
Swift ARC (引用&循环引用的造成及解决方案)

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

作者: Hellolad | 来源:发表于2018-08-20 20:24 被阅读4次

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)

相关文章

网友评论

    本文标题:Swift ARC (引用&循环引用的造成及解决方案)

    本文链接:https://www.haomeiwen.com/subject/aeqriftx.html