机动引用计数,循环引用实例深入分析

图片 1Swift

  • Swift 使用自动引用计数(ARC)机制来跟踪和管理你的应用程序的内存。

  • 引用计数仅仅应用于类的实例。结构体和枚举类型是值类型,不是引用类型,也不是通过引用的方式存储和传递。

  • 将实例赋值给属性、常量或变量,都会创建此实例的强引用。只要强引用还在,实例是不允许被销毁的。

  • 如果两个类实例互相持有对方的强引用,因而每个实例都让对方一直存在。这就是所谓的循环强引用。

  • 可以通过定义类之间的关系为弱引用或无主引用,以替代强引用,从而解决循环强引用的问题。

  • Swift 提供了两种办法用来解决在使用类的属性时所遇到的循环强引用问题:弱引用(weak reference)和无主引用(unowned reference)。

  • 对于生命周期中会变为nil的实例使用弱引用。相反地,对于初始化赋值后再也不会被赋值为nil的实例,使用无主引用。

自动引用计数

swift使用自动引用计数(ARC)机制来跟踪和管理你的应用程序的内存。通常情况下,Swift内存管理机制会一直起作用,我们无须自己来考虑内存的管理。ARC会在类的实例不再被使用时,自动释放其占用的内存。

note:引用计数仅仅应用于类的实例。结构体和枚举类型是值类型,不是引用类型,也不是通过引用的方式存储和传递。

iOS 使用引用计数来进行内存管理。在某些情况下,代码编写不慎会产生循环引用,进而导致内存泄露。根据我们目前项目中的代码来看,开发成员对导致循环引用的原因可能没有完全掌握清楚,所以存在着滥用以及漏用 weak 的现象,这里进行简要分析,并进行规范,防止 weak 的滥用及漏用。

除了类的实例之间会产生循环强引用之外,在闭包和类之间也可能产生强引用。这种强引用出现在将闭包赋值给类的属性,同时在闭包内部引用了这个类的实例时。究其原因,是因为闭包也是引用类型,当在闭包内部引用类的实例属性和方法时,闭包默认对类的实例拥有强引用。要解决这个问题,需要使用闭包捕获对象。

弱引用(Weak References)

  • 声明属性或者变量时,在前面加上weak关键字表明这是一个弱引用。

  • 弱引用必须被声明为变量,表明其值能在运行时被修改。弱引用不能被声明为常量。

  • 因为弱引用可以没有值,必须将每一个弱引用声明为可选类型。在 Swift 中,推荐使用可选类型描述可能没有值的类型。

  • ARC 会在弱引用的实例被销毁后自动将其赋值为nil。

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

john = nil
unit4A = nil

自动引用计数的工作机制

为了确保使用的实例不会被销毁,ARC会跟踪和计算每一个实例正在被多数属性,常量和变量所引用。哪怕实例的引用数为1,ARC都不会销毁这个实例。

为了使上述成为可能,无论你将实例赋值给属性、常量或变量,它们都会创建此实例的强引用。之所以称为“强”引用,是因为它会将实例牢牢的保持住,只要强引用还在,实例就不允许被销毁的。

首先要明确,引用计数是对 reference types(即 classes、closures) 而言的,value types(如 struct、enumeration)不使用引用计数进行内存管理,所以也就不存在“循环引用”这个概念。

下面这个例子是一个实例和闭包相互之间的强引用
class Student{ var name:String? var score:Int lazy var level: -> String = { switch self.score{ case 0..<60: return "C" case 60..<85: return "B" case 85..<100: return "A" default: return "D" } } init(name:String,score:Int){ self.name = name self.score = score } deinit { print("Student 对象:被销毁了") }}var xiaoMing:Student? = Student(name:"小明",score:86)print("(xiaoMing!.name!)成绩水平为:(xiaoMing!.levelxiaoMing = nil// 当实例被赋予nil时,没有调用析构函数

解决这个问题我们使用捕获列表[weak self,unowned delegate = self.delegate][unowned self, weak delegate = self.delegate!]

无主引用(Unowned References)

  • 和弱引用不同的是,无主引用是永远有值的。因此,无主引用总是被定义为非可选类型(non-optional type)。

  • 可以在声明属性或者变量时,在前面加上关键字unowned表示这是一个无主引用。

  • ARC 无法在实例被销毁后将无主引用设为nil,因为非可选类型的变量不允许被赋值为nil。

  • 如果试图在实例被销毁后,访问该实例的无主引用,会触发运行时错误。使用无主引用,必须确保引用始终指向一个未销毁的实例。

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

var allen: Customer?
allen = Customer(name: "Allen Appleseed")
allen!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

allen = nil
// 打印 “Allen Appleseed is being deinitialized”
// 打印 ”Card #1234567890123456 is being deinitialized”

类实例之间的循环强引用

我们可能会写出一个类实例的强引用数永远不能变成0的代码。如果两个类实例互相持有对方的强引用,因而每个实例都让对方一直存在,就是这种情况。这就是所谓的循环强引用。

我们可以通过定义类之间的关系为弱引用或无主引用,以替代强引用,从而解决循环强引用的问题。

class A{

    let name:String
    init(name:String){
        self.name = name
    }
    var b:  B?

    deinit{
        print("a (name) is dead")
    }
}

class B {
    let name:String
    init(name:String) {
        self.name = name
    }
    var a:A?
    deinit{
        print("b (name) is dead")
    }
}

func test(){
    let a = A(name: "obj a")
    let b = B(name: "obj b")

    a.b = b
    b.a = a
}

test()

我们一般会为被代理对象创建一个 delegate 属性,并且 delegate 需要由对象持有,此时如果我们的 delegate 对象同时持有了被代理对象,那么就会导致循环引用。如下例:

下面重新写一个
class Student2{ var name:String? var score:Int lazy var level: -> String = { [weak self] in switch self!.score{ case 0..<60: return "C" case 60..<85: return "B" case 85..<100: return "A" default: return "D" } } init(name:String,score:Int){ self.name = name self.score = score } deinit { print("Student对象:被销毁了") }}var xiaoMing2:Student2? = Student2(name:"小明",score:86)print("(xiaoMing2!.name!)成绩水平为:(xiaoMing2!.levelxiaoMing2 = nil

通过[weak self] in 在闭包前增加了捕获列表的定义。并且由于self可能为nil,因为是weak的弱引用,因此需要叹号解包self使用,

图片 2闭包引起的循环强引用结果输出

无主引用以及隐式解析可选属性(Unowned References and Implicitly Unwrapped Optional Properties)

  • 存在着第三种场景,在这种场景中,两个属性都必须有值,并且初始化完成后永远不会为nil。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解析可选属性。
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: "Canada", capitalName: "Ottawa")
print("(country.name)'s capital city is called (country.capitalCity.name)")
// 打印 “Canada's capital city is called Ottawa”

!会引起崩溃,不适合用。这里用可选型或者普通变量都比这个!好

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

Swift提供了两种办法用来解决你在使用类的属性时所遇到的循环强引用问题:弱引用(weak reference)和无主引用(unowned reference)。

弱引用和无主引用允许循环引用中的一个实例引用另外一个实例而不保持强引用。这样实例能够相互引用而不产生循环强引用。

对于生命周期中会变为nil的实例使用弱引用。相反地,对于初始化赋值后再也不会被赋值为nil的实例,使用无主引用。

protocol TestBDelegate: class { func printMessage() -> String}class TestB { var delegate: TestBDelegate init(delegate: TestBDelegate) { self.delegate = delegate } func callDelegate() { print(delegate.printMessage } deinit { print("TestB deinit") }}class TestC: TestBDelegate { var testb: TestB? init() { self.testb = TestB(delegate: self) } func printMessage() -> String { return "Hello world" } deinit { print("TestC deinit") }}var testc: TestC? = TestC()testc = nil
代码已上传至git:

闭包引起的循环强引用(Strong Reference Cycles for Closures)

  • 循环强引用还会发生在将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了这个类实例时。这个闭包体中可能访问了实例的某个属性,例如self.someProperty,或者闭包中调用了实例的某个方法,例如self.someMethod()。这两种情况都导致了闭包“捕获”self,从而产生了循环强引用。

  • 循环强引用的产生,是因为闭包和类相似,都是引用类型。

  • Swift 提供了一种优雅的方法来解决这个问题,称之为闭包捕获列表(closure capture list)。

  • 在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。

  • Swift 有如下要求:只要在闭包内使用self的成员,就要用self.someProperty或者self.someMethod()(而不只是someProperty或someMethod())。这提醒你可能会一不小心就捕获了self。

  • 捕获列表中的每一项都由一对元素组成,一个元素是weak或unowned关键字,另一个元素是类实例的引用(例如self)或初始化过的变量(如delegate = self.delegate!)。这些项在方括号中用逗号分开。

  • 在闭包和捕获的实例总是互相引用并且总是同时销毁时,将闭包内的捕获定义为无主引用。

  • 相反的,在被捕获的引用可能会变为nil时,将闭包内的捕获定义为弱引用
    。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。这使我们可以在闭包体内检查它们是否存在。

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: Void -> 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")
    }
}

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// 打印 “<p>hello, world</p>”

paragraph = nil
// 打印 “p is being deinitialized”

关于引用循环,尽量用weak;实在没办法,才考了用无主引用。

弱引用

弱引用不会对引用的实例保持强引用,因而不会阻止ARC销毁被引用的实例。这个特性阻止了引用变为循环强引用。声明属性或者变量时,在前面加上weak关键字声明这是一个弱引用。

在实例的生命周期中,如果某些时候引用没有值,哪么弱引用可以避免循环强引用。如果引用总是有值,则可以使用无主引用。

note: 弱引用必须被声明为变量,表明其值能在运行时被修改。弱引用不能被声明为常量。 弱引用可以没有值,我们必须将每一个弱引用声明为可选类型。在Swift中,推荐使用可选类型描述可能没有值的类型。

因为弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC会在引用的实例被销毁后自动将其赋值为nil。

class A{

    let name:String
    init(name:String){
        self.name = name
    }
    var b:  B?

    deinit{
        print("a (name) is dead")
    }
}

class B {
    let name:String
    init(name:String) {
        self.name = name
    }
    weak  var a:A?
    deinit{
        print("b (name) is dead")
    }
}

func test(){
    let a = A(name: "obj a")
    let b = B(name: "obj b")

    a.b = b
    b.a = a
}

test()

上述示例中,testc 不会被释放,原因如下:

--> 传送门:Swift_基本语法

无主引用

和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用是永远有值的。因此,无主引用总被定义为非可选类型。我们可以在声明属性或者变量时,在前面添加关键字unowned表示这是一个无主引用。

由于无主引用是非可选类型,我们不需要在使用它的时候将其展开。无主引用总是可以被直接访问。不过ARC无法在实例被销毁后将无主引用设为nil,因为非可选类型的变量不允许被赋值为nil。

note:如果我们试图在实例被销毁后,访问该实例的无主引用,会触发运行时错误。使用无主引用,我们必须确保引用始终指向一个未销毁的实例。
还需要注意的是如果我们试图访问实例已经被销毁的无主引用,Swift确保程序会直接奔溃,而不会发生无法预期的行为。所以我们应当避免这样的事情发生。

class A{

    let name:String
    init(name:String){
        self.name = name
    }
    var b:  B?

    deinit{
        print("a (name) is dead")
    }
}

class B {
    let name:String
    init(name:String,a:A ) {
        self.name = name
        self.a = a
    }
    unowned var a:A
    deinit{
        print("b (name) is dead")
    }
}


func test(){
    let a = A(name: "obj a")
    let b = B(name: "obj b",a:a)


}

test()
  • testc 是代理对象,testb 是被代理对象;
  • testc 强引用了 testb;
  • TestC 的构造器中为 testb 的 delegate 参数传入了 self;
  • testb 中强引用了 delegate 属性,而此时 delegate 属性正是 testc,形成循环引用;

无主引用以及隐式解析可选属性

还存在着第三种场景,在这种场景中,两个属性都必须有值,并且初始化完后永远不会为nil。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解析可选属性。

这使两个属性在初始化完成后被直接访问(不需要可选展开),同时避免了循环引用。

class Country {

    let name: String
    var capitalCity: City!

    init(name:String,capitalCityName:String){
        self.name = name
        self.capitalCity = City(name: capitalCityName, country: self)
    }

    deinit{
        print("country dead")
    }

}

class City{

    let name:String
    unowned let country: Country

    init(name:String,country:Country){
        self.name = name
        self.country = country
    }
    deinit{
        print("city dead")
    }
}

func test(){
    var country = Country(name: "china", capitalCityName: "beijing")
    let city = country.capitalCity
    print("(country.name) and (country.capitalCity.name)")
    print("(city.country.name)and (city.country.capitalCity.name) ")
}

test()

我们有两种方式来打破循环引用:weak & unowned。

闭包引起的循环强引用

循环强引用还会发生在当我们将一个闭包赋值给类实例的某个属性,并且这个闭包中又使用了这个类的实例。这个闭包中可能访问了实例的某个属性(self.someProperty),或者闭包中调用了实例的某个方法(self.someMethod),这两种情况都导致闭包捕获self,从而产生了循环强引用。

循环强引用的产生,是因为闭包和类相似,都是引用类型。当我们把一个闭包赋值给某个属性时,你也把一个引用赋值给这个闭包。实质上,这跟之前的问题一样的---两个强引用让彼此一有效。

class HTMLElement{

    let name:String
    let text:String?

    lazy var asHTML: Void ->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 dead")
    }

}


func test(){
    var  html = HTMLElement(name: "h1", text: "hello world!")
    print(html.asHTML())
}

test()

note:虽然闭包多次使用了self,它只捕获HTMLElement实例的一个强引用。

为了打破这种循环引用,我们可以使用 weak 来声明 delegate 属性。weak 表示不对该属性进行强引用,也就是不会将 weak 声明的属性的引用计数加 1,这样就防止了循环引用。当 weak 属性引用的对象被释放后,weak 属性会被 ARC 置为 nil。修改后的 TestB 代码如下:

解决闭包引起的循环强引用

在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无主引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。

note:swift有如下要求:只要闭包内使用self的成员,就要用self.someproperty或者self.someMethod(),而不是someproperty或someMethod()。这提醒我们可能一不小心就捕获了self。

class TestB { weak var delegate: TestBDelegate? init(delegate: TestBDelegate) { self.delegate = delegate } func callDelegate() { print(delegate?.printMessage } deinit { print("TestB deinit") }}

定义捕获列表

捕获列表中的每一项都由一个对元素组成,一个元素是weak或unowned关键字,另外一个元素是类实例的引用(如self)或初始化过的变量(如delegate = self.delegate!)。这些项在放括号中用逗号分开。

class HTMLElement{

    let name:String
    let text:String?

    lazy var asHTML: Void ->String = {

        [weak weakSelf = self] in

        if let text = weakSelf!.text {
            return "<(weakSelf!.name)> (text)</(weakSelf!.name)>"
        }else{
            return "</(weakSelf!.name)>"
        }

    }

    init(name:String,text:String? = nil){
        self.name = name
        self.text = text
    }

    deinit{
        print("(name) is dead")
    }

}


func test(){
    var  html:HTMLElement? = HTMLElement(name: "h1", text: "hello world!")
    print(html!.asHTML())
    html = nil
}

test()

如果闭包有参数列表或返回类型,把捕获列表放在它们前面:

lazy var someClosure: (Int,String)->String = {

        [unowned self,weak delegate = self.delegate!] (index:Int,StringToProcess:String)-> String in

        //closure statement

    }
    ```

如果闭包没有指明参数列表或者返回类型,即它们会通过上下文推断,哪么可以把捕获列表和关键字in放在闭包最开始的地方:

lazy var someClosure: Void ->String = {

    [unowned self,weak delegate = self.delegate!]  in

    //closure statement

}
```

这里注意两点:

弱引用和无主引用

在闭包和捕获的实例总是相引用时并且总是同时销毁时,将闭包内的捕获定义为无主引用。

相反的,在捕获的引用可能会变为nil时,将闭包内的捕获定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会被自动设置为nil。这使我们可以在闭包体内检查它们是否存在。

note:如果被捕获的引用绝对不会变为nil,应该用无主引用,而不是弱引用。

class HTMLElement{

    let name:String
    let text:String?

    lazy var asHTML: Void ->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 dead")
    }

}


func test(){
    var  html:HTMLElement? = HTMLElement(name: "h1", text: "hello world!")
    print(html!.asHTML())
    html = nil
}

test()
  • TestB 的 delegate 属性使用了 weak 来声明,这样声明后,TestB 的实例对象就不会再强引用 delegate 对象。
  • 对于 weak 声明的属性,必须声明为 Optional 类型(如上例中的 TestBDelegate?),因为 weak 引用的对象被释放时,会将 weak 引用设置为 nil;

同 weak 一样,使用 unowned 声明的属性表示不对该属性进行强引用,也就是不会将 unowned 声明的属性的引用计数加 1,这样可以防止循环引用。单从这一点上看,unowned 与 weak 的作用是相同的,但是两者还是有一些区别的:

  • unowned 属性引用的对象被释放后,unowned 属性不会被 ARC 置为 nil,如果 unowned 被释放后,仍然使用 unowned 属性,就属于引用无效内存,从而导致 crash;
  • 使用 unowned 声明属性表示该属性的生命周期一定大于对象本身,原因如第一条所述;
  • weak 声明的属性必须声明为 Optional 类型,而 unowned 声明的属性不用声明为 Optional 类型,因为我们确认 unowned 属性的生命周期大于本对象;

使用 unowned 声明 delegate 的代码如下:

class TestB { unowned var delegate: TestBDelegate init(delegate: TestBDelegate) { self.delegate = delegate } func callDelegate() { print(delegate.printMessage } deinit { print("TestB deinit") }}

这里注意三点:

  • TestB 的 delegate 属性使用了 unowned 来声明,这样声明后,TestB 的实例对象就不会再强引用 delegate 对象。
  • 对于 unowned 声明的属性,我们没有声明为 Optional 类型,因为我们明确知道 testb 的生命周期不会长于 delegate
  • 如果 TestB 对象的 delegate 生命周期比 TestB 对象生命周期短,则会触发 crash。例如下述代码会导致 crash:
var testc: TestC? = TestC()var testb: TestB? = testc?.testbtestc = niltestb?.callDelegate() // testc 已经被释放,此时如果调用 testb 的 delegate 属性,会触发 crash,因为 unowned 声明的属性不会自动被置为 nil

我们可以看到 unowned 存在 crash 的风险,那么为什么还要有 unowned 这种声明方式呢?

  • 使用 unowned 声明,可以明确的告诉代码阅读者,unowned 属性的生命周期大于本对象;
  • 使用 unowned 声明,我们可以不使用 Optional 类型,这样引用的时候就无需使用可选链去调用该属性了;
  • 其余的原因暂时未想到,如果有想到,欢迎补充;

如下例:

class TestA { var testADescription: String? var describeTestA:  -> Void)? init() { describeTestA = { self.testADescription = "Hello, TestA" } } deinit { print("TestA deinit") }}var testa: TestA? = TestA()testa = nil

上述示例中不会 testa 不会被释放。原因如下:

  • describeTestA 是一个 closure,而 Swift 的 closure 也是使用引用计数管理内存的;
  • testa 对象强持有了 describeTestA closure;
  • describeTestA 中,我们使用了 testa 对象的属性 testADescription,这时,testa 对象会被 describeTestA closure 捕获,并且是强引用捕获住,这样就导致了循环引用;

这类循环引用的情况可以归结为:一个对象强持有了一个 closure,同时 closure 强引用捕获了该对象。closure 强捕获对象的情况发生在 closure 内部调用了对象的方法,或者使用了对象的某个属性。

对于 closure 循环引用问题,可以使用 closure 的捕获参数列表来解决,在捕获参数列表中,我们可以将某个对象声明为 weak 或者 unowned,例如:

class TestA { var testADescription: String? var describeTestA:  -> Void)? init() { describeTestA = { [weak self] in if let strongSelf = self { strongSelf.testADescription = "Hello, TestA" } } } deinit { print("TestA deinit") }}var testa: TestA? = TestA()testa = nil

使用上述方式来定义 describeTestA 则可以避免循环引用。当然上述示例中也可以使用 [unowned self],unowned 与 weak 的区别已在上面详述过。还有一点需要注意,在 describeTestA closure 内部,使用了 if let strongSelf = self 的方式,使用这种方式的原因以及用处请参考:I finally figured out weakSelf and strongSelf。

对于非逃逸闭包,编译器可以保证在函数返回时闭包会释放它捕获的所有对象,所以对于非逃逸闭包,不会出现循环引用问题,例如:

class TestA { var testADescription: String? func decribe(decribeClosure: () -> Void) { decribeClosure() print(testADescription) } init() { decribe { self.testADescription = "Hello world" } } deinit { print("TestA deinit") }}var testa: TestA? = TestA()testa = nil

这里传递给 describe 函数的闭包就是非逃逸闭包,这类闭包强引用 self 并不会造成循环引用,因为在 init 函数执行结束后,闭包会释放它捕获的对象。所以,如果使用了非逃逸闭包,我们是无需使用 [weak self] 这类方式来声明捕获参数的,因为其不会产生循环引用问题。对于什么是非逃逸闭包,建议仔细阅读:可选型的非逃逸闭包

对于逃逸闭包而言,是有可能造成循环引用的,因为逃逸闭包可能会被赋值给本对象的某个强引用属性的,这时就导致了循环引用,如下:

class TestA { var testADescription: String? var describeClosure:  -> Void)? func decribe(describeClosure: @escaping () -> Void) { describeClosure() self.describeClosure = describeClosure print(testADescription) } init() { decribe { self.testADescription = "Hello world" } } deinit { print("TestA deinit") }}var testa: TestA? = TestA()testa = nil

上述代码产生了循环引用,因为传入 describe 函数的参数 describeClosure 被赋值给了 testa 的 describeClosure 属性。这里再提一点,如果 describe 参数没有被声明为逃逸闭包,那么编译器是不允许我们将其赋值给 testa 的 describeClosure 属性的。

虽然逃逸闭包可能造成循环引用,但是并不是所有的逃逸闭包都会造成循环引用,下面举几个例子。

使用 UIView animation 的接口如下:

open class func animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIViewAnimationOptions = [], animations: @escaping () -> Swift.Void, completion:  -> Swift.Void)? = nil)

我们可以看到 completion closure 和 animations clousre 都是逃逸闭包(completion 没有使用 escaping 关键字,但是参考 可选型的非逃逸闭包 可知 completion 也是逃逸闭包),所以这两个闭包内会强持有 self 对象。

但是这里并不会造成循环引用问题,因为我们传入的 animations 和 completion 闭包并没有被 self 持有(使用时我们是直接新建了一个 closure 传给了 animate 函数),这两个闭包被 Core Animation 持有,所以不存在循环引用。

虽然 animations/completion closure 没有被使用动画的对象持有,但是 animations/completion closure 强持有了使用动画的对象(我们这里假设为 self),那么会不会导致 self 的释放时机受到 animations 的影响而延迟呢?从内存管理的角度分析是会这样的,只要 Core Animation 没有释放 closure,那么 self 就不会被释放掉。由于没有看过动画实现的源码,下述只是推测:

  • 当 View 从 window 上移除后,会立即释放在该 View 以及 subviews 添加的动画,这时,animations/completion closure 被释放,随后 self 被释放。
  • 至于什么时候从 window 上移除,这里举个例子,当 viewController disappear 后,其 view 会从 window 上移除。一般来说,对于动画,我们肯定是要对 UIView 来处理的,而当我们不需要 UIView 时,这个 view 也肯定会被 window 移除,此时,动画其实完全可以停止了(因为 view 已经不在 window 上了,再做动画也没意义),animations/completion closure 就会释放,所以 UIView animate 动画的 closure 不会引起循环引用问题,一般也不会导致与 View 有关的内存释放延迟。

Alamofire 发送请求使用方法如下:

Alamofire.request(baseUrl   url, method: httpMethod, parameters: parameters as? [String: Any], encoding: encoding, headers: header).responseString {  in self.completeHandler(response: response, success: success, failure: failure) }@discardableResultpublic func responseString( queue: DispatchQueue? = nil, encoding: String.Encoding? = nil, completionHandler: @escaping (DataResponse<String>) -> Void) -> Self

responseString 的最后一个参数是一个逃逸闭包,虽然在这个闭包里面使用 self 强引用了调用 Alamofire.request 的对象,但是由于传递给 responseString 的 completionHandler 闭包并没有被调用对象所引用,所以这里是不会产生循环引用的。

但是这里存在一个问题:调用对象的释放时机收到了 completionHandler 生命周期的影响,我们深入看一下 responseString 的源码会发现如下:

@discardableResultpublic func responseString( queue: DispatchQueue? = nil, encoding: String.Encoding? = nil, completionHandler: @escaping (DataResponse<String>) -> Void) -> Self { return response(queue: queue, responseSerializer: DataRequest.stringResponseSerializer(encoding: encoding), completionHandler: completionHandler )}@discardableResultpublic func response<T: DataResponseSerializerProtocol>( queue: DispatchQueue? = nil, responseSerializer: T, completionHandler: @escaping (DataResponse<T.SerializedObject>) -> Void) -> Self { // ...... (queue ?? DispatchQueue.main).async { completionHandler(dataResponse) } } return self}

也就是说 completionHandler 最终是传给了 gcd 派发系统,所以completionHandler 会被 gcd 强引用住,如果这个 completionHandler 被释放的实际比较晚,那么它所引用的 self 也会延迟释放。

  • request 发送一般属于 controller 的工作,如果 controller 已经被 pop 出去了,我们再去执行数据源刷新、UI 渲染这类操作是完全没有意义的,对于这类情况,需要使用 [weak self] 和 if let strongSelf = self {} 的方式捕获 self 对象,以便对象能够及时释放。
  • 当然,也有例外,例如我们这个请求就是为了刷新一些用户数据,那么我们是完全可以直接捕获 self 的,这样我们可以保证 self 不被释放,能够正确的处理数据。

使用 SnapKit 布局的一般代码如下:

secondLabel.snp.makeConstraints {  in make.left.equalTo(self.firstLabel.snp.right).offset make.centerY.equalTo(self.firstLabel.snp.centerY) make.height.equalTo make.width.equalTo(ThemeSizes.pixelLineWidth1}

我们可以看一下 makeConstraints 这个函数的参数:

public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) { ConstraintMaker.makeConstraints(view: self.view, closure: closure)}// ConstraintMaker 的 makeConstraints 函数internal static func makeConstraints(view: ConstraintView, closure: (_ make: ConstraintMaker) -> Void)

这个函数的 closure 参数是一个非逃逸闭包,也就是说这个闭包不会被其他属性所持有,执行完毕后就会被释放,所以,这里不会有循环引用问题。

本文分析了造成循环引用的常见原因,并讲述了如何打破循环引用,最后,给出了几种常见的 closure 引用对象的内存分析。

总的来说,分析 iOS 的内存管理重点在于引用计数,而引用计数在于有几个强引用,我们在分析内存时不要着急,要理清楚相互之间的引用关系,这样才能够正确分析每个对象的释放时机,也就掌握了每个对象的生命周期,更利于编写出我们想要的代码。

本文由星彩网app下载发布于计算机编程,转载请注明出处:机动引用计数,循环引用实例深入分析

TAG标签: 星彩网app下载
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。