什么是闭包
维基百科中的解释:
- 在计算机科学中,闭包(
Closure
),又称词法闭包(Lexical Closure
)或函数闭包(function closures
),是在支持头等函数的编程语言中实现词法绑定的一种技术。- 闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。
func test() {
print("test")
}
上述代码,
test
是⼀个全局函数,也是⼀种特殊的闭包,只不过当前全局函数并不捕获值
func makeIncrementer() -> () -> Int {
var runningTotal = 12
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
上述代码,
incrementer
称之为内嵌函数,同时从上层函数makeIncrementer
中捕获变量runningTotal
{(age: Int) -> Int in
return age
}
上述代码,称之为闭包表达式,是⼀个匿名函数,可以从上下⽂中捕获变量和常量
闭包表达式
闭包表达式是
Swift
语法,可以更简洁的传达信息,具有以下特性:
- 利⽤上下⽂推断参数和返回值类型
- 单表达式可以隐⼠返回,既省略
return
关键字- 参数名称可简写,⽐如
$0
- 尾随闭包表达式
闭包表达式书写格式:
{(param) -> ReturnType in //函数体 }
- 作⽤域,也就是⼤括号
- 参数和返回值
- 函数体,也就是
in
之后的代码
Swift
中闭包即可以当做变量,也可以当做参数传递var closure: (Int) -> Int = {(age: Int) in return age } func test(param : (Int) -> Int){ print(param(10)) } test(param: closure)
可以将闭包声明⼀个可选类型,需要在参数和返回值外整体加
()?
//错误写法 //var closure: (Int) -> Int? //closure = nil //正确写法 var closure: ((Int) -> Int)? closure = nil
注释中的错误写法,相当于仅返回值是可选类型
可以通过
let
关键字将闭包声明为⼀个常量,⼀旦赋值后不可改变let closure: (Int) -> Int closure = {(age: Int) in 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) -> Bool in
return (item1 + item2 < item3)
}
尾随闭包写法,⼀眼看上去就知道是函数的调⽤。后⾯是⼀个闭包表达式,
{}
放在函数外⾯
var array = [1, 2, 3]
//常规写法
array.sort{(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、$1、$2...按顺序替代对应位置的参数
array.sort{ return $0 < $1 }
//单表达式可省略return关键字
array.sort{ $0 < $1 }
//使用高阶函数,指定排序规则
array.sort(by: <)
上述代码,展示了闭包表达式的好处:
- 使用尾随闭包,
{}
放在函数外⾯,提⾼可读性- 利⽤上下⽂推断参数和返回值类型
- 单表达式可以隐⼠返回,可省略
return
关键字- 参数简写,通过
$0、$1、$2...
按顺序替代对应位置的参数- 最后的代码并不是尾随闭包的写法,使用高阶函数指定排序规则,可以只传入
<
,这种方式by:
不能省略
捕获值
func makeIncrementer() -> () -> Int {
var runningTotal = 10
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
print("makeIncrementer:\(makeIncrementer()())")
print("makeIncrementer:\(makeIncrementer()())")
print("makeIncrementer:\(makeIncrementer()())")
print("--------------------")
let makeInc = makeIncrementer()
print("makeInc:\(makeInc())")
print("makeInc:\(makeInc())")
print("makeInc:\(makeInc())")
//输出以下内容:
//makeIncrementer:11
//makeIncrementer:11
//makeIncrementer:11
//--------------------
//makeInc:11
//makeInc:12
//makeInc:13
上述代码中,使用
makeIncrementer()()
调用,每次都重新分配runningTotal
变量并进行+1
,所以输出结果都是11
。但使用makeInc()
调用,内嵌函数incrementer
内部捕获了runningTotal
变量,所以输出结果是11、12、13
通过SIL代码,分析
makeIncrementer
函数是如何捕获变量的
将上述代码生成SIL文件:
swiftc -emit-sil main.swift | xcrun swift-demangle
alloc_box
官方文档说明:在堆上分配一块内存空间,存储了metadata
、refCount
、当前的value
通过断点查看汇编代码,确实调⽤了
swift_allocObject
⽅法
- 捕获变量的本质:就是在堆上开辟内存空间,将当前的变量存储到里面
- 闭包的本质:就是当前的内嵌函数,加上捕获的变量或者常量
- ⼀个闭包能够从上下⽂捕获已被定义的常量和变量。即使定义这些常量和变量的原作⽤域已经不存在, 闭包仍能够在其函数体内引⽤和修改这些值
- 每次修改捕获值的时候,修改的是堆区中的
value
值- 每次重新执⾏当前函数的时候,都会重新创建内存空间
闭包是引⽤类型
makeIncrementer
函数赋值给变量,这时变量⾥⾯存储的是什么?是函数地址吗?
这⾥通过断点查看汇编代码,无法看出
makeInc
变量存储的是什么
这时可以把
SIL
代码再降⼀级,通过IR
来观察数据的构成
IR语法
- 数组
[<elementnumber> x <elementtype>] //example alloca [24 x i8], align 8 //24个i8都是0
- 结构体
%swift.refcounted = type { %swift.type*, i64 } //表示形式 %T = type {<type list>} //这种和C语⾔的结构体类似
- 指针类型
<type> * //example i64* //64位的整形
getelementptr
指令
LLVM
中获取数组和结构体的成员,通过getelementptr
指令:<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}* <result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
创建
test.c
文件,运行一个来自LLVM
官⽹的getelementptr
指令的案例struct munger_struct { int f1; int f2; }; void munge(struct munger_struct *P) { P[0].f1 = P[1].f1 + P[2].f2; } struct munger_struct array[3]; //int main(int argc, const char * argv[]) { // // munge(array); // return 0; //}
将上述代码生成IR文件:
clang -S -fobjc-arc -emit-llvm test.c
结合以下代码理解⼀下
int array[4] = {1, 2, 3, 4}; int a = array[0];
其中
int a = array[0]
这句对应LLVM
代码应该是这样的:a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i6 4 0
结合下面这张图,可以看到第⼀个
0
,使⽤基本类型[4 * i32]
,因此返回的指针前进0 * 16字节
,也就是当前数组的⾸地址。第⼆个index
,使⽤的基本类型是i32
,返回的指针前进0字节
,也就是当前数组的第⼀个元素。返回的指针类型为i32 *
- 第⼀个索引不会改变返回指针的类型,也就是说
ptrval
前⾯的*
对应什么类型,就返回什么类型- 第⼀个索引的偏移量是由第⼀个索引的值和第⼀个
ty
指定的基本类型共同确定的- 后⾯的索引是在数组或结构体内进⾏索引
- 每增加⼀个索引,就会使该索引使⽤的基本类型和返回指针的类型去掉⼀层
GEP
作⽤于结构体时,其索引⼀定要是常量。GEP
指令只是返回⼀个偏移后的指针,并没有访问内存
了解IR语法后,回到
makeIncrementer
案例,通过IR代码,查看makeInc
变量存储的是什么
将上述代码生成IR文件:
swiftc -emit-ir main.swift | xcrun swift-demangle
找到
main.makeIncrementer()
函数
bitcast
在进⾏指针类型转换的过程中,sourceType
的位⼤⼩和destType
必须相同。如果源类型是指针,则⽬标类型也必须是相同⼤⼩的指针,转换就好像该值已存储到内存中并作为destType
类型读取⼀样。类似Swift
中的unsafeBitCast
定义
FuntionData
结构体,仿照IR
的swift.function
进行代码还原struct FuntionData<T>{ var ptr: UnsafeRawPointer var captureValue: UnsafePointer<T> }
ptr
:内嵌函数地址captureValue
:捕获值地址
定义
HeapObject
结构体,相当于swift.refcounted
struct HeapObject{ var type: UnsafeRawPointer var refCount1: UInt32 var refCount2: UInt32 }
定义
Box
结构体,相当于swift.full_boxmetadata
struct Box<T> { var refCounted: HeapObject var value: T }
refCounted
:HeapObjectvalue
:捕获值
定义完所有结构体,在使用过程中,
makeInc
无法直接绑定为FuntionData<Box<Int>>
类型,因为编译器无法推断出具体类型
定义
VoidIntFun
结构体,用结构体将函数包裹一层,并将函数设为结构体的第一个属性,目的是利用结构体地址就是首元素地址的特性struct VoidIntFun { var f: () ->Int }
声明
makeInc
结构体,传入makeIncrementer
函数,获取VoidIntFunc
类型指针ptr
,再将ptr
绑定为FunctionData<Box<Int>>
类型,最终返回指针var makeInc = VoidIntFun(f: makeIncrementer()) let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1) ptr.initialize(to: makeInc) let ctx = ptr.withMemoryRebound(to: FuntionData<Box<Int>>.self, capacity: >1) { $0.pointee } print("incrementer内嵌函数地址:\(ctx.ptr)") print("runningTotal值:\(ctx.captureValue.pointee.value)") //输出以下内容: //incrementer内嵌函数地址:0x0000000100005800 //runningTotal值:10
搜索符号:
nm
【Mach-O
路径】| grep
【地址】。在终端搜索地址0000000100005800
,记住没有前面的0x
nm /Users/x/Library/Developer/Xcode/DerivedData/LGSwiftTest-ditbotpgxmdgkigjpenpcpgbyiwt/Build/Products/Debug/LGSwiftTest | grep 0000000100005800
输出
incrementer
内嵌函数的符号0000000100005800 t _$s11LGSwiftTest15makeIncrementerSiycyF11incrementerL_SiyFTA
还原符号名称:
xcrun swift-demangle
【符号】,在终端还原符号名称,记住没有前面的_$
xcrun swift-demangle s11LGSwiftTest15makeIncrementerSiycyF11incrementerL_SiyFTA
输出
incrementer
内嵌函数$s11LGSwiftTest15makeIncrementerSiycyF11incrementerL_SiyFTA ---> partial apply forwarder for incrementer #1 () -> Swift.Int in LGSwiftTest.makeIncrementer() -> () -> Swift.Int
如果捕获的是多个值,内存布局是什么样子的?
修改案例,让makeIncrementer
函数接收一个Int
类型参数func makeIncrementer(forIncrement amount: Int) -> () -> Int { var runningTotal = 5 func incrementer() -> Int { runningTotal += amount return runningTotal } return incrementer }
将上述代码生成IR文件:
swiftc -emit-sil main.swift | xcrun swift-demangle
通过
lldb
查看内存
案例:捕获多个值
捕获
makeIncrementer
函数的入参amount
,以及函数内部的runningTotal
、val
、str
三个不同数据类型变量
struct HeapObject{
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
struct FuntionData<T>{
var ptr: UnsafeRawPointer
var captureValue: UnsafePointer<T>
}
struct Box<T1,T2,T3,T4> {
var refCounted: HeapObject
var valueBox: UnsafePointer<ValueBox<T1,T2,T3>>
var value: T4
}
struct ValueBox<T1,T2,T3> {
var obj1: ValueBoxObj<T1>
var obj2: ValueBoxObj<T2>
var obj3: ValueBoxObj<T3>
}
struct ValueBoxObj<T> {
var refCounted: HeapObject
var value: T
var type: UnsafeRawPointer
}
struct VoidIntFun {
var f: () ->Int
}
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal: Int = 3
var val: Double = 5.5
var str: String = "Zang"
func incrementer() -> Int {
runningTotal += amount
val += Double(amount)
str += String(amount)
return runningTotal
}
return incrementer
}
var makeInc = VoidIntFun(f: makeIncrementer(forIncrement: 10))
let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
ptr.initialize(to: makeInc)
let ctx = ptr.withMemoryRebound(to: FuntionData<Box<Int,Double,String,Int>>.self, capacity: 1) {
$0.pointee
}
print("内嵌函数地址:\(ctx.ptr)")
print("amount值:\(ctx.captureValue.pointee.value)")
print("runningTotal值:\(ctx.captureValue.pointee.valueBox.pointee.obj1.value)")
print("val值:\(ctx.captureValue.pointee.valueBox.pointee.obj2.value)")
print("str值:\(ctx.captureValue.pointee.valueBox.pointee.obj3.value)")
//输出以下内容:
//内嵌函数地址:0x00000001000033f0
//amount值:10
//runningTotal值:3
//val值:5.5
//str值:Zang
- 捕获值的原理:在堆上开辟内存空间,将捕获的值放到这个内存空间里
- 修改捕获值的时候,去堆上把变量拿出来,修改的就是堆空间里的值
- 闭包是一个引用类型(地址传递),底层结构是结构体,包含函数地址和捕获变量的值
函数也是引用类型
func makeIncrementer(inc: Int) -> Int {
var runningTotal = 10
return runningTotal + inc
}
var makeInc = makeIncrementer
将上述代码生成IR文件:
swiftc -emit-ir main.swift | xcrun swift-demangle
struct FuntionData{
var ptr: UnsafeRawPointer
var captureValue: UnsafeRawPointer?
}
struct VoidIntFun {
var f: (Int) -> Int
}
func makeIncrementer(inc: Int) -> Int {
var runningTotal = 10
return runningTotal + inc
}
var makeInc = VoidIntFun(f: makeIncrementer)
let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
ptr.initialize(to: makeInc)
let ctx = ptr.withMemoryRebound(to: FuntionData.self, capacity: 1){$0.pointee}
print("函数地址:\(ctx.ptr)")
print("捕获值:\(ctx.captureValue)")
//输出以下内容:
//函数地址:0x0000000100002070
//捕获值:nil
搜索符号:
nm
【Mach-O
路径】| grep
【地址】。在终端搜索地址0000000100002070
,记住没有前面的0x
nm /Users/x/Library/Developer/Xcode/DerivedData/LGSwiftTest-aqvqasiqlxeaiodejtwafdmetstw/Build/Products/Debug/LGSwiftTest | grep 0000000100002070
输出
makeIncrementer
内嵌函数的符号0000000100002070 T _$s11LGSwiftTest15makeIncrementer3incS2i_tF
还原符号名称:
xcrun swift-demangle
【符号】,在终端还原符号名称,记住没有前面的_$
xcrun swift-demangle s11LGSwiftTest15makeIncrementer3incS2i_tF
输出
makeIncrementer
内嵌函数$s11LGSwiftTest15makeIncrementer3incS2i_tF ---> LGSwiftTest.makeIncrementer(inc: Swift.Int) -> Swift.Int
函数的本质:函数是引用类型,底层结构是结构体
{函数地址,null}
。结构体只有函数地址,捕获值为nil
逃逸闭包
逃逸闭包的定义:当闭包作为⼀个实际参数传递给⼀个函数时,并且在函数返回后调⽤,我们就说这个闭包逃逸了。当我们声明⼀个接收闭包作为形式参数的函数时,可以在形式参数前写
@escaping
来明确闭包是允许逃逸的
案例1:
将闭包赋值给变量
complitionHandler
,存储后进行调用,如果⽤@escaping
修饰闭包后,在逃逸闭包中应该显示引用self
class LGTeacher {
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("LGTeaher deinit")
}
}
var t = LGTeacher()
t.doSomething()
t.complitionHandler?(10)
//输出以下内容:
//10
complitionHandler
在LGTeacher
的makeIncrementer
⽅法调⽤完成之后才会调⽤,此时闭包的⽣命周期⽐当前⽅法⽣命周期更⻓,需要将闭包声明成逃逸闭包。此案例没有打印LGTeaher deinit
,说明内部存在循环引用
案例2:
使用
DispatchQueue.global().asyncAfter
延迟调用
class LGTeacher {
func makeIncrementer(amount: Int, handler: @escaping (Int) -> Void){
var runningTotal = 0
runningTotal += amount
DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) {
handler(runningTotal)
}
}
func doSomething(){
self.makeIncrementer(amount: 10) {
print($0)
}
}
deinit {
print("LGTeaher deinit")
}
}
var t = LGTeacher()
t.doSomething()
//输出以下内容:
//LGTeaher deinit
//10
makeIncrementer
⽅法的执⾏,不会等待闭包执⾏完再执⾏,⽽是直接返回。如果闭包的⽣命周期⽐⽅法更⻓,需要将闭包声明成逃逸闭包
上述两个案例,如果不使用
@escaping
修饰,编译报错
- 逃逸闭包的使用,一般都是案例中存储、延迟这两种情况。逃逸闭包非常消耗资源,且逃逸闭包可能造成循环引用,需慎用
- 逃逸闭包中应该显示引用
self
,表示这里有可能产生循环引用,起到提示作用,没什么其他目的
非逃逸闭包
Swift 3.0
之后,系统默认闭包为非逃逸闭包,参数被@nonescaping
修饰,可以通过SIL代码查看func test(by: () -> Void) { by() }
将上述代码生成SIL文件:
swiftc -emit-sil main.swift | xcrun swift-demangle
- 生命周期与函数一致,闭包只能在函数体内执行
- 函数执行完后,闭包表达式消失
- 非逃逸闭包不会产生循环引用
- 非逃逸闭包编译器会做优化,省略掉一些内存管理调用
- 非逃逸闭包上下文保存栈上,而不是堆上(官方文档看到的,没有验证出来)
自动闭包
func debugOutPrint(_ condition: Bool , _ message: String){
if condition {
print("lg_debug:\(message)")
}
}
debugOutPrint(true, "Application Error Occured")
//输出以下内容:
//lg_debug:Application Error Occured
上述代码,只有在
conditon
为true
的时候,才会打印传入的错误信息,也就意味着false
的时候当前条件不会执⾏
如果当前的字符串,需要在某个业务逻辑功能中获取,一般会这样写:
func debugOutPrint(_ condition: Bool , _ message: String){
if condition {
print("lg_debug:\(message)")
}
}
func doSomething() -> String{
//do something and get error message
return "NetWork Error Occured"
}
debugOutPrint(true, doSomething())
//输出以下内容:
//lg_debug:NetWork Error Occured
这种写法会有⼀个问题,那就是当前的
conditon
,⽆论是true
还是false
,doSomething
⽅法都会被执⾏。如果doSomething
⽅法内部是耗时的任务操作,那么这⾥就造成了资源浪费
这时会想到把当前参数修改成⼀个闭包,以此来解决资源浪费的问题
func debugOutPrint(_ condition: Bool , _ message: () -> String){
if condition {
print("lg_debug:\(message())")
}
}
func doSomething() -> String{
//do something and get error message
return "NetWork Error Occured"
}
debugOutPrint(true, doSomething)
//输出以下内容:
//lg_debug:NetWork Error Occured
这样的修改,虽然能满⾜仅在符合条件的情况下才执行
doSomething
⽅法,但新问题⼜随之⽽来了。这⾥是⼀个闭包,如果输出的信息是一个需要从外界传⼊的String
,那又该怎么办呢?直接给方法传入String
是会编译报错的
这种情况可以使⽤
@autoclosure
关键字,将当前的表达式声明成⼀个⾃动闭包。⾃动闭包不接收任何参数,返回值是当前内部表达式的值。所以实际上我们传⼊的String
就是放在⼀个闭包表达式中,在调⽤的时候返回
func debugOutPrint(_ condition: Bool , _ message: @autoclosure () -> String){
if condition {
print("lg_debug:\(message())")
}
}
debugOutPrint(true, "Application Error Occured")
//输出以下内容:
//lg_debug:Application Error Occured
使用
@autoclosure
声明⾃动闭包,相当于将传⼊的String
使用闭包进行包裹,然后又在闭包中将String
直接进行return
。就像下面的代码一样:{ return "Application Error Occured" }
- ⾃动闭包不接收任何参数
- 返回值是内部表达式的值,在调⽤的时候返回
使用
@autoclosure
声明⾃动闭包,也可以直接传入函数,且该函数在未满足条件时,不会被执行
func debugOutPrint(_ condition: Bool , _ message: @autoclosure () -> String){
print("condition当前为:\(condition)")
if condition {
print("lg_debug:\(message())")
}
print("----------\n")
}
func doSomething() -> String{
print("执行了doSomething函数")
return "NetWork Error Occured"
}
debugOutPrint(true, doSomething())
debugOutPrint(false, doSomething())
//输出以下内容:
//condition当前为:true
//执行了doSomething函数
//lg_debug:NetWork Error Occured
//----------
//
//condition当前为:false
//----------