本文主要分析逃逸闭包 、非逃逸闭包、自动闭包
逃逸闭包 & 非逃逸闭包
逃逸闭包定义
当闭包作为一个实际参数传递给一个函数
时,并且是在函数返回之后调用
,我们就说这个闭包逃逸了。当声明一个接受闭包作为形式参数的函数时,可以在形式参数前写@escaping
来明确闭包是允许逃逸
的
- 如果
用@escaping
修饰闭包后,我们必须显示的在闭包中使用self
- swift3.0之后,系统默认闭包参数就是被
@nonescaping
,可以通过SIL来验证
- 1、执行时机:在函数体内执行
- 2、闭包生命周期:函数执行完之后,闭包也就消失了
逃逸闭包的两种调用情况
- 1、延迟调用
- 2、作为属性存储,在后面进行调用
1、作为属性
当闭包作为存储属性
时,主要有以下几点说明:
- 1、
定义
一个闭包属性
- 2、在
方法中对闭包属性进行赋值
- 3、在
合适的时机调用
(与业务逻辑相关)
如下所示,当前的complitionHandler
作为CJLTeacher
的属性,是在方法makeIncrementer
调用完成后才会调用,这时,闭包的生命周期要比当前方法的生命周期长
//*********1、闭包作为属性 class CJLTeacher { //定义一个闭包属性 var complitionHandler: ((Int)->Void)? //函数参数使用@escaping修饰,表示允许函数返回之后调用 func makeIncrementer(amount: Int, handler: @escaping (Int)->Void){ var runningTotal = 0 runningTotal += amount //赋值给属性 self.complitionHandler = handler } func doSomething(){ self.makeIncrementer(amount: 10) { print($0) } } deinit { print("CJLTeacher deinit") } } //使用 var t = CJLTeacher() t.doSomething() t.complitionHandler?(10) <!--打印结果--> 10
2、延迟调用
- 【延迟方法中使用】 1、在
延迟方法
中调用逃逸闭包
class CJLTeacher { //定义一个闭包属性 var complitionHandler: ((Int)->Void)? //函数参数使用@escaping修饰,表示允许函数返回之后调用 func makeIncrementer(amount: Int, handler: @escaping (Int)->Void){ var runningTotal = 0 runningTotal += amount //赋值给属性 self.complitionHandler = handler //延迟调用 DispatchQueue.global().asyncAfter(deadline: .now()+0.1) { print("逃逸闭包延迟执行") handler(runningTotal) } print("函数执行完了") } func doSomething(){ self.makeIncrementer(amount: 10) { print($0) } } deinit { print("CJLTeacher deinit") } } //使用 var t = CJLTeacher() t.doSomething() <!--打印结果--> 函数执行完了 逃逸闭包延迟执行 10
当前方法执行的过程中不会等待闭包执行完成后再执行,而是直接返回
,所以当前闭包的生命周期要比方法长
逃逸闭包 vs 非逃逸闭包 区别
- 非逃逸闭包:一个接受闭包作为参数的函数,闭包是在这个函数结束前内被调用,即可以理解为
闭包是在函数作用域结束前被调用
- 1、
不会产生循环引用
,因为闭包的作用域在函数作用域内,在函数执行完成后,就会释放闭包捕获的所有对象 - 2、针对非逃逸闭包,
编译器会做优化
:省略内存管理调用 - 3、非逃逸闭包捕获的上下文
保存在栈上
,而不是堆上(官方文档说明)。注:针对这点目前没有验证出来,有验证出来的童鞋欢迎留言解惑
- 逃逸闭包:一个接受闭包作为参数的函数,逃逸闭包可能会在函数返回之后才被调用,即
闭包逃离了函数的作用域
- 1、
可能会产生循环引用
,因为逃逸闭包中需要显式的引用self
(猜测其原因是为了提醒
开发者,这里可能会出现循环引用了),而self可能是持有闭包变量的(与OC中block的的循环引用类似) - 2、一般用于异步函数的返回,例如网络请求
- 使用建议:如果没有特别需要,开发中使用
非逃逸闭包是有利于内存优化
的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包
自动闭包
有下面一个例子,当condition
为true
时,会打印错误信息,即如果是false
,当前条件不会执行
//1、condition为false时,当前条件不会执行 func debugOutPrint(_ condition: Bool, _ message: String){ if condition { print("cjl_debug: \(message)") } } debugOutPrint(true, "Application Error Occured")
- 如果字符串是在某个业务逻辑中获取的,会出现什么情况?
func debugOutPrint(_ condition: Bool, _ message: String){ if condition { print("cjl_debug: \(message)") } } func doSomething() -> String{ print("doSomething") return "Network Error Occured" } <!--如果传入true--> debugOutPrint(true, doSomething()) //打印结果 doSomething cjl_debug: Network Error Occured <!--如果传入false--> debugOutPrint(false, doSomething()) //打印结果 doSomething
通过结果发现,无论是传入true还是false,当前的方法都会执行
,如果这个方法是一个非常耗时的操作,这里就会造成一定的资源浪费。所以为了避免这种情况,需要将当前参数修改为一个闭包
- 【修改】:将
message参数修改成一个闭包
,需要传入的是一个函数
//3、为了避免资源浪费,将当前参数修改成一个闭包 func debugOutPrint(_ condition: Bool, _ message: () -> String){ if condition { print("cjl_debug: \(message())") } } func doSomething() -> String{ print("doSomething") return "Network Error Occured" } debugOutPrint(true, doSomething)
修改后运行结果如下
如果此时传入一个string
,又需要如何处理呢?
可以通过@autoclosure
将当前的闭包声明成一个自动闭包
,不接收任何参数,返回值是当前内部表达式的值
。所以当传入一个String时,其实就是将String放入一个闭包表达式中,在调用的时候返回
//4、将当前参数修改成一个闭包,并使用@autoclosure声明成一个自动闭包 func debugOutPrint(_ condition: Bool, _ message: @autoclosure() -> String){ if condition { print("cjl_debug: \(message())") } } func doSomething() -> String{ print("doSomething") return "Network Error Occured" } <!--使用1:传入函数--> debugOutPrint(true, doSomething()) <!--使用2:传入字符串--> debugOutPrint(true, "Application Error Occured") <!--打印结果--> doSomething cjl_debug: Network Error Occured cjl_debug: Application Error Occured
自动闭包就相当于
debugOutPrint(true, "Application Error Occured") 相当于用{}包裹传入的对象,然后返回{}内的值 { //表达式里的值 return "Network Error Occured" }
总结
逃逸闭包
:一个接受闭包作为参数的函数,逃逸闭包可能会在函数返回之后才被调用,即闭包逃离了函数的作用域,例如网络请求,需要在形参前面使用@escaping来明确闭包是允许逃逸的。
- 一般
用于异步函数的回调
,比如网络请求 - 如果标记为了@escaping,必须在闭包中显式的引用self
非逃逸闭包
:一个接受闭包作为参数的函数,闭包是在这个函数结束前内被调用,即可以理解为闭包是在函数作用域结束前被调用- 为什么要区分
@escaping 和 @nonescaping
?
- 1、为了内存管理,
闭包会强引用它捕获的所有对象
,这样闭包会持有当前对象,容易导致循环引用
- 2、
非逃逸闭包不会产生循环引用
,它会在函数作用域内使用,编译器可以保证在函数结束时闭包会释放它捕获的所有对象 - 3、
使用非逃逸闭包可以使编译器应用更多强有力的性能优化
,例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用 - 4、
非逃逸闭包
它的上下文的内存可以保存在栈上
而不是堆上
- 总结:如果没有特别需要,
开发中使用非逃逸闭包是有利于内存优化
的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包