Swfit进阶-15-Swift闭包的多个值捕获

966 阅读3分钟

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

1. defer

关于defer{}的定义:在函数执行完成后或者返回后,会执行defer中的函数,即使抛出异常的情况也会调用。 看个简单的列子:

func f() {

    defer { print("First defer") }

    defer { print("Second defer") }

    print("End of function")

}

f()

如果多个 defer 语句出现在同一作用域中,则它们出现的顺序与它们执行的顺序相反,也就是 先出现的后执行。 执行结果 image.png

  • return的情况
func test(){

    defer {

        print(#function)

    }

    guard false else{

        return

    }

}


test()

执行结果,如果把defer{}放在判断下面,直接return了,就不会执行了

image.png

我们对于一些统一的操作可以使用defer{},这样会使代码更加优雅。比如我们知道手动创建指针内存空间的时候,不用的时候要销毁,这个时候就可以使用defer

let count = 1

let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)

pointer.initialize(repeating: 0, count: count)


defer {

    pointer.deinitialize(count: count)

    pointer.deallocate()

}

或者网络请求的时候,可能有不同的分支进行回调函数的执行

func netRquest(completion: () -> Void) {

defer {

//self.isLoading = false

completion()

    

}

let requestStatus = false

    

guard requestStatus == true else {

    print("requestStatus 400")

    return

}

    

}

2. 捕获2个变量或多个变量

之前我们分析过了,关于闭包捕获一个变量的情况,那么如果闭包中是对象的话

class Person {

    var age = 10

}


func test() {

    

    let p = Person()

    

    let closure = {

        

        p.age += 1

    }

    closure()

}

test()

我们查看IR的代码 image.png

这里我们没有在堆区再次创建实例变量p,而是拿到p的地址存储到闭包中。这个过程不需要捕获实例变量p到堆区了。因此只需要在使用的时候进行引用类型地址的传递即可

那么多个情况下是什么样的情况

func makeIncrementer() -> (_ amount:Int) -> Int {


var runningTotal = 10

var  a = 1

func incrementer(_ amount:Int) -> Int {


runningTotal += 1

           a += 1

runningTotal += a

runningTotal += amount

return runningTotal


}

return incrementer



}

let makeInc = makeIncrementer()

print(makeInc(1))

我们添加2个变量,查看IR代码

image.png 在捕获多个值后,相对应的也多次调用了 swift_allocObject 方法。第一次和第二次都是存储变量Int类型的值。第三次的时候返回的实例被强制转换成了一个结构体指针:<{ %swift.refcounted, %swift.refcounted*, %swift.refcounted* }>*,之后通过getelementptr把创建的结构体也就是捕获的变量存储到 %13 这个结构体。

我们用结构体进行还原

func makeIncrementer() -> (_ amount: Int) -> Int{

    var runningTotal = 1

    var b = 2

    //内嵌函数,也是一个闭包

    func incrementer(_ amount: Int) -> Int{

        runningTotal += amount//21

        b += 2;//4

        runningTotal += b//25

        return runningTotal

    }

    return incrementer

}
//数据结构 : 闭包的执行地址  + 捕获变量堆空间的地址 
struct ClosureData<T>{         
var ptr: UnsafeRawPointer     
var object: UnsafePointer<T> 
}


//捕获值的box

struct TowParamsClosureData<T1, T2>{

    var object: HeapObject

    var value1: UnsafePointer<Box<T1>> //第一个变量

    var value2: UnsafePointer<Box<T2>>  //第二个变量

}
struct HeapObject{

    var metadata: UnsafeRawPointer

    var refcount1: Int32

    var refcount2: Int32

}


struct Box<T>{

    var object: HeapObject

    var value: T

}


struct NoMeanStruct{

    var f: (Int) -> Int

}
var f = NoMeanStruct(f: makeIncrementer())

f.f(20)


let ptr = UnsafeMutablePointer<NoMeanStruct>.allocate(capacity: 1)

ptr.initialize(to: f)


let ctx = ptr.withMemoryRebound(to: ClosureData<TowParamsClosureData<Int, Int>>.self, capacity: 1){

    $0.pointee

}


print(ctx.ptr)

print(ctx.capatureValue.pointee.value1.pointee.value)

print(ctx.capatureValue.pointee.value2.pointee.value)

打印结果

image.png

当我们捕获3个变量的时候,类似我们在box中添加value3,之后打印

image.png

这里的amount并不要捕获,这个相当于实例对象的属性,类似我们block中参数,不会造成引用计数增加

  • 改变参数的位置 我们把amount的位置改为外部的函数的参数不是闭包的函数

image.png

我们再改变结构体类型以及去掉闭包的类型

image.png

我们打印捕获的变量runningTotal和传入的参数100.

image.png

我们直接打印ctx

image.png

3. 总结

对于多个值进行捕获的时候,会把捕获的变量放到一个结构体的boxValues中,之后存储到闭包的结构体中。对于闭包的参数不会进行捕获,相当于闭包对象的属性。闭包不会捕获堆区的对象,只会引用堆区对象的地址