Swift进阶(八)—— 闭包

函数类型

在Swift中,函数和其他数据类型拥有一样的地位,函数不仅可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。 所以函数也有自己的类型,当我们使用函数作为变量的时候,如果有同名函数,那么当你指定函数类型的时候,编译器就不会报错。

截屏2022-03-26 下午4.56.25.png

当函数赋值给一个变量时,我们来看一下这个变量里面存储了什么? 截屏2022-03-26 下午11.11.01.png 可以看到,函数类型也是一个引用类型,而且和其它数据结构一样,也有自己的Metadata。我们可以从源码里面去探个究竟。 截屏2022-03-27 下午11.12.34.png 通过源码我们可以知道,TargetFunctionTypeMetadata继承自 TargetMetadata,因此,会有一个Kind属性。于此同时,它还有一个Flags属性和用来标识返回值类型的ResultType属性。另外,这个类里面还有一个连续的内存数组空间用来存放参数列表。可以看到,参数列表里面存放的都是TargetMetadata类型。也就是说,函数里面的参数类型和返回值类型是Any.Type类型。

接下来我们来看下TargetFunctionTypeFlags类,这是存储函数类型的标志位的类。可以在这个类里面获取到函数参数个数。

class TargetFunctionTypeFlags {

enum : int_type {
    NumParametersMask = 0x0000FFFFU,
    ConventionMask = 0x00FF0000U,
    ConventionShift = 16U,
    ThrowsMask = 0x01000000U,
    ParamFlagsMask = 0x02000000U,
    EscapingMask = 0x04000000U,
    DifferentiableMask = 0x08000000U,
    GlobalActorMask = 0x10000000U,
    AsyncMask = 0x20000000U,
    SendableMask = 0x40000000U,
// NOTE: The next bit will need to introduce a separate flags word.

};

    int_type Data;
public:
unsigned getNumParameters() const { return Data & NumParametersMask; }
复制代码

从上面的代码我们可以得到这个TargetFunctionTypeMetadata的结构

struct TargetFunctionTypeMetadata {
    var kind: Int
    var flags: Int
    var resultType: Any.Type
    var arguments:ArgumentsBuffer<Any.Type>
    
    func numberArguments() -> Int {
        return self.flags & 0x0000FFFF
    }
}

struct ArgumentsBuffer<Element>{
    var element: Element
    
    mutating func buffer(n: Int) -> UnsafeBufferPointer<Element> {
        return withUnsafePointer(to: &self) {
            let ptr = $0.withMemoryRebound(to: Element.self, capacity: 1) { start in
                return start
            }

            return UnsafeBufferPointer(start: ptr, count: n)
        }
    }    

    mutating func index(of i: Int) -> UnsafeMutablePointer<Element> {
        return withUnsafePointer(to: &self {
            return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: Element.self).advanced(by: i))
        }
    }
}
复制代码

接下来,我们根据上面的数据结构来获得一个函数的参数个数、返回值、以及参数类型。

func addTwoInts(_ a: Int, _ b: String) -> Bool {
    return true
}

let value = type(of: addTwoInts)

let functionType = unsafeBitCast(value as Any.Type to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)

let numberOfArguments = functionType.pointee.numberArguments()

print("该函数\(value)\(numberOfArguments) 个参数")

let returnType = functionType.pointee.resultType

print("该函数\(value) 的返回类型是 \(returnType)")

for i in 0..<numberOfArguments {
    let argumentType = functionType.pointee.arguments.index(of: i).pointee
    print("该函数\(value) 的第\(i+1)个参数的类型是\(argumentType)")
}

//打印结果
该函数(Int, String) -> Bool2 个参数
该函数(Int, String) -> Bool 的返回类型是 Bool
该函数(Int, String) -> Bool 的第1个参数的类型是Int
该函数(Int, String) -> Bool 的第2个参数的类型是String
复制代码

闭包

什么是闭包

闭包是一个捕获了上下文的常量或者是变量的函数。我们看一下官方给的示例。

func makeIncrementer() -> () -> Intvar runningTotal = 10 
    func incrementer() -> Int { 
        runningTotal += 1 
        return runningTotal 
    } 
    return incrementer 
}
复制代码

在代码中,incrementer作为一个闭包,也是一个函数。而且incrementer的生命周期比makeIncrementer要长。当makeIncrementer执行完毕后,内部包含的变量runningTotal也随之消失,但是incrementer有可能还没执行。要想incrementer能够执行,这是就需要捕获runningTotalincrementer内部中,因此构成闭包有两个关键点,一个是函数,另外一个是能够捕获外部变量或者常量

闭包表达式

在swift中,我们可以用以下的表达式来定义闭包

{ (param) -> (returnType) in 

//do something 

}
复制代码

可以看到,闭包表达式由作用域函数参数返回值关键字in函数体构成。

闭包表达式语法能够使用常量形式参数、变量形式参数和输入输出形式参数,但不能提供默认值。可变形式参数也能使用,但需要在形式参数列表的最后面使用。元组也可被用来作为形式参数和返回类型。

闭包的函数整体部分由关键字in 导入,这个关键字表示闭包的形式参数类型和返回类型定义已经完成,并且闭包的函数体即将开始。

闭包的使用

在Swift中,闭包可以当做变量使用,也可以当做函数的参数传递。

  • 闭包当做变量
var closure : (Int) -> Int = { (age: Int) in 
    return age 
}
复制代码
  • 闭包声明成一个可选类型
var closure : ((Int) -> Int)? 
closure = nil
复制代码
  • 闭包当做一个常量(一旦赋值之后就不能改变了)
let closure: (Int) -> Int 

closure = {(age: Int) in 
    return age 
}
复制代码
  • 闭包当做函数参数
func test(param : () -> Int)print(param()) 

}

var age = 10 

test { () -> Int in 
    age += 1 
    return age
}
复制代码

尾随闭包

当我们把闭包表达式作为函数的最后一个参数,如果当前的闭包表达式很长,我们可以通过尾随闭包的书写方式来提高代码的可读性。

我们首先定义一个函数

func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) ->Bool) -> Bool{
    
    return by(a, b, c)
}
复制代码
  • 未使用尾随闭包
test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
    return (item1 + item2 < item3)
})
复制代码
  • 使用尾随闭包
test(10, 20, 30){(_ item1: Int, _ item2: Int, _ item3: Int) in
    return item1 + item2 < item3
}
复制代码

闭包表达式简写

在swift中,使用闭包表达式能更简洁的传达信息。但是也不能太过简略。

var array = [1, 2, 3]
array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1 < item2 })
复制代码
  • 利用上下文推断参数和返回值类型
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 }) 

array.sort(by: {(item1, item2) in return item1 < item2 })
复制代码
  • 单表达式可以隐士返回,既省略 return 关键字
array.sort{(item1, item2) in item1 < item2 }
复制代码
  • 参数名称的简写(比如$0
array.sort{ return $0 < $1 }
array.sort{ $0 < $1 }
复制代码
  • 尾随闭包表达式
array.sort(by: <)
复制代码

闭包捕获值

闭包捕获一个全局变量

我们首先来看一下闭包捕获全局变量的情况,代码如下

var i = 1

let closure = {
    print("closure\(i)")
}

i += 1

print("before closure \(i)")

closure()

print("after closure \(i)")

//打印结果
before closure 2

closure2

after closure 2
复制代码

可以看到,i的值发生变化后,closure里面的i也发生了变化。和OC里面的block很像。接下来我们通过sil文件来探究一下。 截屏2022-03-29 下午11.26.42.png 截屏2022-03-29 下午11.27.27.png 通过上面的sil源码我们可以知道,当执行到closure闭包的时候,直接去寻找变量i的地址,然后把i的值取出来。而此时i的值已经发生了变化,因此取出来i的值就是2。

也就是说此时闭包是直接拿到了全局变量去修改值,那么这里应该就不能叫捕获全局变量了,因为根本没有对全局变量做额外的操作。

闭包捕获一个局部变量

当闭包捕获一个局部变量时,内部又进行了哪些操作呢? 我们拿官方的例子验证一下。

func makeIncrementer() -> () -> Intvar runningTotal = 10 
    func incrementer() -> Int { 
        runningTotal += 1 
        return runningTotal 
    } 
    return incrementer 
}

let makeInc = makeIncrementer()
复制代码

然后我们把它转换成sil文件,可以看到makeIncrementer()函数构成如下:

截屏2022-03-30 下午10.52.52.png 截屏2022-03-30 下午11.00.48.png 可以看到,在makeIncrementer()函数中,使用了alloc_box,而在闭包incrementer()中,又使用到了project_box。我们去官方文档里面去找这两个命令的定义。 截屏2022-03-30 下午11.06.10.png 截屏2022-03-30 下午11.07.17.png官方文档中可以看到,alloc_box 是在堆空间里面创建实例对象,project_box是从这个实例对象地址中取出其中的值。所以闭包捕获变量其实是在堆空间里面创建一个实例对象,并且把捕获变量的值存储到这个实例对象中,每次调用闭包使用的都是同一个堆空间的实例变量地址,所以在闭包外面修改值,闭包内部的值也会改变。 截屏2022-03-30 下午11.24.27.png 通过lldb打印makeInc,我们也可以看到,闭包里面也有Metadata

闭包的本质

这次我们需要通过分析IR文件来探究闭包的本质,首先我们要了解一下IR语法

IR语法

i8 :Int8或者void *i16 :Int16i32Int32i64Int64void *

  • 数组
[<elementnumber> x <elementtype>] 
//example 
alloc [24 x i8], align 8 24个i8都是0 

alloc [4 x i32] === array
复制代码
  • 结构体
%T  = type {<type list>}
//swift.refcount 类型的结构体 有两个成员,分别为swift.type* 类型 和 i64 类型。
%swift.refcount = type {%swift.type*,i64}
复制代码
  • 指针
<type> *
// Int64位的整形指针
i64*
复制代码
  • getelementptr

在LLVM中获取数组或结构体的成员,语法规则如下

<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}

<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}
复制代码

我们使用例子来进一步说明

struct munger_struct{
    int f1;
    int f2;
};

//获取结构体munger_struct的内存首地址
//i64 0 取出的是 struct.munger_struct类型的指针,指针指向的是内存首地址
getelementptr inbounds %struct.munger_struct, %struct.munger_struct %1, i64 0

// 获取munger_struct 的第一个元素
// i64 0 取出的是 struct.munger_struct类型的指针,指针指向的是内存首地址
// i32 0取出的是 struct.munger_struct结构体中的第一个元素
getelementptr inbounds %struct.munger_struct, %struct.munger_struct %1, i64 0, i32 0

// 获取 munger_struct 第二个元素
// i64 0 取出的是 struct.munger_struct类型的指针,指针指向的是内存首地址
// i32 1取出的是 struct.munger_struct结构体中的第二个元素
getelementptr inbounds %struct.munger_struct, %struct.munger_struct %1, i64 0, i32 1

int main(int argc, const char * argv[]) {
    int array[4] = {1, 2, 3, 4};
    int a = array[0];
    return 0;
}

其中 int a = array[0] 这句对应的LLVM代码应该是这样的:
a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i32 0
- [4 x i32]* array:返回一个数组的指针。
- 第一个0:指针指向数组的首地址。
- 第二个0:相对于数组元素的偏移,即数组第一个成员变量。
复制代码

对于getelementptr,我们可以得到以下结论:

  • 第一个索引不会改变返回的指针的类型,也就是说ptrval前面的*对应什么类型,返回就是什么类型 

  • 第一个索引的偏移量的是由第一个索引的值和第一个ty指定的基本类型共同确定的。 

  • 第二个索引以及后面的索引都是在数组或者结构体内进行索引 

  • 每增加一个索引,就会使得该索引使用的基本类型和返回的指针的类型去掉一层。

闭包的本质

接下来,我们使用IR来分析一下闭包,代码如下:

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }

    return incrementer
}

var makeInc = makeIncrementer()
复制代码

转换成IR文件后,我们截取一些代码如下:

%swift.function = type { i8*, %swift.refcounted* }
%swift.refcounted = type { %swift.type*, i64 }
%swift.type = type { i64 }
%swift.full_boxmetadata = type { void (%swift.refcounted*)*, i8**, %swift.type, i32, i8* }
%TSi = type <{ i64 }>

//main函数
define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  %3 = call swiftcc { i8*, %swift.refcounted* } @"$s4main15makeIncrementerSiycyF"()
  %4 = extractvalue { i8*, %swift.refcounted* } %3, 0
  %5 = extractvalue { i8*, %swift.refcounted* } %3, 1
  store i8* %4, i8** getelementptr inbounds (%swift.function, %swift.function* @"$s4main7makeIncSiycvp", i32 0, i32 0), align 8
  store %swift.refcounted* %5, %swift.refcounted** getelementptr inbounds (%swift.function, %swift.function* @"$s4main7makeIncSiycvp", i32 0, i32 1), align 8
  ret i32 0
}
复制代码

截屏2022-04-01 下午4.12.37.png 从代码里面我们可以了解到,在main函数里面,先是去调用了makeIncrementer()函数,返回了一个{ i8*, %swift.refcounted* }的结构体,而%swift.refcounted也是一个结构体{ %swift.type*, i64 },而%swift.type又是一个{ i64 }结构体,里面包含了一个64位整型的变量。

所以在main函数中,其实是做了这么一个操作。调用makeIncrementer()函数,返回了一个结构体,然后去把这个结构体的成员变量值都拿出来,接着给变量makeInc创建一个{ i8*, %swift.refcounted* }的内存空间,把取出来的成员变量都存到这个内存空间里面,也就是完成了一次赋值操作。

接下来我们去看一下makeIncrementer()函数的IR代码,看下这个函数里面具体做了些什么。

define hidden swiftcc { i8*, %swift.refcounted* } @"$s4main15makeIncrementerSiycyF"() #0 {
entry:
  %runningTotal.debug = alloca %TSi*, align 8
  %0 = bitcast %TSi** %runningTotal.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %0, i8 0, i64 8, i1 false)
  %1 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #1
  %2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*
  %3 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %2, i32 0, i32 1
  %4 = bitcast [8 x i8]* %3 to %TSi*
  store %TSi* %4, %TSi** %runningTotal.debug, align 8
  %._value = getelementptr inbounds %TSi, %TSi* %4, i32 0, i32 0
  store i64 10, i64* %._value, align 8
  %5 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %1) #1
  call void @swift_release(%swift.refcounted* %1) #1
  %6 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementerSiycyF11incrementerL_SiyFTA" to i8*), %swift.refcounted* undef }, %swift.refcounted* %1, 1
  ret { i8*, %swift.refcounted* } %6
}
复制代码

我们来具体分析一下这个代码:

%runningTotal.debug = alloca %TSi*, align 8
%0 = bitcast %TSi** %runningTotal.debug to i8*
复制代码

这里是给局部变量runningTotal创建一个%TSi*结构体,用来存放局部变量的值,然后使用一个i8类型的指针指向这个结构体。

%1 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #1
复制代码

这个代码是在堆空间里面创建了一个%swift.refcounted*类型的实例变量。

%2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*
复制代码

把一个%swift.refcounted*指针转换成{ %swift.refcounted, [8 x i8] }结构体的指针。

%3 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %2, i32 0, i32 1
复制代码

这个代码是把指针指向<{ %swift.refcounted, [8 x i8] }>结构体的索引值为1的元素。也就是[8 x i8]数组。

%4 = bitcast [8 x i8]* %3 to %TSi*
store %TSi* %4, %TSi** %runningTotal.debug, align 8
复制代码

这段代码是把指向[8 x i8]数组的指针,转成%TSi*指针,同时把局部变量runningTotal%TSi*结构体存入这个数组里面。

%._value = getelementptr inbounds %TSi, %TSi* %4, i32 0, i32 0
store i64 10, i64* %._value, align 8
复制代码

这个代码先去获取[8 x i8]数组的第一个元素,也就是刚才存进去的局部变量runningTotal%TSi*结构体,同时把值10存入到这个结构体里面。

%5 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %1) #1
call void @swift_release(%swift.refcounted* %1) #1
复制代码

这个代码里面做的是跟引用计数相关的,不做研究。

%6 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementerSiycyF11incrementerL_SiyFTA" to i8*), %swift.refcounted* undef }, %swift.refcounted* %1, 1
ret { i8*, %swift.refcounted* } %6
复制代码

截屏2022-04-01 下午4.12.37.png 上面这个代码就是把指向incrementer()函数的指针转成i8*,同时把刚才上面创建的保存着数据10%swift.refcounted*类型的实例变量,一起插入到{ i8*, %swift.refcounted* }结构体中。并把这个结构体返回出去。

通过上面的分析,我们可以把闭包在内存中的结构用以下结构体来表示:

struct ClosureData{
    var ptr: UnsafeRawPointer // 函数地址
    var object: HeapObject // 存储捕获堆空间地址的值
}

struct HeapObject {
    var matedata: UnsafeRawPointer
    var refcount1: Int32
    var refcount2: Int32
}
复制代码

我们在SIL分析中看到,闭包有使用了alloc_box来创建实例变量,同时我们知道闭包中%swift.refcounted*类型的实例变量里面存储这捕获变量的值。因此,我们可以进一步还原object属性变量

struct Box<T>{
    var object: HeapObject
    var value: T
}
struct HeapObject {
    var matedata: UnsafeRawPointer
    var refcount1: Int32
    var refcount2: Int32
}
复制代码

所以闭包的最终内存结构可以这样表示:

struct ClosureData{
    var ptr: UnsafeRawPointer // 函数地址
    var object: UnsafePointer<Box>// 存储捕获堆空间地址的值
}

struct Box<T>{
    var object: HeapObject
    var value: T
}
struct HeapObject {
    var matedata: UnsafeRawPointer
    var refcount1: Int32
    var refcount2: Int32
}
复制代码

验证闭包结构分析结果

接下来我们验证一下上面的结构体是否正确。验证代码如下:

struct NoMeanStruct {
    var f: () -> Int
}
var f = NoMeanStruct(f: makeIncrementer())
let ptr = UnsafeMutablePointer<NoMeanStruct>.allocate(capacity: 1)
ptr.initialize(to: f)

let ctx = ptr.withMemoryRebound(to: ClosureData<Box<Int>>.self, capacity: 1) {
    $0.pointee
}

print("闭包函数的内存地址: \(ctx.ptr)")
print("闭包函数的堆空间地址: \(ctx.object)")
print("闭包函数的捕获变量值: \(ctx.object.pointee.value)")

//打印结果
闭包函数的内存地址: 0x0000000100008c80
闭包函数的堆空间地址: 0x0000000100748aa0
闭包函数的捕获变量值: 10
复制代码

现在我们来验证这个结果 截屏2022-04-01 下午11.04.35.png 看结果和推断的结果完全一致。

捕获引用类型

如果闭包捕获的变量类型是一个引用类型,比如一个类的实例对象,那又是怎么捕获的呢?首先我们举一个例子

class LGTeacher {
    var age = 10
}

func test() {
    var t = LGTeacher()
    let clousure = {
        t.age += 10
    }    
    clousure()
}

test()
复制代码

转换成IR代码,我们看一下main函数里面做了什么

define hidden swiftcc void @"$s4main4testyyF"() #0 {
entry:
  %0 = alloca %T4main9LGTeacherC*, align 8
  %1 = bitcast %T4main9LGTeacherC** %0 to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %1, i8 0, i64 8, i1 false)
  //创建了一个%swift.function结构体
  %clousure.debug = alloca %swift.function, align 8
  %2 = bitcast %swift.function* %clousure.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %2, i8 0, i64 16, i1 false)
  %3 = bitcast %T4main9LGTeacherC** %0 to i8*
  call void @llvm.lifetime.start.p0i8(i64 8, i8* %3)
  %4 = call swiftcc %swift.metadata_response @"$s4main9LGTeacherCMa"(i64 0) #7
  %5 = extractvalue %swift.metadata_response %4, 0
  
  //调用了LGTeacher.__allocating_init()方法,也就是创建了一个实例对象t,并且把这个实例对象地址存储到了%6寄存器。
  %6 = call swiftcc %T4main9LGTeacherC* @"$stmain9LGTeacherCACycfC"(%swift.type* swiftself %5)
  %7 = bitcast %T4main9LGTeacherC* %6 to %swift.refcounted*
  %8 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %7) #3
  store %T4main9LGTeacherC* %6, %T4main9LGTeacherC** %0, align 8
  %9 = bitcast %T4main9LGTeacherC* %6 to %swift.refcounted*
  %10 = bitcast %swift.function* %clousure.debug to i8*
  call void @llvm.lifetime.start.p0i8(i64 16, i8* %10)
  %clousure.debug.fn = getelementptr inbounds %swift.function, %swift.function* %clousure.debug, i32 0, i32 0
  //把test函数的地址存入%clousure.debug.fn里面,也就是void*。
  store i8* bitcast (void (%swift.refcounted*)* @"$s4main4testyyFyycfU_Tf2i_nTA" to i8*), i8** %clousure.debug.fn, align 8
  
  %clousure.debug.data = getelementptr inbounds %swift.function, %swift.function* %clousure.debug, i32 0, i32 1
  
  //把实例变量t的地址存到%clousure.debug.data中,因为实例变量t的地址已经在堆区,不需要重新建立。
  store %swift.refcounted* %9, %swift.refcounted** %clousure.debug.data, align 8
  
  //对实例对象进行引用计数的操作
  %11 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %9) #3
  
  //调用了clousure闭包
  call swiftcc void @"$s4main4testyyFyycfU_Tf2i_nTA"(%swift.refcounted* swiftself %9)
  call void @swift_release(%swift.refcounted* %9) #3
  call void @swift_release(%swift.refcounted* %9) #3
  
  //把捕获的实例变量t的地址存储到闭包的%swift.refcounted结构体中
  %toDestroy = load %T4main9LGTeacherC*, %T4main9LGTeacherC** %0, align 8
  call void bitcast (void (%swift.refcounted*)* @swift_release to void (%T4main9LGTeacherC*)*)(%T4main9LGTeacherC* %toDestroy) #3
  %12 = bitcast %T4main9LGTeacherC** %0 to i8*
  call void @llvm.lifetime.end.p0i8(i64 8, i8* %12)
  ret void
}
复制代码

截屏2022-04-03 下午3.24.04.png

从这里可以看出,在捕获引用类型时候,其实也不需要捕获实例对象,因为它已经在堆区了,就不需要再去创建一个堆空间的实例了,只需要将它的地址存储到闭包的结构中,操作实例对象的引用计数,就可以了。

接下来我们使用上面闭包的结构验证一下

class LGTeacher {
    var age = 10
}

func test() {
    let t = LGTeacher()
    let clousure = {
        t.age += 10
    }
    
    let f = NoMeanStruct(f: clousure)
    let ptr = UnsafeMutablePointer<NoMeanStruct>.allocate(capacity: 1)
    ptr.initialize(to: f)

    let ctx = ptr.withMemoryRebound(to: ClosureData<Box<Int>>.self capacity: 1) {
        $0.pointee
    }

    print("闭包函数的内存地址: \(ctx.ptr)")
    print("闭包函数的堆空间地址: \(ctx.object)")
    print("闭包函数的捕获变量值: \(ctx.object.pointee.value)")
    print("end")
}

test()
复制代码

打印结果如下: 截屏2022-04-03 下午3.50.49.png 闭包存储的堆空间地址,就是捕获到的引用类型实例变量的地址。

闭包捕获多个变量

我们上面还原的是闭包捕获一个变量的情况,那如果闭包捕获多个变量,又是什么样的呢?我们继续分析两个变量的情况,代码如下:

func makeIncrementer(_ amount: Int) -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

var makeInc = makeIncrementer(10)
复制代码

接下来我们把它编译成IR文件,makeIncrementer()函数的IR代码如下:

define hidden swiftcc { i8*, %swift.refcounted* } @"$s4main15makeIncrementerySiycSiF"(i64 %0) #0 {
entry:
  %amount.debug = alloca i64, align 8
  %1 = bitcast i64* %amount.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %1, i8 0, i64 8, i1 false)
  %runningTotal.debug = alloca %TSi*, align 8
  %2 = bitcast %TSi** %runningTotal.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %2, i8 0, i64 8, i1 false)
  store i64 %0, i64* %amount.debug, align 8
  %3 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #2
  %4 = bitcast %swift.refcounted* %3 to <{ %swift.refcounted, [8 x i8] }>*
  %5 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %4, i32 0, i32 1
  %6 = bitcast [8 x i8]* %5 to %TSi*
  store %TSi* %6, %TSi** %runningTotal.debug, align 8
  %._value = getelementptr inbounds %TSi, %TSi* %6, i32 0, i32 0
  store i64 10, i64* %._value, align 8
  %7 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %3) #2
  %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
  %9 = bitcast %swift.refcounted* %8 to <{ %swift.refcounted, %swift.refcounted*, %TSi }>*
  %10 = getelementptr inbounds <{ %swift.refcounted, %swift.refcounted*, %TSi }>, <{ %swift.refcounted, %swift.refcounted*, %TSi }>* %9, i32 0, i32 1
  store %swift.refcounted* %3, %swift.refcounted** %10, align 8
  %11 = getelementptr inbounds <{ %swift.refcounted, %swift.refcounted*, %TSi }>, <{ %swift.refcounted, %swift.refcounted*, %TSi }>* %9, i32 0, i32 2
  %._value1 = getelementptr inbounds %TSi, %TSi* %11, i32 0, i32 0
  store i64 %0, i64* %._value1, align 8
  call void @swift_release(%swift.refcounted* %3) #2
  %12 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementerySiycSiF11incrementerL_SiyFTA" to i8*), %swift.refcounted* undef }, %swift.refcounted* %8, 1
  ret { i8*, %swift.refcounted* } %12
}
复制代码

现在我们来逐一分析一下:

%amount.debug = alloca i64, align 8
 %1 = bitcast i64* %amount.debug to i8*
 call void @llvm.memset.p0i8.i64(i8* align 8 %1, i8 0, i64 8, i1 false)
 %runningTotal.debug = alloca %TSi*, align 8
 %2 = bitcast %TSi** %runningTotal.debug to i8*
 call void @llvm.memset.p0i8.i64(i8* align 8 %2, i8 0, i64 8, i1 false)
 store i64 %0, i64* %amount.debug, align 8
复制代码

这上面的代码分别是给捕获的变量amountrunningTotal分别创建了一个内存空间,并放在了%1%2寄存器里面。接着把0存入变量amount里面,因为amount是外部传进来的变量,还不知道传值多少,因此默认数值为0

%3 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #2
 %4 = bitcast %swift.refcounted* %3 to <{ %swift.refcounted, [8 x i8] }>*
 %5 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %4, i32 0, i32 1
 %6 = bitcast [8 x i8]* %5 to %TSi*
 store %TSi* %6, %TSi** %runningTotal.debug, align 8
 %._value = getelementptr inbounds %TSi, %TSi* %6, i32 0, i32 0
 store i64 10, i64* %._value, align 8
复制代码

这里和上面的捕获单个变量的情形一样,创建一个%swift.refcounted*结构体,并且把runningTotal的内存地址存到这个结构体里面,并把10存到runningTotal的内存地址里。

%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
  %9 = bitcast %swift.refcounted* %8 to <{ %swift.refcounted, %swift.refcounted*, %TSi }>*
复制代码

这里又新建了一个%swift.refcounted*结构体,并且指针指向了{ %swift.refcounted, %swift.refcounted*, %TSi }结构体。

%11 = getelementptr inbounds <{ %swift.refcounted, %swift.refcounted*, %TSi }>, <{ %swift.refcounted, %swift.refcounted*, %TSi }>* %9, i32 0, i32 2
  %._value1 = getelementptr inbounds %TSi, %TSi* %11, i32 0, i32 0
  store i64 %0, i64* %._value1, align 8
  call void @swift_release(%swift.refcounted* %3) #2
复制代码

这里是把存有runningTotal变量的%swift.refcounted*结构体,存到了{ %swift.refcounted, %swift.refcounted*, %TSi }结构体的第二个%swift.refcounted*属性里,并对其引用计数进行释放操作。同时把amount变量存入到%TSi中。

%12 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementerySiycSiF11incrementerL_SiyFTA" to i8*), %swift.refcounted* undef }, %swift.refcounted* %8, 1
  ret { i8*, %swift.refcounted* } %12
复制代码

这里把incrementer()函数的内存地址,和指向{ %swift.refcounted, %swift.refcounted*, %TSi }结构体的指针,共同构成了闭包的数据结构。

综上所述,这里将第一个捕获的值存储到堆区后,在捕获第二个值创建了新的对象,然后把第一个对象存储进新的对象里面。

所以,当闭包捕获到两个变量的时候,它的内存数据结构表示如下:

struct ClosureParamData<T>{
    var ptr: UnsafeRawPointer // 函数地址
    var captureValue: UnsafePointer<T>// 存储捕获堆空间地址的值
}

struct TwoParamerStruct<T1,T2>{
    var object: HeapObject
    var value1: UnsafePointer<Box<T1>>
    var value2: T2
}

struct Box<T>{
    var object: HeapObject
    var value: T
}

struct HeapObject {
    var matedata: UnsafeRawPointer
    var refcount1: Int32
    var refcount2: Int32
}
复制代码

接下来我们还原makeIncrementer()函数的内存结构。

let f = NoMeanStruct(f: makeIncrementer(20))

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

ptr.initialize(to: f)

let ctx = ptr.withMemoryRebound(to: ClosureData<TwoParamerStruct<Int, Int>>.self, capacity: 1) {
    $0.pointee
}

print("闭包函数的内存地址: \(ctx.ptr)")
print("闭包函数的堆空间地址: \(ctx.captureValue)")
print("闭包函数的第一个捕获变量值: \(ctx.captureValue.pointee.value1.pointee.value)")
print("闭包函数的第二个捕获变量值: \(ctx.captureValue.pointee.value2)")

//打印结果
闭包函数的内存地址: 0x0000000100008660
闭包函数的堆空间地址: 0x000000010113c430
闭包函数的第一个捕获变量值: 10
闭包函数的第二个捕获变量值: 20
复制代码

闭包捕获三个变量

我们接着看闭包捕获三个变量的情况,代码如下:

func makeIncrementer(_ amount: Int, _ amount1: Int) -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += amount
        runningTotal += amount1
        return runningTotal
    }
    return incrementer
}

var makeInc = makeIncrementer(20,30)
复制代码

编译成IR后,makeIncrementer()函数的代码如下 截屏2022-04-05 上午12.16.48.png 可以看到,和捕获两个变量的代码差不多一样,不同的是结构体变成了{ %swift.refcounted, %swift.refcounted*, %TSi, %TSi },多了一个%TSi用来存储捕获的第三个变量的值。

闭包捕获多个变量总结

通过上面闭包捕获两个和三个变量的分析,我们可以知道,闭包的数据结构有捕获单个变量和多个变量这两种情况。因此我们还原闭包数据结构的代码如下:

//单个值
struct ClosureData<Box>{
    var ptr: UnsafeRawPointer // 函数地址
    var captureValue: UnsafePointer<Box> // 存储捕获堆空间地址的值
}

//多个值
struct ClosureData<MutiValue>{
    var ptr: UnsafeRawPointer // 函数地址
    var captureValue: UnsafePointer<MutiValue> // 存储捕获堆空间地址的值
}

struct MutiValue<T1,T2......>{
    var object: HeapObject
    var value:  UnsafePointer<Box<T1>>
    var value:  T2
    var value:  T3
    .....
}
struct Box<T>{
    var object: HeapObject
    var value: T
}
struct HeapObject {
    var matedata: UnsafeRawPointer
    var refcount1: Int32
    var refcount2: Int32
}
复制代码

逃逸闭包

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

使用逃逸闭包一般满足以下两个条件:

  • 当闭包被当作属性存储,导致函数完成时闭包生命周期被延长

截屏2022-04-05 下午9.43.54.png

  • 当闭包异步执行,导致函数完成时闭包生命周期被延长。

截屏2022-04-05 下午9.50.12.png

  • 可选类型的闭包默认是逃逸闭包。

截屏2022-04-05 下午9.54.21.png

以下这种闭包其实也是逃逸,对于编译器来说,把一个闭包赋值给了一个变量,编译器认为这个闭包可能会在其他地方去执行。 截屏2022-04-05 下午9.56.31.png

所以,逃逸闭包所需的条件:

  • 作为函数的参数传递。
  • 当前闭包在函数内部异步执行或者被存储。
  • 函数结束,闭包被调用,闭包的生命周期未结束。

自动闭包

@autoclosure是一种自动创建的闭包,用于将参数包装成闭包。这种闭包不接受任何参数,当它被调用的时候,会返回传入的值。这种便利语法让你在调用的时候能够省略闭包的花括号

函数中有一个 ()-> Any类型的参数,用@autoclosure修饰时,调用函数的时候可以传入一个确定的值 a,这个值会被自动包装成(){return a}的闭包,就不需要显示的将闭包表达式写出来

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

func getString()->String{
    return "Application Error Occured"
}
复制代码

defer关键字

defer {}里的代码会在当前代码块返回的时候执行,无论当前代码块是从哪个分支return的,即使程序抛出错误,也会执行。

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

举一个简单例子

func f() {
    defer { print("First defer") }

    defer { print("Second defer") }

    print("End of function")
}

f()
//打印结果
End of function
Second defer
First defer
复制代码

猜你喜欢

转载自juejin.im/post/7083118239022055460
今日推荐