Swift 循环引用

1、循环引用

  • 如果两个类互相持有对方的强引用,就会出现循环引用的情况。

  • Swift 提供了两种解决循环引用的方法,弱引用和无主引用

1.1 循环引用示例

  • 比如存在一个学生类,一个班级类,学生类有一个属性叫学生所属的班级,而班级有一个属性是班长,它引用了一个学生类,它们都是可选型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Student {
    var name: String?
    var theClass: Class?

    deinit {
    print("Student deinit")
    }
    }

    class Class {
    var name: String?
    var classMonitor: Student?

    deinit {
    print("Class deinit")
    }
    }
  • 接着我们构造一个学生类实例小明和一个班级类实例三年二班,为了能够销毁对象,把对象声明为可选型,这样在我们不需要对象的时候就可以给它赋值 nil

    1
    2
    3
    4
    5
    6
    7
    8
    var sneb: Class? = Class()
    sneb?.name = "sannianerban"

    var xiaoming: Student? = Student()
    xiaoming?.name = "xiaoming"

    sneb?.classMonitor = xiaoming
    xiaoming?.theClass = sneb
  • 这时麻烦出现了,根据 ARC 规则,xiaomingsneb 之间互相强引用对方,出现强引用。

  • 在使用完 snebxiaoming 这两个对象后,后面的代码再也不会用到 snebxiaoming 了,此时尝试把这两个变量置为 nil

    1
    2
    sneb = nil
    xiaoming = nil
  • 但是你会发现控制台没有任何打印信息,没有任何一个析构器被调用,这是因为 snebxiaoming 之间通过属性的方式强引用了对方,二者的引用计数都为 1,ARC 根据规则阻止了实例被销毁,从而造成了内存泄漏。

2、闭包循环引用

  • 由于闭包也是引用类型的,闭包会持有内部的对象,当把闭包作为类的属性的时候,闭包与类之间也会出现循环引用的情况。

    • 在一个闭包中使用其外层的类的属性时,编译器会强制你加上 self 关键字,以此作为 “持有” 的提醒。
    • 将闭包类型的属性声明为 lazy 是因为通常声明一个闭包类型的属性是为了缓存某些操作,这些操作不一定会被执行,所以使用懒加载的方法提高运行效率,表示当闭包被调用的时候再进行初始化。
  • 要解决闭包引起的循环引用,Swift 中引入了 “捕获列表” 的概念,在闭包的参数列表中将闭包体中涉及的所有被 “持有” 对象声明为 “无主引用” 或者 “弱引用”,以逗号隔开。

  • 将闭包作为类的属性是一种非常常见的做法,相比于定义一个方法,定义一个闭包类型的属性可以随时修改闭包中的执行内容,以此达到复用的效果,减少一个类中的代码量。同时闭包中的代码可以 “跨越” 不同的类型,替代传统的 delegate 用法。

  • 相比于把闭包当作存储属性这样有点新奇的用法,闭包最常见的用途还是当作方法的参数,尤其是作为方法的最后一个参数。当闭包作为方法参数的时候,即便闭包中会持有 self,也不会引起循环引用。

2.1 闭包循环引用示例

  • 实例代码如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Example {

    var num = 10

    lazy var method: (Int) -> Int = { (i: Int) in

    return self.num + i
    }

    deinit {
    print("Example deinit")
    }
    }
  • method 是一个闭包类型的参数,在类的定义中被初始化为与类的属性 num 进行加法运算。

    1
    2
    3
    4
    var example: Example? = Example()
    let _ = example?.method(3)

    example = nil
  • 按照上面的方法使用 Example,会发现析构器没有执行,与基本类型的属性一样,Example 类对 method 闭包是强引用的,同时闭包的定义中又需要 num 属性参与运算,此时闭包会通过 self 属性 “持有” 类本身,这样产生了一个循环引用。

  • Example 中引入捕获列表后代码如下,此时 Example 的循环引用就被打破了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Example {

    var num = 10

    lazy var method: (Int) -> Int = { [unowned self] (i: Int) in

    return self.num + i
    }

    deinit {
    print("Example deinit")
    }
    }
  • 捕获列表除了可以设置 self,还可以单独声明引用类型成员。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Example {

    var delegate: UITableViewDelegate?

    lazy var method: (Int) -> Int = { [weak delegate = self.delegate] (i: Int) in

    // 操作 delegate
    }
    }
文章目录
  1. 1. 1、循环引用
    1. 1.1. 1.1 循环引用示例
  2. 2. 2、闭包循环引用
    1. 2.1. 2.1 闭包循环引用示例
隐藏目录