Swift(十八)-闭包的循环引用

5,662 阅读3分钟

「这是我参与2022首次更文挑战的第26天,活动详情查看:2022首次更文挑战

我们在弱引用与无主引用中讲过两个类实例之间的相互引用,如果在一个类中,其有属性是闭包,则也可能产生属性之间的相互引用;

闭包是一种特殊的语法结构,在其中使用的引用类型的实例都会使引用计数加1。因此,如果在闭包属性中使用self关键字,就会对当前类实例本身进行引用计数加1。由于闭包是当前类的一个属性,闭包属性无法销毁,当前类实例也就无法销毁。反过来,当前类实例无法销毁,闭包属性也无法销毁。如此产生循环引用,将造成内存泄漏。

闭包的使用

我们先来看一段简单的代码:

image.png

我们在闭包内部对num进行了修改,而最终打印的结果显示,外部的num值也发生了变化;

闭包内部变量的修改会改变外部原始变量的值;

那么,我们在闭包中捕获类的属性,是否会导致无法被销毁呢?

image.png

如上代码所示,我们在闭包closure中捕获了age属性,但是p依然能够被正常的释放销毁;

闭包属性

接下来,我们在Person中定义一个闭包属性,代码如下:

image.png

我们在Person中定义了一个闭包closure,在闭包中使用了self.age,此时,即使我们将p置为nil,其依然无法被销毁;也即是p的引用计数不为0;那么如何解决呢?

为了解决这种情况下产生的循环引用,Swift语言为闭包结构提供了捕获列表,用来对闭包内使用到的变量或者实例进行弱引用或者无主引用的转换。捕获列表在结构上需要紧跟在闭包起始大括号后使用中括号包围,我们将代码修改如下:

image.png

在使用[weak self]将其转换为弱引用关系之后,最终实例被释放;当然,使用unowned也能达到同样的效果:

image.png

捕获列表

那么什么是捕获列表呢?我们通过下边一段代码来分析一下:

image.png

我们定义了两个变量widthheight,在闭包中定义了一个width的捕获列表,我们在外部修改了widthheight之后,调用闭包时发现,只有height的值发生了改变,而width没有被修改;那么,捕获列表究竟对width变量做了什么操作呢?

我们声明了width捕获列表之后,那么在程序运行的过程中,编译器就会在程序运行的上下文中,查找与之同名的变量或者常量,找到之后,就会使用当前变量字面量的值来初始化age(捕获列表中的age),所以我们在闭包中使用width的时候,他其实还是10;外部无论怎么修改width的值,闭包内部始终是不变的,闭包内部的width的值,取决于与捕获列表中的width同名的变量字面量的值

我们可以将[width]捕获列表中的width当做一个常量来看待,这个常量的值,就是与之同名的变量或者常量字面量的值捕获列表中的width与外部的变量width是两个完全不同的东西;捕获的时机发生在闭包执行的时候;

如果捕获列表中需要对多个引用类型的值进行转换,可以使用逗号进行分隔;

image.png

小知识

闭包内部,可以使用withExtendedLifetime函数来延长某一个可选值的生命周期:

image.png

需要注意的是,延长的范围就在withExtendedLifetime闭包表达式范围之内;