Swift进阶09:闭包(二)

第九节课:闭包(二)

闭包补充

上节课我们看了捕获一个变量的内存结构,如果捕获的是两个变量的值,当前内存结构是什么玩意?

func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 12
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

查看其IR代码


捕获两个变量IR分析-返回值.png

返回值仍然是void* ,swift.refcounted指针,所以原来的仿写代码中FuntionData<T>是不变的。

接下来我们往上看

%12 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementer12forIncrementSiycSi_tF11incrementerL_SiyFTA" to i8*), %swift.refcounted* undef }, %swift.refcounted* %8, 1

首先insertvalue是往{ i8*, %swift.refcounted* }这个结构体里面插入
将内嵌函数的地址放到了i8*里面,也就是void*内存中
%swift.refcounted* %8指针插入index=1的位置

接下来我们来看%8是个什么

%8 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata.3, i32 0, i32 2), 
i64 32, 
i64 7) #2

首先swift_allocObject开辟堆区内存空间
getelementptr是从swift.full_boxmetadata中偏移i32 0,即不偏移,取结构体
然后从swift.full_boxmetadata* @metadata.3中找出index=2的元素地址,也就是%swift.type字段
i64 32,i64 7 分配的内存大小是32字节,然后8字节对齐

内部结构仿写

根据捕获一个变量的仿写,继续仿写捕获两个变量的情况

//2、闭包捕获多个值的原理
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函数返回值结构体
//BoxType 是一个泛型,最终是由传入的Box决定的
struct FunctionData<BoxType>{
    var ptr: UnsafeRawPointer//内嵌函数地址
    var captureValue: UnsafePointer<BoxType>
}

//捕获值的结构体
struct Box<T> {
    var refCounted: HeapObject
    var value: T
}

//封装闭包的结构体,目的是为了使返回值不受影响
struct VoidIntFun {
    var f: () ->Int
}

//下面代码的打印结果是什么?
func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 0
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
var makeInc = makeIncrementer(forIncrement: 10)
var f = VoidIntFun(f: makeInc)

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的内存空间
ptr.initialize(to: f)
//将ptr重新绑定内存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue)

<--打印结果-->
0x0000000100005840
0x0000000102014cd0

通过cat 查看第一个地址,即内嵌函数地址


cat查看内嵌函数地址.png

x/8g 第二个地址

内嵌函数地址内存.png

再次x/8g查看

第二次x:8g.png

发现这个值跟我们的runningTotal有点像哎~那就验证一下
runningTotal改成10

验证.png

所以,闭包捕获两个变量时,Box结构体内部发生了变化,修改后的仿写代码如下:

//2、闭包捕获多个值的原理
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函数返回值结构体
//BoxType 是一个泛型,最终是由传入的Box决定的
struct FunctionData<BoxType>{
    var ptr: UnsafeRawPointer//内嵌函数地址
    var captureValue: UnsafePointer<BoxType>
}

//捕获值的结构体
struct Box<T> {
    var refCounted: HeapObject
    //valueBox用于存储Box类型
    var valueBox: UnsafeRawPointer
    var value: T
}

//封装闭包的结构体,目的是为了使返回值不受影响
struct VoidIntFun {
    var f: () ->Int
}

//下面代码的打印结果是什么?
func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 12
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

var makeInc = makeIncrementer(forIncrement: 10)
var f = VoidIntFun(f: makeInc)

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的内存空间
ptr.initialize(to: f)
//将ptr重新绑定内存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int, Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee)
print(ctx.captureValue.pointee.valueBox)

<!--打印结果-->
0x0000000100002b30
Box<Int>(refCounted: _7_Clourse.HeapObject(type: 0x0000000100004090, refCount1: 3, refCount2: 4), valueBox: 0x00000001006094a0, value: 10)
0x00000001006094a0

总结:
1.捕获值的原理:堆上开辟内存空间,捕获的值放到内存空间里
2.修改捕获值的时候修改堆空间里面的值(11,12,13的例子)
3.闭包是一个引用类型(地址传递),闭包的底层结构(结构体:函数的地址+捕获变量的值 == 闭包)

逃逸闭包&非逃逸闭包

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

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

简单写一个例子

func test(by:()->()){
    by()
}

查看SIL


SIL分析.png

非逃逸闭包

  • 函数体内执行
  • 函数执行完之后,闭包消失

逃逸闭包

  • 函数返回后调用
  • 延迟调用
  • 作为属性存储,后面进行调用

例子:
属性存储

class HZMTeacher{

    var complitionHandler: ((Int)->Void)?

    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("HZMTeaher deinit")
    }

}

var t = HZMTeacher()

t.doSomething()

t.complitionHandler?(10)

如上所示,当前的complitionHandler作为HZMTeacher的属性,是在方法makeIncrementer调用完成后才会调用,这时,闭包的生命周期要比当前方法的生命周期长

延迟调用

class HZMTeacher {
    //定义一个闭包属性
    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("HZMTeacher deinit")
    }
}
//使用
var t = HZMTeacher()
t.doSomething()

<--打印结果-->
函数执行完了
逃逸闭包延迟执行
10

当前方法执行的过程中不会等待闭包执行完成后再执行,而是直接返回,所以当前闭包的生命周期要比方法长

逃逸闭包与非逃逸闭包的区别

非逃逸闭包:一个接受闭包作为参数的函数,闭包是在这个函数结束前内被调用,即可以理解为闭包是在函数作用域结束前被调用

  • 不会产生循环引用(函数调用完成后释放捕获对象)
  • 编译器优化(省略的内存管理调用,return、release)
  • 非逃逸闭包可以保存在栈上,而不是堆上(官方文档说明,目前没有验证出来)

逃逸闭包:一个接受闭包作为参数的函数,逃逸闭包可能会在函数返回之后才被调用,即闭包逃离了函数的作用域(生命周期比函数长)

  • 可能会产生循环引用(因为逃逸闭包中需要显式的引用self(猜测其原因是为了提醒开发者,这里可能会出现循环引用了),而self可能是持有闭包变量的(与OC中block的的循环引用类似))
  • 一般用于异步函数返回,例如网络请求

使用建议:如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包

自动闭包

先看一个例子

func debugOutPrint(_ condition: Bool, _ message: String){
    if condition {
        print("debug: \(message)")
    }
}
debugOutPrint(true, "Application Error Occured")

conditiontrue时,会打印错误信息,即如果是false,当前条件不会执行

如果字符串是在某个业务逻辑中获取的,会怎么样?

func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}

debugOutPrint(true, doSomething())
debugOutPrint(false, doSomething())

通过结果发现,传入true还是false,当前的doSomething()都会执行,如果这个方法是一个非常耗时的操作,这里就会造成一定的资源浪费。所以为了避免这种情况,需要将当前参数修改为一个闭包

func debugOutPrint(_ condition: Bool, _ message: () -> String){
    if condition {
        print("cjl_debug: \(message())")
    }
}
func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}
debugOutPrint(false, doSomething)

这样,我们的doSomething方法就不会执行了

问题来了,如果就是要传一个字符串怎么办,有没有兼容的方式?
可以通过@autoclosure将当前的闭包声明成一个自动闭包,不接收任何参数,返回值是当前内部表达式的值。所以当传入一个String时,其实就是将String放入一个闭包表达式中,在调用的时候返回

func debugOutPrint(_ condition: Bool, _ message: @autoclosure() -> String){
    if condition {
        print("debug: \(message())")
    }
}
func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}
debugOutPrint(true, doSomething())

debugOutPrint(true, "Application Error Occured")

这样就兼容了两种参数形式

debugOutPrint(true, "Network Error Occured")
自动闭包相当于用{}包裹传入的对象,然后返回{}内的值
{
    //表达式里的值
    return "Network Error Occured"
}

总结:

逃逸闭包:一个接受闭包作为参数的函数,逃逸闭包可能会在函数返回之后才被调用,即闭包逃离了函数的作用域(生命周期比函数长)

  • 可能会产生循环引用(因为逃逸闭包中需要显式的引用self(猜测其原因是为了提醒开发者,这里可能会出现循环引用了),而self可能是持有闭包变量的(与OC中block的的循环引用类似))
  • 一般用于异步函数返回,例如网络请求
  • 如果标记为了@escaping,必须在闭包中显式的引用self

非逃逸闭包:一个接受闭包作为参数的函数,闭包是在这个函数结束前内被调用,即可以理解为闭包是在函数作用域结束前被调用

  • 不会产生循环引用(函数调用完成后释放捕获对象)
  • 编译器优化(省略的内存管理调用,return、release)
  • 非逃逸闭包可以保存在栈上,而不是堆上(官方文档说明,目前没有验证出来)

为什么要区分@escaping 和 @nonescaping?
1、为了内存管理,闭包会强引用它捕获的所有对象,这样闭包会持有当前对象,容易导致循环引用

2、非逃逸闭包不会产生循环引用,它会在函数作用域内使用,编译器可以保证在函数结束时闭包会释放它捕获的所有对象

3、使用非逃逸闭包可以使编译器应用更多强有力的性能优化,例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用

4、非逃逸闭包它的上下文的内存可以保存在栈上而不是堆上

PS: 如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包

自动闭包相当于用{}包裹传入的对象,然后返回{}内的值

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容