Swift 闭包
函数类型
在swift中函数本身也有自己的类型,它由形 式参数类型,返回类型组成。在使用函数作为变量时,如果有同名函数不指定类型会报错
函数可以被当作变量,那么就看一下它里面存储的是什么
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)
可以看到这里存储了metadata,在源码里面其实也可以找到关于函数的metadata定义
从这里可以看出,它有一个 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 来明确闭包是允许逃逸的。
- 当闭包被当作属性存储,导致函数完成时闭包生命周期被延长
- 当闭包异步执行,导致函数完成时闭包生命周期被延长
- 可选类型的闭包默认是逃逸闭包
- 其实在使用中还有一种情况
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)")
}
就可以看到了OC的类被编译成了 swift的类型
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")// 调用
Swift 编译的OC版本
闭包捕获值
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看一下
这里看也是直接拿到了全局变量去修改值,那么这里应该就不能叫捕获全局变量了,因为根本没有对全局变量做额外的操作
捕获一个局部变量
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分析一下:
可以看到在
test函数中调用了 alloc_box,在闭包的调用中使用了一个 project_box
在官方文档中看到,
alloc_box 是分配一个堆空间的地址存储值,project_box是从一个堆空间地址取出其中的值,那么也就是说这里闭包中使用的值和外部的i其实是一个堆空间中存储的值,也就是说所谓的捕获变量其实是把变量存储到了堆空间中,每次使用的都是同一个堆空间变量,所以在闭包外面修改值,闭包内部的值也会改变
这里通过lldb来看一下闭包的内存是什么
可以发现,这里的闭包也有
metadata
闭包捕获值的本质
上面说的捕获变量时会分配堆空间,那么这些堆空间对于闭包来说怎么存储的呢,下面就来分析一下,闭包捕获值之后的结构
了解一下IR的语法
i8 :Int8 void *
i16 : Int16
i32 : Int32
i64 : Int64
- 创建数组
[<元素个数> 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
IR分析闭包的本质
func makeIncrementer() -> () -> Int {
var runningTotal = 10
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
var a = makeIncrementer()
从这里可以看出来,调用 makeIncrementer后返回了一个 { i8*, %swift.refcounted* }类型的结构体,这个结构体中有 i8 类型和%swift.refcounted类型的成员, %swift.refcounted中是一个 { %swift.type*, i64 } 结构体,他内部是一个 %swift.type结构体类型 和 i64 类型的成员 %swift.type 这个结构体只有一个 i64 类型的成员
main函数中IR代码可以得知到,闭包的结构大致是一个 { i8*, %swift.refcounted* }的结构
,那么来从makeIncrementer函数的代码中开看一下,这个结构体中存储的都是些什么
从这里的分析可以看出,整个过程是创建了一个实例对象,并且存放了捕获的局部变量,然后将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()//输出
这里的结果可以看出和我们推断的结果是一致的。
对于函数地址的确定,也可以通过Mach-o文件来找对应的函数地址,来证实我们的猜想。 我们需要借助nm -p命令。
$ nm -p <mach-o path> | grep <函数地址(不带0x)>
//还原Mach-O中的符号
$ xcrun swift-demangle <string>
捕获引用类型
class Person {
var age: Int = 10
}
func test(){
var p = Person()
var closure = {
p.age += 10
}
closure()
}
test()
从这里可以看出,在捕获引用类型时候,其实也不需要捕获实例对象,因为它已经在堆区了,就不需要再去创建一个堆空间的实例包裹它了,只需要将它的地址存储到闭包的结构中,操作实例对象的引用计数,就可以了
同样验证一下
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()//输出
}
捕获多个变量
两个变量
上面说的是捕获了一个变量的情况,如果是捕获多个变量是不是创建了多个堆空间的对象呢,下面我们继续分析
func makeIncrementer(_ amount: Int) -> () -> Int {
var runningTotal = 10
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
var makeInc = makeIncrementer(20)
可以看到,这里将第一个捕获的值存储到堆区后,在捕获第二个值创建了新的对象,然后把第一个对象存储进新的对象里面。当前闭包的结构边成了如下情况:
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()//输出```

三个变量
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)
这里发现只有两次创建对象,然后存储了第一次创建的对象,再依次存储后续捕获的值
小结
通过上面的介绍对于整个闭包的结构应该有两种情况 捕获单个值和捕获多个值的情况
//单个值
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()
定义闭包参数时默认为非逃逸闭包,再通过IR代码来查看一下函数执行情况怎么执行
从这里看非逃逸闭包中并没有创建堆空间捕获变量,而是直接将值存储在栈上
- 逃逸闭包
var completeHandler: (() -> Void)?
func test() -> Int{
var age = 10
clouserNoescaping {
age += 20
}
return age
}
func clouserNoescaping(_ f: @escaping () -> Void){
completeHandler = f
}
很显然这里发生了值的捕获
由此看来系统对这种变量/闭包的生命周期和函数一致的情况下做了优化,不会去捕获局部变量,而是直接使用,这样就不会有循环引用的情况发生
综上所述,非逃逸闭包的优势具有一下优势
- 不会产生循环引用,函数作用域内释放
- 编译器更多性能优化 (如堆空间的分配,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