Swift-08:闭包

1.分析闭包以及闭包捕获变量的原理
2.逃逸闭包 & 非逃逸闭包

一、闭包

闭包是一个捕获了全局上下文的常量或者变量的函数,通俗来讲,闭包可以是常量也可以是函数

  • 【全局函数是一种特殊的闭包】:定义一个全局函数,只是当前的全局函数并不捕获值
func test(){
    print("test")
}

  • 【函数闭包】:下面的函数是一个闭包,函数中的incrementer是一个内嵌函数,可以从makeIncrementer中捕获变量runningTotal
func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

  • 【闭包表达式 / 匿名函数】:下面是一个闭包表达式,即一个匿名函数,而且是从上下文中捕获变量和常量
//闭包表达式
{ (param) -> ReturnType in
    //方法体
}

使用闭包的好处

  • 1、利用上下文推断参数和返回值类型

  • 2、单表达式可以隐式返回,即省略return关键字

  • 3、参数名称的简写,例如 $0表示第一个参数

  • 4、尾随闭包表达式

闭包表达式

OC与swift的对比

  • OC中的Block其实是一个匿名函数,需要具备以下特点:

    • 1、作用域 {}

    • 2、参数和返回值

    • 3、函数体(in)之后的代码

  • swift中的闭包,可以当做变量,也可以当做参数传递

var clourse: (Int)->(Int) = { (age: Int) in
    return age
}

闭包表达式的使用方式

  • 【可选类型的闭包表达式】1、将闭包表达式声明成一个可选类型
//声明一个可选类型的闭包

var clourse: (Int) -> Int?
clourse = nil


var clourse: ((Int) -> Int)?
clourse = nil

  • 【闭包常量】2、通过let将闭包声明成一个常量(即一旦赋值之后就不能更改
//2、通过let将闭包声明为一个常量,即一旦赋值后就不能改变了
let clourse: (Int) -> Int
clourse = {(age: Int) in
    return age
}
//报错:Immutable value 'clourse' may only be initialized once
clourse = {(age: Int) in
    return age
}

image
  • 【闭包参数】3、将闭包作为 函数的参数
//3、将闭包作为函数的参数
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, item2, item3) -> Bool in
    return (item1 + item2 < item3)
}

  • 我们平常使用的array.sorted其实就是一个尾随闭包,且这个函数就只有一个参数,如下所示
//array.sorted就是一个尾随闭包
var array = [1, 2, 3]
//1、完整写法
array.sorted { (item1: Int, item2: Int) -> Bool in return item1 < item2}
//2、省略参数类型:通过array中的参数推断类型
array.sorted { (item1, item2) -> Bool in return item1 < item2}
//3、省略参数类型 + 返回值类型:通过return推断返回值类型
array.sorted { (item1, item2) in return item1 < item2}
//4、省略参数类型 + 返回值类型 + return关键字:单表达式可以隐士表达,即省略return关键字
array.sorted { (item1, item2) in item1 < item2}
//5、参数名称简写
array.sorted {return $0 < $1}
//6、参数名称简写 + 省略return关键字
array.sorted {$0 < $1}
//7、最简:直接传比较符号
array.sorted (by: <)

捕获一个变量

下面代码的打印结果是什么?

func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = makeIncrementer()
print(makeInc())
print(makeInc())
print(makeInc())


11
12
13

打印结果如下,从结果中可以看出,每次的结构都是在上次函数执行的基础上累加的,但是我们所知的runningTotal是一个临时变量,按理说每次进入函数都是10,这里为什么会每次累加呢? 主要原因:内嵌函数捕获了runningTotal,不再是单纯的一个变量了

  • 如果是下面这种方式调用呢?
print(makeIncrementer()())
print(makeIncrementer()())
print(makeIncrementer()())


11
11
11

为什么这种方式每次打印的结果就是同一个呢?

1、SIL分析

将上述代码通过SIL分析:

  • 1、通过alloc_box申请了一个堆上的引用计数,并将引用计数地址给了RunningTotal,将变量存储到堆上

  • 2、通过project_box从堆上取出变量

  • 3、将取出的变量交给闭包进行调用

    image

    结论:所以,捕获值的本质是 将变量存储到堆上

2、断点验证

  • 也可以通过断点来验证,在makeIncrementer方法内部调用了swift_allocObject方法

    image

总结

  • 一个闭包能够从上下文捕获已经定义的常量和变量,即使这些定义的常量和变量的原作用域不存在,闭包仍然能够在其函数体内引用和修改这些值

  • 当每次修改捕获值时,修改的是堆区中的value值

  • 当每次重新执行当前函数时,都会重新创建内存空间

所以上面的案例中我们知道:

  • makeInc是用于存储makeIncrementer函数调用的全局变量,所以每次都需要依赖上一次的结果

  • 而直接调用函数时,相当于每次都新建一个堆内存,所以每次的结果都是不关联的,即每次结果都是一致的

闭包是引用类型

这里还要一个疑问,makeInc存储的到底是什么?个人猜测存储的是runningTotal的堆区地址,下面我们通过分析来验证是否如此

但是此时我们发现,通过SIL并没有办法分析出什么,那么可以将SIL降一级,通过IR代码来观察数据的构成

在分析之前,首先来了解下IR的基本语法

IR基本语法

  • 通过以下命令将代码转换为IR文件
swiftc -emit-ir 文件名 > ./main.ll && code main.ll

例如:
- cd 文件所在路径
- swiftc -emit-ir main.swift > ./main.ll && open main.ll

  • 数组
/*
- elementnumber 数组中存放数据的数量
- elementtype 数组中存放数据的类型
*/
[ x ]


/*
24个i8都是0
- iN:表示多少位的整型,即8位的整型 - 1字节
*/
alloca [24 x i8], align 8

  • 结构体
/*
- T:结构体名称
-  :列表,即结构体的成员列表
*/
//和C语言的结构体类似
%T = type {}


/*
- swift.refcounted:结构体名称
- %swift.type*:swift.type指针类型
- i64:64位整型 - 8字节
*/
%swift.refcounted = type { %swift.type*, i64}

  • 指针类型
 *


//64位的整型 - 8字节
i64*

  • getelementptr指令
    在LLVM中获取数组和结构体的成员时通过getelementptr,语法规则如下:
 = getelementptr , * {, [inrange]  }*

 = getelementptr inbounds , * {, [inrange]  }*


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

通过下面的命令将c/c++编译成IR

clang -S -emit-llvm 文件名 > ./main.ll && code main.ll


clang -S -emit-llvm ${SRCROOT}/06-EnumTestC/main.c > ./main.ll && code main.ll

image
  • 第一个索引:%struct.munger_struct* %13, i32 0 等价于 第一个索引类型 + 第一个索引值 ==》 共同决定 第一个索引的偏移量
  • 第二个索引:i32 0

再结合图来理解

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代码应该是这样的:
/*
- [4 x i32]* array:数组首地址
- 第一个0:相对于数组自身的偏移,即偏移0字节 0 * 4字节
- 第二个0:相对于数组元素的偏移,即结构体第一个成员变量 0 * 4字节
*/
a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i64 0

  • 可以看到其中的第一个0,使用基本类型[4 x i32],因此返回的指针前进0 * 16字节,即当前数组首地址

  • 第二个index,使用基本类型 i32,返回的指针前进0字节,即当前数组的第一个元素,返回的指针类型是 i32*

    image

总结

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

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

  • 后面的索引是在数组或者结构体内进行索引

  • 每增加一个索引,就会使得该索引使用基本类型和返回的指针类型去掉一层(例如 [4 x i32] 去掉一层是 i32)

IR分析

分析IR代码

  • 查看makeIncrementer方法
    • 1、首先通过swift_allocObject创建swift.refcounted结构体
    • 2、然后将swift.refcounted转换为<{ %swift.refcounted, [8 x i8] }>*结构体(即Box)
    • 3、取出结构体中index等于1的成员变量,存储到[8 x i8]*连续的内存空间中
    • 4、将内嵌函数的地址存储到i8即void地址中
    • 5、最后返回一个结构体
image

其结构体定义如下

image

仿写

通过上述的分析,仿写其内部的结构体,然后构造一个函数的结构体,将makeInc的地址绑定到结构体中

struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函数返回值结构体
//BoxType 是一个泛型,最终是由传入的Box决定的
struct FunctionData{
    //内嵌函数地址
    var ptr: UnsafeRawPointer
    var captureValue: UnsafePointer
}

//捕获值的结构体
struct Box {
    var refCounted: HeapObject
    var value: T
}

//封装闭包的结构体,目的是为了使返回值不受影响
struct VoidIntFun {
    var f: () ->Int
}

//下面代码的打印结果是什么?
func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = VoidIntFun(f: makeIncrementer())

let ptr = UnsafeMutablePointer.allocate(capacity: 1)
//初始化的内存空间
ptr.initialize(to: makeInc)
//将ptr重新绑定内存
let ctx = ptr.withMemoryRebound(to: FunctionData>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee)


0x0000000100002bc0
Box(refCounted: _7_Clourse.HeapObject(type: 0x0000000100004038, refCount1: 3, refCount2: 2), value: 10)

  • 终端命令查找0000000100002bc0(其中0x0000000100002bc0内嵌函数的地址
nm -p /Users/chenjialin/Library/Developer/Xcode/DerivedData/07、Clourse-bsccpnlhsrkbzkdglsojfgisewnx/Build/Products/Debug/07、Clourse | grep 0000000100002bc0

其中s10_7_Clourse15makeIncrementerSiycyF11incrementerL_SiyFTA是内嵌函数的地址对应的符号

image

结论:所以当我们var makeInc2 = makeIncrementer()使用时,相当于给makeInc2就是FunctionData结构体,其中关联了内嵌函数地址,以及捕获变量的地址,所以才能在上一个的基础上进行累加

捕获两个变量的情况

上面的案例中,我们分析了闭包捕获一个变量的情况,如果是将捕获一个变量更改为捕获两个变量呢?如下所示修改makeIncrementer函数

func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 0
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

  • 查看其IR代码

    image

内部结构仿写

根据捕获一个变量的仿写,继续仿写捕获两个变量的情况

//2、闭包捕获多个值的原理
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函数返回值结构体
//BoxType 是一个泛型,最终是由传入的Box决定的
struct FunctionData{
    var ptr: UnsafeRawPointer//内嵌函数地址
    var captureValue: UnsafePointer
}

//捕获值的结构体
struct Box {
    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.allocate(capacity: 1)
//初始化的内存空间
ptr.initialize(to: f)
//将ptr重新绑定内存
let ctx = ptr.withMemoryRebound(to: FunctionData>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue)


0x0000000100002910
0x00000001040098e0

  • 通过终端命令查看第一个地址是否是内嵌函数的地址

    image

    注:(函数必须使用VoidIntFun包装下,否则转换后的地址不是内嵌函数的地址),如下所示

    image
  • 通过cat查看 第一个地址,即内嵌函数的地址

    image
    • x/8g 第二个地址

      image
    • 继续查看内存情况

      image

如果将runningTotal改成12呢?来验证是否如我们猜想的一样。事实证明,确实是存储的runningTotal

image

所以,闭包捕获两个变量时,Box结构体内部发生了变化,修改后的仿写代码如下:

//2、闭包捕获多个值的原理
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函数返回值结构体
//BoxType 是一个泛型,最终是由传入的Box决定的
struct FunctionData{
    var ptr: UnsafeRawPointer//内嵌函数地址
    var captureValue: UnsafePointer
}

//捕获值的结构体
struct Box {
    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.allocate(capacity: 1)
//初始化的内存空间
ptr.initialize(to: f)
//将ptr重新绑定内存
let ctx = ptr.withMemoryRebound(to: FunctionData>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee)
print(ctx.captureValue.pointee.valueBox)


0x0000000100002b30
Box(refCounted: _7_Clourse.HeapObject(type: 0x0000000100004090, refCount1: 3, refCount2: 4), valueBox: 0x00000001006094a0, value: 10)
0x00000001006094a0

疑问:如果是捕获3个变量呢?

  • 如下所示,是捕获三个值的内存情况

    image
  • 通过IR文件发现,从返回值倒推


ret { i8*, %swift.refcounted* } %15


%15 = insertvalue { i8*, %swift.refcounted* }
{ i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementer12forIncrement7amount2SiycSi_SitF11incrementerL_SiyFTA" to i8*),
    %swift.refcounted* undef }, %swift.refcounted* %10, 1


//与捕获两个变量相比,区别在于 i64 32 变成了 i64 40
%10 = call noalias %swift.refcounted* @swift_allocObject(
%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata.3, i32 0, i32 2),
i64 40, i64 7) #1

所以Box结构体改为

//捕获值的结构体
struct Box {
    var refCounted: HeapObject
    //这也是一个HeapObject
    var valueBox: UnsafeRawPointer
    var value1: T
    var value2: T
}

最终完整的仿写代码为

//3、捕获3个值
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函数返回值结构体
//BoxType 是一个泛型,最终是由传入的Box决定的
struct FunctionData{
    var ptr: UnsafeRawPointer//内嵌函数地址
    var captureValue: UnsafePointer
}

//捕获值的结构体
struct Box {
    var refCounted: HeapObject
    //valueBox用于存储Box类型
    var valueBox: UnsafeRawPointer
    var value1: T
    var value2: T

}

//封装闭包的结构体,目的是为了使返回值不受影响
struct VoidIntFun {
    var f: () ->Int
}
//下面代码的打印结果是什么?
func makeIncrementer(forIncrement amount: Int, amount2: Int) -> () -> Int{
    var runningTotal = 1
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += amount
        runningTotal += amount2
        return runningTotal
    }
    return incrementer
}
var makeInc = makeIncrementer(forIncrement: 10, amount2: 2)
var f = VoidIntFun(f: makeInc)

let ptr = UnsafeMutablePointer.allocate(capacity: 1)
//初始化的内存空间
ptr.initialize(to: f)
//将ptr重新绑定内存
let ctx = ptr.withMemoryRebound(to: FunctionData>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee.value1)
print(ctx.captureValue.pointee.value2)


10
2

从打印结果可以看出,正好是传入的两个参数值

总结

  • 1、捕获值原理:在堆上开辟内存空间,并将捕获的值放到这个内存空间里

  • 2、修改捕获值时:实质是修改堆空间的值

  • 3、闭包是一个引用类型(引用类型是地址传递),闭包的底层结构(是结构体:函数地址 + 捕获变量的地址 == 闭包

  • 4、函数也是一个引用类型(本质是一个结构体,其中只保存了函数的地址),例如还是以makeIncrementer函数为例

func makeIncrementer(inc: Int) -> Int{
    var runningTotal = 1
    return runningTotal + inc
}

var makeInc = makeIncrementer

分析其IR代码,函数在传递过程中,传递的就是函数的地址

image

将仿写的FunctionData进行修改

struct FunctionData{
    var ptr: UnsafeRawPointer//内嵌函数地址
    var captureValue: UnsafePointer
}

然后改版后的结构仿写如下

//函数也是引用类型
struct FunctionData{
    //函数地址
    var ptr: UnsafeRawPointer
    var captureValue: UnsafeRawPointer?
}

//封装闭包的结构体,目的是为了使返回值不受影响
struct VoidIntFun {
    var f: (Int) ->Int
}

func makeIncrementer(inc: Int) -> Int{
    var runningTotal = 1
    return runningTotal + inc
}

var makeInc = makeIncrementer
var f = VoidIntFun(f: makeInc)

let ptr = UnsafeMutablePointer.allocate(capacity: 1)
//初始化的内存空间
ptr.initialize(to: f)
//将ptr重新绑定内存
let ctx = ptr.withMemoryRebound(to: FunctionData.self, capacity: 1) {
     $0.pointee
}

print(ctx.ptr)
print(ctx.captureValue)


0x0000000100003370
nil

通过cat命令查看该地址,地址就是makeIncrementer函数的地址

image

总结

  • 一个闭包能够从上下文中捕获已经定义的常量/变量,即使其作用域不存在了,闭包仍然能够在其函数体内引用、修改

    • 1、每次修改捕获值:本质修改的是堆区中的value值

    • 2、每次重新执行当前函数,会重新创建新的内存空间

  • 捕获值原理:本质是在堆区开辟内存空间,并将捕获值存储到这个内存空间

  • 闭包是一个引用类型(本质是函数地址传递),底层结构为:闭包 = 函数地址 + 捕获变量的地址

  • 函数也是引用类型(本质是结构体,其中保存了函数的地址)

二、逃逸闭包 & 非逃逸闭包

逃逸闭包定义

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

  • 如果用@escaping修饰闭包后,我们必须显示的在闭包中使用self

  • swift3.0之后,系统默认闭包参数就是被@nonescaping,可以通过SIL来验证

    image
    • 1、执行时机:在函数体内执行

    • 2、闭包生命周期:函数执行完之后,闭包也就消失了

逃逸闭包的两种调用情况

  • 1、延迟调用

  • 2、作为属性存储,在后面进行调用

1、作为属性

闭包作为存储属性时,主要有以下几点说明:

  • 1、定义一个闭包属性

  • 2、在方法中对闭包属性进行赋值

  • 3、在合适的时机调用(与业务逻辑相关)

如下所示,当前的complitionHandler作为CJLTeacher的属性,是在方法makeIncrementer调用完成后才会调用,这时,闭包的生命周期要比当前方法的生命周期长

//*********1、闭包作为属性
class CJLTeacher {
    //定义一个闭包属性
    var complitionHandler: ((Int)->Void)?
    //函数参数使用@escaping修饰,表示允许函数返回之后调用
    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("CJLTeacher deinit")
    }
}
//使用
var t = CJLTeacher()
t.doSomething()
t.complitionHandler?(10)


10

2、延迟调用

  • 【延迟方法中使用】 1、在延迟方法中调用逃逸闭包
class CJLTeacher {
    //定义一个闭包属性
    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("CJLTeacher deinit")
    }
}
//使用
var t = CJLTeacher()
t.doSomething()


函数执行完了
逃逸闭包延迟执行
10

当前方法执行的过程中不会等待闭包执行完成后再执行,而是直接返回,所以当前闭包的生命周期要比方法长

逃逸闭包 vs 非逃逸闭包 区别

  • 非逃逸闭包:一个接受闭包作为参数的函数,闭包是在这个函数结束前内被调用,即可以理解为闭包是在函数作用域结束前被调用

    • 1、不会产生循环引用,因为闭包的作用域在函数作用域内,在函数执行完成后,就会释放闭包捕获的所有对象

    • 2、针对非逃逸闭包,编译器会做优化:省略内存管理调用

    • 3、非逃逸闭包捕获的上下文保存在栈上,而不是堆上(官方文档说明)。注:针对这点目前没有验证出来,有验证出来的童鞋欢迎留言解惑

  • 逃逸闭包:一个接受闭包作为参数的函数,逃逸闭包可能会在函数返回之后才被调用,即闭包逃离了函数的作用域

    • 1、可能会产生循环引用,因为逃逸闭包中需要显式的引用self(猜测其原因是为了提醒开发者,这里可能会出现循环引用了),而self可能是持有闭包变量的(与OC中block的的循环引用类似)

    • 2、一般用于异步函数的返回,例如网络请求

  • 使用建议:如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包

自动闭包

有下面一个例子,当conditiontrue时,会打印错误信息,即如果是false,当前条件不会执行

//1、condition为false时,当前条件不会执行
func debugOutPrint(_ condition: Bool, _ message: String){
    if condition {
        print("cjl_debug: \(message)")
    }
}
debugOutPrint(true, "Application Error Occured")

  • 如果字符串是在某个业务逻辑中获取的,会出现什么情况?
func debugOutPrint(_ condition: Bool, _ message: String){
    if condition {
        print("cjl_debug: \(message)")
    }
}
func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}


debugOutPrint(true, doSomething())
//打印结果
doSomething
cjl_debug: Network Error Occured


debugOutPrint(false, doSomething())
//打印结果
doSomething

通过结果发现,无论是传入true还是false,当前的方法都会执行,如果这个方法是一个非常耗时的操作,这里就会造成一定的资源浪费。所以为了避免这种情况,需要将当前参数修改为一个闭包

  • 【修改】:将message参数修改成一个闭包,需要传入的是一个函数
//3、为了避免资源浪费,将当前参数修改成一个闭包
func debugOutPrint(_ condition: Bool, _ message: () -> String){
    if condition {
        print("cjl_debug: \(message())")
    }
}
func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}
debugOutPrint(true, doSomething)

修改后运行结果如下

image
  • 如果此时传入一个string,又需要如何处理呢?
    可以通过@autoclosure将当前的闭包声明成一个自动闭包不接收任何参数,返回值是当前内部表达式的值。所以当传入一个String时,其实就是将String放入一个闭包表达式中,在调用的时候返回
//4、将当前参数修改成一个闭包,并使用@autoclosure声明成一个自动闭包
func debugOutPrint(_ condition: Bool, _ message: @autoclosure() -> String){
    if condition {
        print("cjl_debug: \(message())")
    }
}
func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}

debugOutPrint(true, doSomething())


debugOutPrint(true, "Application Error Occured")


doSomething
cjl_debug: Network Error Occured

cjl_debug: Application Error Occured

自动闭包就相当于

debugOutPrint(true, "Application Error Occured")

相当于用{}包裹传入的对象,然后返回{}内的值
{
    //表达式里的值
    return "Network Error Occured"
}

总结

  • 逃逸闭包:一个接受闭包作为参数的函数,逃逸闭包可能会在函数返回之后才被调用,即闭包逃离了函数的作用域,例如网络请求,需要在形参前面使用@escaping来明确闭包是允许逃逸的。

    • 一般用于异步函数的回调,比如网络请求

    • 如果标记为了@escaping,必须在闭包中显式的引用self

  • 非逃逸闭包:一个接受闭包作为参数的函数,闭包是在这个函数结束前内被调用,即可以理解为闭包是在函数作用域结束前被调用

  • 为什么要区分@escaping 和 @nonescaping

    • 1、为了内存管理,闭包会强引用它捕获的所有对象,这样闭包会持有当前对象,容易导致循环引用

    • 2、非逃逸闭包不会产生循环引用,它会在函数作用域内使用,编译器可以保证在函数结束时闭包会释放它捕获的所有对象

    • 3、使用非逃逸闭包可以使编译器应用更多强有力的性能优化,例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用

    • 4、非逃逸闭包它的上下文的内存可以保存在栈上而不是堆上

  • 总结:如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包

你可能感兴趣的:(Swift-08:闭包)