Swift-进阶 09:闭包(二)逃逸闭包 & 非逃逸闭包

简介: Swift-进阶 09:闭包(二)逃逸闭包 & 非逃逸闭包

本文主要分析逃逸闭包 、非逃逸闭包、自动闭包


逃逸闭包 & 非逃逸闭包


逃逸闭包定义


闭包作为一个实际参数传递给一个函数时,并且是在函数返回之后调用,我们就说这个闭包逃逸了。当声明一个接受闭包作为形式参数的函数时,可以在形式参数前写@escaping明确闭包是允许逃逸


  • 如果用@escaping修饰闭包后,我们必须显示的在闭包中使用self
  • swift3.0之后,系统默认闭包参数就是被@nonescaping,可以通过SIL来验证

image.png

  • 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、一般用于异步函数的返回,例如网络请求
  • 使用建议:如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包


自动闭包


有下面一个例子,当conditiontrue时,会打印错误信息,即如果是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)

修改后运行结果如下


image.png

如果此时传入一个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、非逃逸闭包它的上下文的内存可以保存在栈上而不是堆上
  • 总结:如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包


相关文章
|
2月前
|
Swift UED
Swift 中使用闭包处理事件
【10月更文挑战第20天】使用闭包来处理事件在 Swift 编程中是一种非常常见和有效的方式。它提供了简洁、灵活和高效的解决方案,使我们能够更轻松地实现各种交互功能。通过深入理解闭包的原理和应用方法,我们可以更好地利用闭包来处理各种事件,提升应用的用户体验和开发效率。
|
24天前
|
存储 Swift iOS开发
Swift 闭包
10月更文挑战第28天
20 0
|
7月前
|
存储 API Swift
【Swift开发专栏】Swift函数与闭包的实战应用
【4月更文挑战第30天】本文介绍了 Swift 中函数和闭包的实战应用。首先,函数的基本使用包括定义、参数与返回值、函数类型以及高级技巧如嵌套函数。接着,讨论了闭包的语法,包括无名函数、作为函数参数、简写形式和尾随闭包。最后,展示了函数和闭包在实战中的应用,如排序过滤集合和处理异步任务的回调。
51 0
|
7月前
|
存储 Swift
Swift 语言:什么是闭包(Closure)?它们与函数的区别是什么?
Swift 语言:什么是闭包(Closure)?它们与函数的区别是什么?
136 1
|
7月前
|
存储 Swift
Swift中,函数和闭包
Swift中,函数和闭包
76 1
|
Swift
swift 闭包简写实际参数名$0、$1等理解
swift 闭包简写实际参数名$0、$1等理解
108 0
|
存储 编译器 Swift
Swift-进阶 08:枚举enum
Swift-进阶 08:枚举enum
625 1
Swift-进阶 08:枚举enum
|
存储 编译器 Swift
Swift5.0 - day4-闭包、属性、方法、下标
Swift5.0 - day4-闭包、属性、方法、下标
327 0
Swift5.0 - day4-闭包、属性、方法、下标
|
Swift
Swift5.1—闭包的循环强引用
Swift5.1—闭包的循环强引用
230 0
Swift5.1—闭包的循环强引用
|
数据安全/隐私保护 Swift
Swift-进阶 11:访问控制权限
Swift-进阶 11:访问控制权限
321 0
Swift-进阶 11:访问控制权限