ARC简介
Swift使用ARC(自动引用计数)来跟踪和管理应用程序的内存使用情况。大多数情况下,您不需要自己考虑内存管理。当不在需要类实例时,ARC会自动释放类实例使用的内存。结构体和枚举是值类型,而不是引用类型,他们不是通过引用传递和存储,所以ARC不会管理他们。
ARC的工作原理
我们每次创建一个类的实例时,ARC都会分配一块内存来存储有关该实例的信息。此内存保存有关实例类型的信息,以及与该实例关联的任何存储属性的值。当不在需要某个实例时,ARC会释放该实例使用的内存,以便将内存用于其他目的,这确保了类实例在不再需要时不会占用内存。但是,如果ARC取消分配仍在使用的实例,则无法在访问该实例的属性或调用该实例的方法,如果你尝试访问该实例,那么你的应用程序很可能会奔溃。
为了确保实例在仍然需要时不会消失,ARC会跟踪当前引用每个类实例的属性、常量和变量的数量,只要该实例的至少一个活动引用仍然存在(至少有一个对该实例有强引用),ARC就不会释放该实例。下面我们来看一个自动引用计数的示例:
从上图看到我们定义了三个Person对象,但是他们都是可选类型,所以它们在内存总并不存在,他们的值也是nil,并且也不会引用Person实例
从上图看出,消息是在调用类的初始化时打印的,这说明初始化已经发生。当我们把Person(name: "张三")赋值给person1后,person1不再为nil,person1这时对Person有了一个强引用,ARC确保它保存在内存中并且不会被释放。
当我们把person1赋值给了person2和person3,这两个实例没有被初始化,只是它们强引用了和person1同样的Person实例。当我们把person1和person2这两个实例都置为nil之后,deinit函数并不会被执行,因为ARC知道还有person3在引用着person1。所以该实例仍然不会被释放。
当我们把person3也置为nil时,person1才会被释放,并调用deinit函数,打印张三 被释放
类实例之间的强引用循环
在上面的实例中,ARC能够追踪对你创建的新实例的引用数量,并在不需要该实例时释放该实例。但是,在编写代码中,其中类的实例不会达到它没有一个强引用的程度。如果两个类相互持有强引用,则可能会发生这种情况,这样每个实例都会使另一个实例保持活动状态,这称为强参考循环。下面,我们来了解下这种循环是怎么产生的:
每个Person实例都有一个name属性和一个apartment的可选属性,它最初为nil,该apartment属性是可选的,因为一个人可能并不总是有公寓的。类似,每个Apartment实例都有一个unit属性和一个tenant的可选属性,它最初为nil,该tenant属性是可选的,因为公寓可能并不总是有租户。
上图中,我们定义了john和unit4A的可选类型变量,并创建了一个特定的Person实例和Apartment实例,将这些实例分配给john和unit4A变量。
下图中是它们的引用关系,
我们将这两个实例链接在一起,以便人拥有公寓,公寓拥有人。
john!.apartment = unit4A
unit4A!.tenant = john
现在它们的引用关系变为下图:
当我们把两个实例置为nil的时候,ARC会把他们的引用计数置为0,这是没有错的,但是控制台却没有打印任何释放实例的信息?
此时我们在看它们的引用情况:
我们发现他们自已已经移除了对Person和Apartment的互相引用,但是ARC依然在追踪他们的属性的引用计数,Person实例和Apartment实例之间的强引用仍然存在,不能被破坏。
解决循环引用的两种方案
当您使用类类型的属性时,Swift 提供了两种解决强引用循环的方法:弱引用(weak)和无主引用(unowned)。
弱引用
为了打破强引用循环,可以将相互引用的实例之间的关系设置为弱(weak)
基本情况下,所有的引用都是强引用,会影响引用计数。弱引用则不会增加对象的引用计数,另外:弱引用永远都要声明为可选类型,因此必须使用 var 声明;当引用数变为0时,引用会自动的设置为 nil
无主引用
与弱引用一样,无主引用不会对其所引用的实例保持强引用。
unowned 和 weak 有什么区别呢?
弱引用必须为可选类型,当引用的对象不存在时,自动设置为 nil;
无主引用则不能为可选类型,如果你引用一个已经被销毁的对象属性,会抛出错误。
由上图可以看出,我们将apartment和tenant属性用weak修饰后,它们就会正常被释放。它们的关系图是:
闭包的循环引用(Closure/Block)
我们在上面看到了当两个类实例属性相互持有强引用时如何创建强引用循环,还看到了如何使用弱引用和无主引用来打破这些强引用循环。而我们将闭包分配给类实例的属性,并且该闭包的主体捕获该实例,也会发生强引用循环。
之所以会出现这种强引用循环,是因为闭包和类一样,都是引用类型。下面我们来看Swift是如何发生这种问题,以及应该怎样去解决这样的问题?
在HTMLElement我们定义了三个属性,其中asHTML是一个懒加载的闭包属性,并且返回()-String的一个函数。我们想要打印出来像这种样式的html语句:<h1>Hello World!</h1>,我们来写下
我们为heading.asHTML赋值,并且打印他,发现h1已经被释放,因为它并没有走asHTML的存储属性的get函数,所以没有捕获到self,因此并不会造成循环引用。下面我们来从新改下:
我们发现即使将实例paragraph置为nil,该实例依然没有被释放。是因为该闭包已经造成了一个循环引用。引用关系如下:
实例的asHTML属性持有对其闭包的强引用。但是,由于闭包self在其主体内引用,因此闭包会捕获self,这意味着它持有对HTMLElement实例的强引用。两者之间形成了一个循环引用。
解决闭包的循环引用
我们知道了闭包造成的循环引用,我们依然需要用weak或unwoned来打破循环引用,我们知道weak是当被修饰的属性被置为nil之后,ARC就会帮助我们销毁该引用,并将retainCount记为0。然而我们的闭包持有了self,而self又持有了闭包,所以self是不可能为nil的,如果为nil,那我们调用该asHTML将会永远crash,所以我们最合适的方式是使用unowned。
由上图我们可以看出,该实例被正常释放。那么它们之间的引用关系就变为了如下图所示:
这样我们也就解决了闭包的循环引用。