第九节课:闭包(二)
闭包补充
上节课我们看了捕获一个变量的内存结构,如果捕获的是两个变量的值,当前内存结构是什么玩意?
func makeIncrementer(forIncrement amount: Int) -> () -> Int{
var runningTotal = 12
//内嵌函数,也是一个闭包
func incrementer() -> Int{
runningTotal += amount
return runningTotal
}
return incrementer
}
查看其IR代码
返回值仍然是void* ,swift.refcounted指针
,所以原来的仿写代码中FuntionData<T>
是不变的。
接下来我们往上看
%12 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementer12forIncrementSiycSi_tF11incrementerL_SiyFTA" to i8*), %swift.refcounted* undef }, %swift.refcounted* %8, 1
首先insertvalue
是往{ i8*, %swift.refcounted* }
这个结构体里面插入
将内嵌函数的地址放到了i8*
里面,也就是void*内存中
将 %swift.refcounted* %8
指针插入index=1的位置
接下来我们来看%8
是个什么
%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
首先swift_allocObject
开辟堆区内存空间
getelementptr
是从swift.full_boxmetadata
中偏移i32 0
,即不偏移,取结构体
然后从swift.full_boxmetadata* @metadata.3
中找出index=2
的元素地址,也就是%swift.type字段
i64 32,i64 7
分配的内存大小是32字节,然后8字节对齐
内部结构仿写
根据捕获一个变量的仿写,继续仿写捕获两个变量的情况
//2、闭包捕获多个值的原理
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
//函数返回值结构体
//BoxType 是一个泛型,最终是由传入的Box决定的
struct FunctionData<BoxType>{
var ptr: UnsafeRawPointer//内嵌函数地址
var captureValue: UnsafePointer<BoxType>
}
//捕获值的结构体
struct Box<T> {
var refCounted: HeapObject
var value: T
}
//封装闭包的结构体,目的是为了使返回值不受影响
struct VoidIntFun {
var f: () ->Int
}
//下面代码的打印结果是什么?
func makeIncrementer(forIncrement amount: Int) -> () -> Int{
var runningTotal = 0
//内嵌函数,也是一个闭包
func incrementer() -> Int{
runningTotal += amount
return runningTotal
}
return incrementer
}
var makeInc = makeIncrementer(forIncrement: 10)
var f = VoidIntFun(f: makeInc)
let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的内存空间
ptr.initialize(to: f)
//将ptr重新绑定内存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
$0.pointee
}
print(ctx.ptr)
print(ctx.captureValue)
<--打印结果-->
0x0000000100005840
0x0000000102014cd0
通过cat 查看第一个地址,即内嵌函数地址
x/8g
第二个地址
再次x/8g
查看
发现这个值跟我们的runningTotal
有点像哎~那就验证一下
将runningTotal
改成10
所以,闭包捕获两个变量时,Box
结构体内部发生了变化,修改后的仿写代码如下:
//2、闭包捕获多个值的原理
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
//函数返回值结构体
//BoxType 是一个泛型,最终是由传入的Box决定的
struct FunctionData<BoxType>{
var ptr: UnsafeRawPointer//内嵌函数地址
var captureValue: UnsafePointer<BoxType>
}
//捕获值的结构体
struct Box<T> {
var refCounted: HeapObject
//valueBox用于存储Box类型
var valueBox: UnsafeRawPointer
var value: T
}
//封装闭包的结构体,目的是为了使返回值不受影响
struct VoidIntFun {
var f: () ->Int
}
//下面代码的打印结果是什么?
func makeIncrementer(forIncrement amount: Int) -> () -> Int{
var runningTotal = 12
//内嵌函数,也是一个闭包
func incrementer() -> Int{
runningTotal += amount
return runningTotal
}
return incrementer
}
var makeInc = makeIncrementer(forIncrement: 10)
var f = VoidIntFun(f: makeInc)
let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的内存空间
ptr.initialize(to: f)
//将ptr重新绑定内存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int, Int>>.self, capacity: 1) {
$0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee)
print(ctx.captureValue.pointee.valueBox)
<!--打印结果-->
0x0000000100002b30
Box<Int>(refCounted: _7_Clourse.HeapObject(type: 0x0000000100004090, refCount1: 3, refCount2: 4), valueBox: 0x00000001006094a0, value: 10)
0x00000001006094a0
总结:
1.捕获值的原理:堆上开辟内存空间,捕获的值放到内存空间里
2.修改捕获值的时候
:修改堆空间里面的值
(11,12,13的例子)
3.闭包是一个引用类型
(地址传递),闭包的底层结构(结构体:函数的地址+捕获变量的值 == 闭包)
逃逸闭包&非逃逸闭包
定义:当闭包作为一个实际参数传递给一个函数时
,并且是在函数返回之后调用,我们就说这个闭包逃逸了。当声明一个接受闭包作为形式参数的函数时,可以在形式参数前写@escaping
来明确闭包是允许逃逸
的
- 如果用
@escaping修饰闭包后
,我们必须显示的在闭包中使用self
-
swift3.0
之后,系统默认闭包参数就是被@nonescaping
,可以通过SIL来验证
简单写一个例子
func test(by:()->()){
by()
}
查看SIL
非逃逸闭包
- 函数体内执行
- 函数执行完之后,闭包消失
逃逸闭包
- 函数返回后调用
- 延迟调用
- 作为属性存储,后面进行调用
例子:
属性存储
class HZMTeacher{
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("HZMTeaher deinit")
}
}
var t = HZMTeacher()
t.doSomething()
t.complitionHandler?(10)
如上所示,当前的complitionHandler
作为HZMTeacher
的属性,是在方法makeIncrementer
调用完成后才会调用,这时,闭包的生命周期要比当前方法的生命周期长
延迟调用
class HZMTeacher {
//定义一个闭包属性
var complitionHandler: ((Int)->Void)?
//函数参数使用@escaping修饰,表示允许函数返回之后调用
func makeIncrementer(amount: Int, handler: @escaping (Int)->Void){
var runningTotal = 0
runningTotal += amount
//赋值给属性
self.complitionHandler = handler
//延迟调用
DispatchQueue.global().asyncAfter(deadline: .now()+0.1) {
print("逃逸闭包延迟执行")
handler(runningTotal)
}
print("函数执行完了")
}
func doSomething(){
self.makeIncrementer(amount: 10) {
print($0)
}
}
deinit {
print("HZMTeacher deinit")
}
}
//使用
var t = HZMTeacher()
t.doSomething()
<--打印结果-->
函数执行完了
逃逸闭包延迟执行
10
当前方法执行的过程中不会等待闭包执行完成后再执行,而是直接返回
,所以当前闭包的生命周期要比方法长
逃逸闭包与非逃逸闭包的区别
非逃逸闭包
:一个接受闭包作为参数的函数,闭包是在这个函数结束前内被调用,即可以理解为闭包是在函数作用域结束前被调用
-
不会产生循环引用
(函数调用完成后释放捕获对象) -
编译器优化
(省略的内存管理调用,return、release) -
非逃逸闭包可以保存在栈上,而不是堆上
(官方文档说明,目前没有验证出来)
逃逸闭包
:一个接受闭包作为参数的函数,逃逸闭包可能会在函数返回之后才被调用,即闭包逃离了函数的作用域(生命周期比函数长)
-
可能会产生循环引用
(因为逃逸闭包中需要显式的引用self(猜测其原因是为了提醒开发者,这里可能会出现循环引用了),而self可能是持有闭包变量的(与OC中block的的循环引用类似)) - 一般用于异步函数返回,例如网络请求
使用建议:如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化
的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包
自动闭包
先看一个例子
func debugOutPrint(_ condition: Bool, _ message: String){
if condition {
print("debug: \(message)")
}
}
debugOutPrint(true, "Application Error Occured")
当condition
为true
时,会打印错误信息,即如果是false
,当前条件不会执行
如果字符串是在某个业务逻辑中获取的,会怎么样?
func doSomething() -> String{
print("doSomething")
return "Network Error Occured"
}
debugOutPrint(true, doSomething())
debugOutPrint(false, doSomething())
通过结果发现,传入true还是false,当前的doSomething()都会执行
,如果这个方法是一个非常耗时的操作,这里就会造成一定的资源浪费。所以为了避免这种情况,需要将当前参数修改为一个闭包
func debugOutPrint(_ condition: Bool, _ message: () -> String){
if condition {
print("cjl_debug: \(message())")
}
}
func doSomething() -> String{
print("doSomething")
return "Network Error Occured"
}
debugOutPrint(false, doSomething)
这样,我们的doSomething
方法就不会执行了
问题来了,如果就是要传一个字符串怎么办,有没有兼容的方式?
可以通过@autoclosure
将当前的闭包声明成一个自动闭包,不接收任何参数,返回值是当前内部表达式的值
。所以当传入一个String
时,其实就是将String放入一个闭包表达式中,在调用的时候返回
func debugOutPrint(_ condition: Bool, _ message: @autoclosure() -> String){
if condition {
print("debug: \(message())")
}
}
func doSomething() -> String{
print("doSomething")
return "Network Error Occured"
}
debugOutPrint(true, doSomething())
debugOutPrint(true, "Application Error Occured")
这样就兼容了两种参数形式
debugOutPrint(true, "Network Error Occured")
自动闭包相当于用{}包裹传入的对象,然后返回{}内的值
{
//表达式里的值
return "Network Error Occured"
}
总结:
逃逸闭包
:一个接受闭包作为参数的函数,逃逸闭包可能会在函数返回之后才被调用,即闭包逃离了函数的作用域(生命周期比函数长)
-
可能会产生循环引用
(因为逃逸闭包中需要显式的引用self(猜测其原因是为了提醒开发者,这里可能会出现循环引用了),而self可能是持有闭包变量的(与OC中block的的循环引用类似)) -
一般用于异步函数返回
,例如网络请求 - 如果标记为了
@escaping
,必须在闭包中显式的引用self
非逃逸闭包
:一个接受闭包作为参数的函数,闭包是在这个函数结束前内被调用,即可以理解为闭包是在函数作用域结束前被调用
-
不会产生循环引用
(函数调用完成后释放捕获对象) -
编译器优化
(省略的内存管理调用,return、release) -
非逃逸闭包可以保存在栈上,而不是堆上
(官方文档说明,目前没有验证出来)
为什么要区分@escaping 和 @nonescaping?
1、为了内存管理,闭包会强引用它捕获的所有对象
,这样闭包会持有当前对象,容易导致循环引用
2、非逃逸闭包不会产生循环引用
,它会在函数作用域内使用,编译器可以保证在函数结束时闭包会释放它捕获的所有对象
3、使用非逃逸闭包可以使编译器应用更多强有力的性能优化
,例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用
4、非逃逸闭包
它的上下文的内存可以保存在栈上而不是堆上
PS: 如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的
,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包
自动闭包相当于用{}包裹传入的对象,然后返回{}内的值