swift 中解决循环引用的三种方法

2,034 阅读8分钟

和OC一样,swift也是使用自动引用计数ARC(Auto Reference Counteting)来自动管理内存的,所以我们不需要过多考虑内存管理.当某个类实例不需要用到的时候,ARC会自动释放其占用的内存.

ARC仅仅能对类的实例做内存管理,也就是只能针对引用类型.结构体和枚举都是值类型,不能通过引用的方式来传递和存储,所以ARC也就不能对它们进行内存管理.

什么情况下会导致循环引用

在swift中,每创建一个实例,ARC都会为其分配一块内存空间,而在不使用的时候,ARC会释放和收回那个实例所占的内存空间,该实例的属性和方法也就不能够被访问,如果要访问就会导致程序崩溃.
怎么确定实例不被使用了?ARC会自动追踪实例被多少常量和变量引用.每追踪到一个,自动引用计数会加一,减少一个引用自动引用计数会减一,如果当自动引用计数变为0的时候,ARC就会收回内存,销毁实例.
下面是一个自动引用计数的实例:

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name)正在被初始化")
    }
    deinit {
        print("\(name)即将被销毁")          // person3 = nil时打印
    }
}
var person1: Person?                      // 可选类型的变量,方便置空
var person2: Person?
var person3: Person?
person1 = Person(name: "Dariel")          //创建Person实例并与person1建立了强引用
person2 = person1                         // 只要有一个强引用在,实例就能不被销毁
person3 = person1                         // 目前该实例共有三个强引用

person1 = nil
person2 = nil                             // 因为还有一个强引用,实例不会被销毁
person3 = nil                             // 最后一个强引用被断开,ARC会销毁该实例

上面的例子中创建的Person实例最后引用计数变为了0被销毁了,但现实世界并不会一直都这么美好, ARC这种机制也有自己的局限性,请看下面的例子:

class People {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?              // 人住的公寓属性
deinit {
        print("People被销毁")
    }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: People?                   // 公寓中的人的属性
    deinit {
        print("Apartment被销毁")
    }
}

var people1: People? = People(name: "Dariel")  // 定义两个实例变量
var apartment1: Apartment? = Apartment(unit: "4A")

people1!.apartment = apartment1           // 两者相互引用
apartment1?.tenant = people1              // 而且彼此都是强引用

people1 = nil
apartment1 = nil                          // 两个引用都置为nil了,但实例并没有销毁

这一次直接创建了两个实例,People中有一个Apartment的属性,Apartment中又有一个People属性,当我们创建了两个实例后分别给实例中的这两个属性赋完值,又将两个可选变量赋值为nil,并没有看到两个实例被销毁的打印信息(deinit函数会在实例被销毁的时候打印).

也就是说ARC并没有销毁两个对象.那么问题在哪里?
当两个可选变量被赋值为nil时,ARC并没有觉得这两个实例已经不在使用了.因为两个实例的相互赋值时使得各自的引用计数+1,这也就是发生循环引用了.

怎么解决循环引用

1. 如果产生循环引用的两个属性都允许为nil,这种情况适合用弱引用来解决

随便哪一个可选类型的属性前面都可以加weak,但记住只要加一个就行了.
话不多说上代码:

class OtherPeople {
    let name: String
    init(name: String) { self.name = name }
    var apartment: OtherApartment?        // 人住的公寓属性
    deinit { print("\(name)被销毁") }
}

class OtherApartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: OtherPeople?         // 加一个weak关键字,表示该变量为弱引用
    deinit { print("\(unit)被销毁") }
}

var otherPeople1: OtherPeople? = OtherPeople(name: "Dariel") // 定义两个实例变量
var otherApartment1: OtherApartment? = OtherApartment(unit: "4A")

otherPeople1!.apartment = otherApartment1 // 两者相互引用
otherApartment1?.tenant = otherPeople1    // 但tenant是弱引用
otherPeople1 = nil
otherApartment1 = nil                     // 实例被销毁,deinit中都会打印销毁的信息

OtherPeopleOtherApartment两个类中,相互引用的两个属性都为可选类型,那么可以在一个属性的前面添加weak关键字,使该变量变为弱引用.
对的,没错,这个weak还是以前OC里面的那个weak.

2. 如果产生循环引用的两个属性一个允许为nil,另一个不允许为nil,这种情况适合用无主引用来解决

只能在不能为nil的那个属性前面加unowned关键字,就是说 unowned设置以后即使它原来引用的内容已经被释放了,它仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不能是 Optional 值,也不会被指向 nil。如果尝试去调用这个引用的方法或者访问成员属性的话,程序就会崩溃.
无主引用的例子:

class Dog {
    let name: String
    var food: Food?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name)被销毁") }
}
class Food {
    let number: Int
    unowned var owner: Dog               // owner是一个无主引用
    init(number: Int, owner: Dog) {
        self.number = number
        self.owner = owner
    }
    deinit { print("食物被销毁") }
}

var dog1: Dog? = Dog(name: "Kate")
dog1?.food = Food(number: 6, owner: dog1!) // dog强引用food,而food对dog是无主引用

dog1 = nil                                 // 这样就可以同时销毁两个实例了

Dogfood属性可以为空,而Foodowner属性不能为空,我们把owner设为无主引用.

3. 如果产生循环引用的两个属性都必须有值,不能为nil,这种情况适合一个类使用无主属性,另一个类使用隐式解析可选类型

隐式解析可选类型: 类似可选类型,默认值可以设置为nil
两个属性一个在类型后面加!设置为隐式解析可选类型,另一个在属性前面加unowned关键字,设置为无主属性.

class Country {
    let name: String
    var capitalCity: City!                // 初始化完成后可以当非可选类型使用
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
    deinit { print("Country实例被销毁") }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
    deinit { print("City实例被销毁") }
}

// 这样一条语句就能够创建两个实例
var country: Country? = Country(name: "China", capitalName: "HangZhou")
print(country!.name)                        // China
print(country!.capitalCity.name)            // HangZhou
country = nil                               // 同时销毁两个实例

CountryCity属性后加!为隐式解析可选属性,类似可选类型,capitalCity属性的默认值为nil,一旦在Country的构造函数中给name属性赋完值后,Country的整个初始化过程就完成了,就能将self作为参数传递给City的构造函数了.
总而言之,就是一条语句创建两个实例,还不产生循环引用.

闭包也是引用类型,怎么解决闭包的循环强引用

闭包中对任何其他元素的引用都是会被闭包自动持有的。如果我们在闭包中写了 self 这样的东西的话,那我们其实也就在闭包内持有了当前的对象。这里就出现了一个在实际开发中比较隐蔽的陷阱:如果当前的实例直接或者间接地对这个闭包又有引用的话,就形成了一个 self -> 闭包 -> self 的循环引用

怎样避免这种情况呢?
可以在闭包开始的时候添加一个标注,来表示这个闭包内的某些要素应该以何种特定的方式来使用
看例子:

class Element {
    let name: String
    let text: String?

    lazy var group:() -> String = {        // 相当于一个没有参数返回string的函数
        [unowned self] in                   // 定义捕获列表,将self变为无主引用
        if let text = self.text {           // 解包
            return "\(self.name), \(text)"
        }else {
            return "\(self.name)"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    deinit { print("\(name)被销毁") }
}

var element1: Element? = Element(name: "Alex", text: "Hello")
print(element1!.group())                     // Alex, Hello,闭包与实例相互引用

element1 = nil                               // self为无主引用,实例能被销毁

在闭包中定义一个捕获列表[unowned self],将self变为无主引用.这样就能够在避免产生循环强引用了.

小结

解决循环引用的三种方法,这三种方法的产生主要还是swift中要考虑属性为空的情况.

  • 如果产生循环引用的两个属性都允许为nil,这种情况适合用弱引用来解决.
  • 如果产生循环引用的两个属性一个允许为nil,另一个不允许为nil,这种情况适合用无主引用来解决.
  • 如果产生循环引用的两个属性都必须有值,不能为nil,这种情况适合一个类使用无主属性,另一个类使用隐式解析可选类型 .

在闭包的循环引用中通过自定义捕获列表来避免产生循环强引用.

如果嫌看文章麻烦的童鞋,可以直接看代码.代码最直接了.
或者也可以看这个Swift3.0语法速查手册,基本会覆盖所有的swift语法点,目前还在努力更新中...

参考

喵神的内存管理,WEAK 和 UNOWNED
官方文档The Swift Programming Language的Language Guides部分的Automatic Reference Counting