Swift-闭包Closure

199 阅读6分钟

Swift 闭包

函数类型

在swift中函数本身也有自己的类型,它由形 式参数类型,返回类型组成。在使用函数作为变量时,如果有同名函数不指定类型会报错

image.png 函数可以被当作变量,那么就看一下它里面存储的是什么

 func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}
func add(_ a: Double, _ b: Double) -> Double {
    return a + b
}

var a : (Int,Int)->Int = add

var b = a
print(a)

image.png

可以看到这里存储了metadata,在源码里面其实也可以找到关于函数的metadata定义

image.png

从这里可以看出,它有一个 kind 属性,有一个Flags属性,还有一个返回值类型ResultTYpe属性,另外,还有一个连续的空间存储的是参数列表,而且,返回值类型和参数列表中的类型都是 TargetMetadata的类型,也就是参数的类型,也就是Any.Type类型

通过TargetFunctionTypeFlags,可以看到通过 getNumParameters 获取参数个数

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;
  unsigned getNumParameters() const { return Data & NumParametersMask; }

从上面的分析可以得到结构如下

struct TargetFunctionTypeMetadata{
    var kind: UnsafeMutablePointer<UInt64>
    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 index(of i: Int) -> UnsafeMutablePointer<Element> {
        return withUnsafePointer(to: &self) {
            return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: Element.self).advanced(by: i))
        }
    }
}

同样可以通过下面的案例验证一下分析的函数结构是否正确

func add(_ a: Int, _ b: Int) -> String {
    return "a"
}

var a : (Int,Int)->String = add
var FuncType = type(of: a)

struct TargetFunctionTypeMetadata{
    var kind: UnsafeMutablePointer<UInt64>
    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 index(of i: Int) -> UnsafeMutablePointer<Element> {
        return withUnsafePointer(to: &self) {
            return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: Element.self).advanced(by: i))
        }
    }
}

var ptr = unsafeBitCast(FuncType as Any.Type , to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)
var resultType = ptr.pointee.resultType
var argumentsNums = ptr.pointee.numberArguments()

print("(\(FuncType)) 共有\(argumentsNums)个参数\n返回值类型为:\(resultType)")

for i in 0..<argumentsNums{
    let argumentType = ptr.pointee.arguments.index(of: i).pointee
    print("第\(i)个参数类型是:\(argumentType)")
}

//((Int, Int) -> String) 共有2个参数
//返回值类型为:String
//第0个参数类型是:Int
//第1个参数类型是:Int

闭包

闭包是一个可以捕获上下文的常量或者变量的函数

初始闭包

通过官方的案例认识一下闭包

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

这里incrementer作为一个闭包,显然他是一个函数,其次为了保证其执行,要捕获外部变量runningTotal 到内部,所以闭包的关键就有捕获外部变量或常量函数

闭包表达式

定义闭包表达式

在使用闭包的时候,可以用下面的方式来定义一个闭包表达式

{ (param type) -> (return type) in
    //do somethings
}

可以看到闭包表达式是由作用域(花括号)函数类型关键字in函数体构成

闭包作为变量和参数

  • 作为变量
var closure: (Int) -> Int = { (a: Int) -> Int in
        return a + 100
}

  • 作为参数
func func3(_ someThing: @escaping (() -> Void)) {
       DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: someThing)
    }

尾随闭包

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

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)
})

逃逸闭包

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

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

image.png

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

image.png

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

image.png

  • 其实在使用中还有一种情况
func test() -> Int{
    var age = 10
    let completeHandler = {
         age += 10
    }
    
    completeHandler()
    return age
}

对于这种闭包其实也是逃逸,对于编译器来说,把一个闭包赋值给了一个变量,编译器认为这个闭包可能会在其他地方去执行,它是逃逸闭包

所以逃逸闭包的条件

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

自动闭包

@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"
}

闭包表达式的简略写法

使用闭包表达式能更简洁的传达信息。但是也不要过于简略le

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: <)

OC Block和Swift 闭包的互相调用

Swift的闭包和OC 中 Block是可以互相调用的

Swift 调用OC

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TestOC : NSObject
+(void)testBlock:(void(^)(NSInteger index))block;
@end

NS_ASSUME_NONNULL_END
#import "TestOC.h"
@implementation TestOC
+(void)testBlock:(void(^)(NSInteger index))block {
    if (block) {
        block(10);
    }
}
@end

//main
TestOC.test { index in
    print("oc-block传过来的值\(index)")
}

image.png

就可以看到了OC的类被编译成了 swift的类型

image.png

OC 调用Swift

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TestOC : NSObject
+(void)testClouser;
@end

#import "TestOC.h"
#import "SwiftTest-Swift.h"
@implementation TestOC
+(void)testClouser{
    Test.closure = ^(NSString * _Nonnull name) {
        NSLog(@"OC使用:%@",name);
    };
}
@end


//main
TestOC.testClouser() //给swift类赋值

Test.closure?("Swift")// 调用

image.png

Swift 编译的OC版本

image.png

闭包捕获值

swift的闭包和OC中的block很相似,那么闭包如何捕获一个外部的值呢

捕获一个全局变量

   var i = 1
   let closure = {
       print("closure:\(i)")
   }
    i += 1
   print("beforClosure:\(i)")
   closure()
   print("afterClosure:\(i)")
}

//输出结果
beforClosure:2
closure:2
afterClosure:2

这一点和OC的block 很像了,应该是直接拿到了全局变量去修改,通过SIL看一下

image.png

image.png

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

捕获一个局部变量


func test(){
   var i = 1
   let closure = {
       print("closure:\(i)")
   }
    i += 1
   print("beforClosure:\(i)")
   closure()
   print("afterClosure:\(i)")
}

test()

//输出结果
beforClosure:2
closure:2
afterClosure:2

从输出结果看,这个和block有一点区别,这里好像也是拿到了局部变量的值去修改,事实是不是这样呢,通过SIL分析一下:

image.png

image.png 可以看到在test函数中调用了 alloc_box,在闭包的调用中使用了一个 project_box

image.png

image.png官方文档中看到,alloc_box 是分配一个堆空间的地址存储值,project_box是从一个堆空间地址取出其中的值,那么也就是说这里闭包中使用的值和外部的i其实是一个堆空间中存储的值,也就是说所谓的捕获变量其实是把变量存储到了堆空间中,每次使用的都是同一个堆空间变量,所以在闭包外面修改值,闭包内部的值也会改变

这里通过lldb来看一下闭包的内存是什么

image.png 可以发现,这里的闭包也有 metadata

闭包捕获值的本质

上面说的捕获变量时会分配堆空间,那么这些堆空间对于闭包来说怎么存储的呢,下面就来分析一下,闭包捕获值之后的结构

了解一下IR的语法

i8Int8 void * i16Int16 i32Int32 i64Int64

  • 创建数组
[<元素个数> x <元素类型>]
 //24个i8类型的数组
 alloc [24 x i8]align 8 
  • 结构体
%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字节 0 * 4字节
- 第二个0:相对于数组元素的偏移,即数组第一个成员变量 0 * 4字节

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

 其中 int a = array[0][0] 这句对应的LLVM代码应该是这样的:
a = getelementptr inbounds [4 x [2 x i32]], [4 x [2 x i32]]* array, i64 0, i64 0,i32 0
- [4 x [2 x i32]]* array:数组首地址
- 第一个0:相对于4 x [2 x i32]]数组自身的偏移,即偏移0字节 0 * 32字节
- 第二个0:相对于4 x [2 x i32]]数组元素的偏移,即数组第一个成员变量 0 * 8字节
- 第三个0:相对于[2 x i32]数组元素的偏移,即数组第一个成员变量 0 * 4字节

对于结构体和数组来说,getelementptr取值是一样的

  • 第一个索引不会改变返回的指针的类型,即ptrval前面对应什么类型,返回的就是什么类型
  • 第一个索引的偏移量是由第一个索引的值和第一个ty指定的基本类型共同确定的,
  • 第二个索引是在数组或者结构体内进行索引,内部偏移多少元素大小,
  • 每增加一个索引,就会使得该索引使用的基本类型和返回的指针类型去掉一层

例 获取[4 x i32] 数组地址中第一个所有去除的类型是 [4 x i32] 第二个索引获取的类型是i32

image.png

IR分析闭包的本质

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

var a = makeIncrementer()


image.png

从这里可以看出来,调用 makeIncrementer后返回了一个 { i8*, %swift.refcounted* }类型的结构体,这个结构体中有 i8 类型和%swift.refcounted类型的成员, %swift.refcounted中是一个 { %swift.type*, i64 } 结构体,他内部是一个 %swift.type结构体类型 和 i64 类型的成员 %swift.type 这个结构体只有一个 i64 类型的成员

main函数中IR代码可以得知到,闭包的结构大致是一个 { i8*, %swift.refcounted* }的结构 ,那么来从makeIncrementer函数的代码中开看一下,这个结构体中存储的都是些什么

image.png

image.png

从这里的分析可以看出,整个过程是创建了一个实例对象,并且存放了捕获的局部变量,然后将makeIncrementer的内部函数incrementer的地址存储到{ i8*, %swift.refcounted* }第一个成员中,然后将堆空间的实例对象存储到了第二个成员中 所以闭包的结构应该是这样的

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

struct HeapObject {
    var matedata: UnsafeRawPointer
    var refcount1: Int32
    var refcount2: Int32
    var value: Any
}

上面的 HeapObject在底层就是metadata和refcount,这里应该还有捕获的变量,所以这个object 应该是 下面的结构

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

那么最终的结构就是

struct ClosureData<Box>{
    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 ClosureStruct {
    var closure :() -> Int
}
var f = ClosureStruct(closure: makeIncrementer())
//f初始化一个ClosureStruct类型指针
let ptr = UnsafeMutablePointer<ClosureStruct>.allocate(capacity: 1)
ptr.initialize(to: f)

//内存重新绑定为 ClosureData<Box<Int>>
let ctx = ptr.withMemoryRebound(to: ClosureData<Box<Int>>.self, capacity: 1){
    $0.pointee
}
print("闭包的调用地址:",ctx.ptr)
print("堆空间地址:",ctx.object)
print("堆空间存储的值", ctx.object.pointee.value)
ptr.deinitialize(count: 1)
ptr.deallocate()//输出

image.png

image.png

这里的结果可以看出和我们推断的结果是一致的。

对于函数地址的确定,也可以通过Mach-o文件来找对应的函数地址,来证实我们的猜想。 我们需要借助nm -p命令。

$ nm -p <mach-o path> | grep <函数地址(不带0x)>

//还原Mach-O中的符号
$ xcrun swift-demangle <string> 

image.png

捕获引用类型

class Person {
    var age: Int = 10
}
func test(){
    var p = Person()
    var closure = {
        p.age += 10
    }
    closure()
}
test()


image.png

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

同样验证一下

func test(){
    var p = Person()
    var closure = {
        p.age += 10
    }
    
    //用结构体包裹一下闭包方便作指针的转换
    struct ClosureStruct {
        var closure :() -> Void
    }
    var f = ClosureStruct(closure: closure)
    //f初始化一个ClosureStruct类型指针
    let ptr = UnsafeMutablePointer<ClosureStruct>.allocate(capacity: 1)
    ptr.initialize(to: f)
    //内存重新绑定为 ClosureData<Box<Int>>
    let ctx = ptr.withMemoryRebound(to: ClosureData<Box<Int>>.self, capacity: 1){
        $0.pointee
    }
    print("闭包的调用地址:",ctx.ptr)
    print("堆空间地址:",ctx.object)
    print("堆空间存储的值", ctx.object.pointee.value)
    ptr.deinitialize(count: 1)
    ptr.deallocate()//输出
}

image.png

捕获多个变量

两个变量

上面说的是捕获了一个变量的情况,如果是捕获多个变量是不是创建了多个堆空间的对象呢,下面我们继续分析

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

image.png

可以看到,这里将第一个捕获的值存储到堆区后,在捕获第二个值创建了新的对象,然后把第一个对象存储进新的对象里面。当前闭包的结构边成了如下情况:

struct ClosureData<TwoParamerStruct>{
    var ptr: UnsafeRawPointer // 函数地址
    var object: UnsafePointer<TwoParamerStruct> // 存储捕获堆空间地址的值
}
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
}
struct ClosureStruct {
    var closure :() -> Int
}
var f = ClosureStruct(closure: makeIncrementer(20))
//f初始化一个ClosureStruct类型指针
let ptr = UnsafeMutablePointer<ClosureStruct>.allocate(capacity: 1)
ptr.initialize(to: f)
//内存重新绑定为 ClosureData<TwoParamerStruct<Int,Int>>
let ctx = ptr.withMemoryRebound(to: ClosureData<TwoParamerStruct<Int,Int>>.self, capacity: 1){
    $0.pointee
}
print("闭包的调用地址:",ctx.ptr)
print("堆空间地址:",ctx.object)
print("堆空间存储的值1", ctx.object.pointee.value1.pointee.value)
print("堆空间存储的值2", ctx.object.pointee.value2)
ptr.deinitialize(count: 1)
ptr.deallocate()//输出```
![](media/16431905047762.jpg)

三个变量

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

var makeInc = makeIncrementer(20)

image.png

这里发现只有两次创建对象,然后存储了第一次创建的对象,再依次存储后续捕获的值

小结

通过上面的介绍对于整个闭包的结构应该有两种情况 捕获单个值和捕获多个值的情况


//单个值
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
}

逃逸闭包和普通闭包的区别

  • 非逃逸闭包捕获变量
func test() -> Int{
    var age = 10
    clouserNoescaping {
        age += 20
    }
    return age
}

func clouserNoescaping(_ f: () -> Void){
    f()
}

test()

image.png

定义闭包参数时默认为非逃逸闭包,再通过IR代码来查看一下函数执行情况怎么执行

image.png

从这里看非逃逸闭包中并没有创建堆空间捕获变量,而是直接将值存储在栈上

  • 逃逸闭包
var completeHandler: (() -> Void)?
func test() -> Int{
    var age = 10
    clouserNoescaping {
        age += 20
    }
    return age
}

func clouserNoescaping(_ f: @escaping () -> Void){
   completeHandler = f
}

image.png

很显然这里发生了值的捕获

由此看来系统对这种变量/闭包的生命周期和函数一致的情况下做了优化,不会去捕获局部变量,而是直接使用,这样就不会有循环引用的情况发生

综上所述,非逃逸闭包的优势具有一下优势

  • 不会产生循环引用,函数作用域内释放
  • 编译器更多性能优化 (如堆空间的分配,retain, relsase等操作)
  • 上下文的内存保存再栈上,不是堆上

defer

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

func testDefer() {
    defer {
        print("First defer")
    }
    defer {
        print("Second defer")
    }
    
    print("end of testDefer")
}
testDefer()

输出顺序
end of testDefer --> Second defer -> First defer