在Swift中,函数和闭包也是一种类型,这意味着函数和闭包可以被存储,可以用作函数的参数或返回值。而将闭包传入函数作为回调函数在Swift中是很常见的一种模式。
在Swift中,闭包的默认属性是Nonescaping,那Nonescaping和Escaping的闭包的区别是什么呢?
-
Nonescaping Closure
非逃逸闭包的生命周期比较简单
- 传入闭包
- 调用闭包(或者不调用)
- 函数返回 (闭包的生命周期和函数的生命周期一致)
非逃逸闭包的意思是:当函数调用结束时,闭包的引用计数和闭包被传入函数前的引用计数相等,即对闭包的引用和释放是平衡的。例如闭包被传入函数前的引用计数为0,那么函数调用结束后,闭包的引用计数也为0(函数调用过程中为1),引用计数为0的闭包就会被内存释放掉。
-
Escaping Closure
逃逸闭包的生命周期如下
- 传入闭包
- 调用闭包(或者不调用)
- 函数返回(逃逸闭包的生命周期未结束,即逃逸闭包的生命周期长比函数的生命周期长)
如何理解逃逸闭包的生命周期比函数调用的生命周期长呢?前面我们讲到一个引用计数为0的非逃逸闭包在函数调用结束后,其引用计数也为0,这时非逃逸闭包会被内存释放掉(生命周期结束)。而在闭包是可逃逸的情况下,一个引用计数为0的逃逸闭包在函数调用结束后,其引用计数可能为1(或者更多),即对逃逸闭包的引用和释放是不平衡的,所以该闭包还不会被内存释放掉(逃逸闭包的生命周期未结束)。
造成闭包逃逸的原因有哪些呢?
异步调用
当传入函数的闭包被放在其他的线程执行,且该线程晚于函数所在的线程被调度,这样就会造成虽然当前函数调用结束,但闭包的调用还未开始的现象(即逃逸,生命周期未结束且被其他实例引用)闭包被存储在全局变量中
当闭包在函数调用中被赋值给全局变量时,就算函数调用结束,闭包也还是被全局变量所引用,所以闭包的生命周期未结束且引用计数增减不平衡(即逃逸)-
使用Escaping的闭包时需要注意的点
使用 @escaping 标记可以逃逸的函数参数(闭包)
闭包会捕获 self,但编译器知道非逃逸闭包不会存在循环引用的现象,所以闭包中不需要使用 self. 来标注使用到的属性或方法(非逃逸闭包的生命周期与函数生命周期一致,所以非逃逸闭包对 self 的引用和 self 对非逃逸闭包的引用是同时断开的,不会存在循环引用的情况);但使用逃逸闭包时,会存在循环引用的现象,所以在闭包中要用 self. 显式地标注使用到的属性或方法,必要时用 weak 修饰捕获列表中的 self 来避免循环引用(因为闭包是逃逸的,所以闭包对 self 的引用不会因为函数调用结束而断开,所以存在循环引用的风险)
-
参考文献