iOS-Swift 独孤九剑:七、闭包的本质

·  阅读 606
iOS-Swift 独孤九剑:七、闭包的本质

一、函数类型

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

同名函数参数不同.png

那么函数类型的本质是什么呢,我们打开源码,在 Metadata.h 文件中找到 TargetFunctionTypeMetadata:

TargetFunctionTypeMetadata.png

注意看,TargetFunctionTypeMetadata 继承自 TargetMetadata,那么它必然有 Kind,而它自身又拥有 Flags 和 ResultType,ResultType 是返回值类型的元数据。接下来我们看到 getParameters 函数,这个函数通过 reinterpret_cast 将 (this + 1) 强制转换成 Parameter * 类型,注意!它返回的是指针类型。所以这个函数返回的是一块连续的内存空间,这一块连续的内存空间存储的是 Parameter 类型的数据。

关于 (this + 1) 可以参考《元类型以及 Mirror 源码和 HandyJson 分析还原枚举、结构体、类的 Metadata》这篇文章的第五点。那么 TargetFunctionTypeMetadata 的结构还原如下:

struct TargetFunctionTypeMetadata {
    var Kind: Int
    var Flags: Int
    var ResultType: Any.Type
    var parameters: ParametersBuffer<Any.Type>
}

struct ParametersBuffer<Element>{
    var element: Element

    mutating func buffer(n: Int) -> UnsafeBufferPointer<Element> {
        return withUnsafePointer(to: &self) {
        let ptr = $0.withMemoryRebound(to: Element.self, capacity: 1) { start in
            return start
        }
        return UnsafeBufferPointer(start: ptr, count: n)
        }
    }

    mutating func index(of i: Int) -> UnsafeMutablePointer<Element> {
        return withUnsafePointer(to: &self) {
            return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: Element.self).advanced(by: i))
        }
    }
}
复制代码

TargetFunctionTypeMetadata 的结构还原出来了之后,我们想获取参数的个数,在源码中是这么获取的:

getNumParameters.png

Flags 的类型是 TargetFunctionTypeFlags,TargetFunctionTypeFlags 的结构如下:

TargetFunctionTypeFlags.png

它的 getNumParameters 方法如下:

getNumParameters 的实现.png

TargetFunctionTypeFlags 存储的是一些掩码信息,通过阅读这个源码,得知 Data & NumParametersMask 得到的就是参数的个数,那么这个 Data 不就是 Flags 么,因为 TargetFunctionTypeFlags 只有 Data 一个成员变量。

此时,将 getNumParameters 函数还原出来的结果如下:

func getNumParameters() -> Int { self.Flags & 0x0000FFFF }
复制代码

接下来我们试着打印 TargetFunctionTypeMetadata 存储的信息,代码如下:

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

let functionType = unsafeBitCast(type(of: add) as Any.Type, to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)
let numParameters = functionType.pointee.getNumParameters()

print("函数参数的个数:\(numParameters)")
for i in 0..<numParameters {
    print("第\(i)个参数的类型:\(functionType.pointee.parameters.index(of: i).pointee)")
}
print("函数参数的返回值类型:\(functionType.pointee.ResultType)")
复制代码
打印结果:
函数参数的个数:20个参数的类型:Double1个参数的类型:Int
函数参数的返回值类型:Double
复制代码

二、闭包表达式

在 Swift 中,可以通过 func 定义一个函数,也可以通过闭包表达式定义一个函数。

1. 闭包表达式的书写

闭包表达式是由花括号、参数列表、返回值类型、in 以及函数体构成的,其书写如下:

{
    (参数列表) -> 返回值类型 in
    函数体代码
}
复制代码

通过 {} 的方式开始,然后 in 前面的分别是参数列表和返回值类型,in 的后面是函数体代码。

闭包表达式定义的函数如下:

var add = {
    (a: Int, b: Int) -> Int in
    return a + b
}
复制代码

func 定义的函数如下:

func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}
复制代码

不管那种方式,在调用的时候都是一样的,结果也是一样的。

print(add(10, 20)) // 30
复制代码

2. 闭包表达式的简写

我们定义一个函数 - exec,这个函数接收三个参数,第一个参数和第二个参数的类型为 Int 类型,第三个参数的类型为函数类型。这个函数类型传两个 Int 类型的参数,返回 Int 类型的参数。

func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}
复制代码

以下是这个函数的几种调用方式:

// 第一种
exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
    return v1 + v2
})
// 第二种
exec(v1: 10, v2: 20, fn: { v1, v2 in
    return v1 + v2
})
// 第三种
exec(v1: 10, v2: 20, fn: { v1, v2 in
    v1 + v2
})
// 第四种
exec(v1: 10, v2: 20, fn: { $0 + $1 })
// 第五种
exec(v1: 10, v2: 20, fn: +)
复制代码
  • 第一种写法没有任何的简写,闭包表达式的参数名、参数类型、返回值类型和函数体全部写上。

  • 第二种写法相对于第一种写法省略了参数类型和返回值类型。

  • 第三种写法相对于第二种写法省略了 return,在 Swift 中,如果函数中返回值的代码很简单的话可以省略 return(就是在函数体中只有一句 return 的代码)。

  • 第四种写法相对于第三种写法省略了参数名和 in,因为函数体比较简单,所以可以直接用 00 和 1 分别代表 v1 和 v2。

  • 第五种写法相对于第四种写法省略了函数体,因为函数体的实现过于简单,只是两个参数相加,所以可以直接用 + 表示。

3. 尾随闭包

如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性。尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式。 例如第 3 点的 exec 函数,调用的时候,可以使用尾随闭包:

exec(v1: 10, v2: 20) { v1, v2 in
    return v1 + v2
}
复制代码

如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数后面加圆括号。

func exec(fn: (Int, Int) -> Int) {
    print(fn(10, 20))
}

exec(fn: {$0 + $1})
exec() {$0 + $1}
exec {$0 + $1}
复制代码

三、闭包

一个函数和它所捕获的变量\常量环境组合起来,称为闭包。一般指定义在函数内部的函数,它捕获的是外层函数的局部变量\常量。

1. 捕获局部变量

如下代码,我们用 typealias 定义一个函数类型 Fn,再定义一个 getFn 的函数,如下:

闭包的环境组合.png

图中红色框框的,组合起来称之为闭包。num 是一个局部变量,调用 getFn 函数来看一下它的变化:

let fn = getFn()
print(fn(1))   // 1
print(fn(1))   // 2
print(fn(1))   // 3
复制代码

每次调用 fn,传的值都为 1,但是每次打印都是不一样的,感觉就像是 num 这个局部变量被放到堆空间存储起来了,在每次调用的时候都是 num += 1。我们通过汇编来看一下 getFn 的调用情况。

getFn汇编.png

注意看,在 getFn 的汇编中,调用了 swift_allocObject 方法,这个方法在干什么,在申请并分配堆空间的内存。所以实际上闭包会开辟堆空间的内存,把 num 的值放到堆空间上,当每次调用 fn 的时候,都会去堆空间访问这个值,然后进行 += 的操作。

怎么知道这个 num 值在堆空间呢,在汇编调试中的 swift_allocObject 方法后面打下一个断点,然后读取开辟堆空间的内存,如图:

读取 rax 的内存.png

接下来我们将断点打在 return num 处,然后将断点过掉,格式化输出 0x0000000101019d10,如图:

return num.png

num 值的存储.png

格式化输出堆空间的内存后发现,num 的值存储在 0x101019d20 这个内存地址,它确实存储在堆空间。

2. 捕获全局变量

当函数捕获一个局部变量/常量时,会开辟堆空间的内存去存储这个局部变量/常量,那如果捕获的是一个全局变量呢,会开辟堆空间吗。代码如下:

num 全局变量.png

我们将断点打在 return plus 和 return num 处,先来来看一下 getFn 在汇编的调用情况。

全局变量后的 getFn.png

可以看到,在 getFn 的汇编里并没有产生任何的堆空间开辟,它是直接将 plus 函数的地址返回出去,我们接下来再来看一下 plus 函数的汇编代码:

plus 全局变量的调用情况.png

注意看,在 plus 函数中,它是直接拿到全局变量 num 直接修改的。所以函数不会去捕获全局变量/常量,因此这种行为严格上也不叫做闭包。

四、闭包的本质

在探索闭包的本质的时候,需要借助 IR 的代码进行分析,所以我们先来熟悉一下 IR 的部分语法。

1. IR 的语法

数组:

[<elementnumber> x <elementtype>]
//example
alloca [24 x i8], align 8 24个i8都是0
alloca [4 x i32] === array
复制代码

结构体:

%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> <idx>}*
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx
复制代码

这里我们看 LLVM 官网当中的一个例子:

struct munger_struct {
    int f1;
    int f2;
};

void munge(struct munger_struct *P) {
    P[0].f1 = P[1].f1 + P[2].f2;
}

getelementptr inbounds %struct.munger_struct, %struct.munger_struct %1, i64
getelementptr inbounds %struct.munger_struct, %struct.munger_struct %1, i32

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, i32 0
复制代码

总结如下:

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

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

  • 第二个索引是在数组或者结构体内进行索引,内部偏移多少元素大小。

  • 每增加一个索引,就会使得该索引使用的基本类型和返回的指针类型去掉一层。

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

IR语法数组.png

2. IR 分析闭包

代码如下:

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 0
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }

    return plus
}

let fn = getFn()
复制代码

2.1. main 函数分析

我们将当前的 main.swift 文件编译成 main.ll 文件,编译方式及的命令在这篇文章:《方法》,生成 main.ll 文件之后,我们打开,找到 main 函数,如下:

define i32 @main(i32 %0, i8** %1) #0 {
entry:
    %2 = bitcast i8** %1 to i8*
    // 这里调用 main 函数中的 getFn 函数,它的返回值在 IR 中变成了 { i8*, %swift.refcounted* }。
    %3 = call swiftcc { i8*, %swift.refcounted* } @"main.getFn() -> (Swift.Int) -> Swift.Int"()
    %4 = extractvalue { i8*, %swift.refcounted* } %3, 0
    %5 = extractvalue { i8*, %swift.refcounted* } %3, 1
    store i8* %4, i8** getelementptr inbounds (%swift.function, %swift.function* @"main.fn : (Swift.Int) -> Swift.Int", i32 0, i32 0), align 8
    store %swift.refcounted* %5, %swift.refcounted** getelementptr inbounds (%swift.function, %swift.function* @"main.fn : (Swift.Int) -> Swift.Int", i32 0, i32 1), align 8
    ret i32 0
}
复制代码

注意看!在 %3 这一行调用了 getFn 函数,并且它的返回值是一个 { i8*, %swift.refcounted* },全局搜索这个返回值,搜索的结果如下:

%swift.function = type { i8*, %swift.refcounted* }
%swift.refcounted = type { %swift.type*, i64 }
%swift.type = type { i64 }
%swift.full_boxmetadata = type { void (%swift.refcounted*)*, i8**, %swift.type, i32, i8* }
复制代码

根据 IR 的语法进行分析:

  • { i8*, %swift.refcounted* } 是一个结构体,这个结构体包含两个成员变量,分别为 i8* 类型的成员和 %swift.refcounted* 类型的成员。

  • %swift.refcounted* 是一个结构体指针,它的结构为 { %swift.type*, i64 },这个结构体包含两个成员变量,分别为 %swift.type* 类型的成员和 i64 类型的成员。

  • %swift.type* 是一个结构体指针,它的结构为 { i64 },它只包含 i64 类型的成员变量。

  • %swift.full_boxmetadata 应该是一个独属于闭包的 metadata,在下面的开辟堆空间的时候,swift_allocObject 传的就是这个玩意儿。

2.2. getFn 函数分析

getFn 的函数实现如下:

define hidden swiftcc { i8*, %swift.refcounted* } @"main.getFn() -> (Swift.Int) -> Swift.Int"() #0 {
entry:
    %num.debug = alloca %TSi*, align 8
    %0 = bitcast %TSi** %num.debug to i8*
    call void @llvm.memset.p0i8.i64(i8* align 8 %0, i8 0, i64 8, i1 false)

    // 调用 swift_allocObject,创建一个实例,根据第一篇的《结构体与类》,中得知,它返回的是一个 HeapObject * 的结构体指针。所以,%swift.refcounted* 应该是一个 HeapObject *。
    // swift_allocObject 的第一个参数要求传的是 metadata,那么 i64 24 和 i64 7 对应的应该是分配内存的大小和内存对齐。
    %1 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #1

    // 将 swift_allocObject 返回的 HeapObject * 强制转换成 { %swift.refcounted, [8 x i8] }
    %2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*

    // 将局部变量 num 的值存储到 { %swift.refcounted, [8 x i8] } 这个结构体。
    %3 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %2, i32 0, i32 1
    %4 = bitcast [8 x i8]* %3 to %TSi*
    store %TSi* %4, %TSi** %num.debug, align 8
    %._value = getelementptr inbounds %TSi, %TSi* %4, i32 0, i32 0
    store i64 0, i64* %._value, align 8
    %5 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %1) #1
    call void @swift_release(%swift.refcounted* %1) #1

    // insertvalue 是插入、存储值。
    // bitcast 是强制转换的意思,将 plus 函数的地址强制转换成 i8*,然后将这个值插入到 { i8*, %swift.refcounted* } 这个结构体的 i8*。
    // 然后将上面的 { %swift.refcounted, [8 x i8] } 存储到 { i8*, %swift.refcounted* } 这个结构体的 %swift.refcounted* 这个位置。
    %6 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (i64, %swift.refcounted*)* @"partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in main.getFn() -> (Swift.Int) -> Swift.Int" to i8*), %swift.refcounted* undef }, %swift.refcounted* %1, 1

    // 将 { i8*, %swift.refcounted* } 结构返回。
    ret { i8*, %swift.refcounted* } %6
}
复制代码

注意看 %1 这一行,调用 swift_allocObject,创建一个实例,根据第一篇的《结构体与类》,中得知,它返回的是一个 HeapObject * 的结构体指针。所以,%swift.refcounted* 应该是一个 HeapObject *。swift_allocObject 的第一个参数要求传的是 metadata,那么 i64 24 和 i64 7 对应的应该是分配内存的大小和内存对齐。

中间的部分做了一些强制转换和赋值 swift_allocObject 返回的 HeapObject * 最终变成了 { %swift.refcounted, [8 x i8] } 这个结构。

看到 %6 这一行。这一行在干什么,在把 plus 函数的地址和 { %swift.refcounted, [8 x i8] } 存储到 { i8*, %swift.refcounted* } 这个结构体,最后将 { i8*, %swift.refcounted* } 返回。

3. 闭包的结构还原

通过以上分析得知:

  • 闭包本质上就是一个 { i8*, %swift.refcounted* } 这样的结构,i8* 存储的是函数的地址,%swift.refcounted* 存储的是一个 box *({ %swift.refcounted, [8 x i8] })。

  • 而 box * 里有 HeapObject * 和一个 64 位的 value。

  • HeapObject * 我们就比较熟悉,分别存储 metadata 和 refcount。

闭包最终的结构就可以还原出来了,还原出来的结构如下:

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

struct Box<T>{
    var heapObject: HeapObject
    // 捕获变量/常量的值
    var value: T
}

struct HeapObject {
    var matedata: UnsafeRawPointer
    var refcount: Int
}
复制代码

用代码验证一下:

//用结构体包裹一下闭包方便作指针的转换
struct ClosureStruct {
    var closure :(Int) -> Int
}

var fn = ClosureStruct(closure: getFn())
fn.closure(10)

//fn 初始化一个ClosureStruct类型指针
let ptr = UnsafeMutablePointer<ClosureStruct>.allocate(capacity: 1)
ptr.initialize(to: fn)

//内存重新绑定为 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()
复制代码

为了验证打印出的函数地址是否就是 plus 函数的地址,我们可以在 return plus 处打一个断点,然后通过汇编来读取 plus 函数的地址,如图:

汇编查看 plus 函数的地址.png

可以看到 plus 函数的地址为 0x0000000100002c00,我们放开断点,继续执行,测试代码打印出的函数地址和堆空间的地址,如图:

验证函数地址以及补货变量的值.png

plus 函数的地址验证成功,捕获的变量的值也存储在堆空间,并且,这个值在 HeapObject 之后,所以我们还原出来的结构是正确的。

4. 捕获引用类型

捕获一个值类型,会在堆空间开辟内存,那么捕获引用类型呢,我们通过汇编来分析,代码如下:

typealias Fn = (Int) -> Int

class SHPerson {
    var age = 0
}

func getFn() -> Fn {
    let person = SHPerson()
    func plus(_ i: Int) -> Int {
        person.age += i
        return person.age
    }
    return plus
}

let fn = getFn()
复制代码

我们在 return plus 和 person.age += i 处打一个断点,先来看一下 getFn 函数的汇编实现情况:

引用类型的捕获.png

可以看到,唯一开辟的堆空间的无非就是 SHPerson 的初始化,那么它捕获的是什么呢,我们通过读取 rax 的值:0x000000010126c330,这个值就是 person 的内存地址。接下来放开断点,用我们的测试代码将闭包的结构打印出来,结构如下:

引用类型打印的结构.png

可以看到,ClosureData 中 object 存储的地址直接就是 person 内存地址。因为在初始化 SHPerson 的时候已经开辟了堆空间,没有必要再开辟一个堆空间来捕获这个 person,所以直接把 person 的内存地址直接放到 ClosureData 中,这样可以避免不必要的内存开销。

5. 捕获多个值

5.1. 分析 getFn 函数

如果捕获多个值,闭包的结构还和第 3 点还原出来的一样吗,代码如下:

typealias Fn = (Int) -> Int
func getFn() -> Fn {
    var num1 = 0
    var num2 = 0
    func plus(_ i: Int) -> Int {
        num1 += i
        num2 += (num1 + 1)
        return num2
    }

    return plus
}

let fn = getFn()
复制代码

我们将当前的 main.swift 文件编译成 main.ll 文件,编译成功之后我们直接看 getFn 函数的实现:

闭包捕获多个值.png

代码比较长,我只截了关键的部分。可以看到,在捕获多个值后,相对应的也多次调用了 swift_allocObject 方法。注意看,第一次和第二次调用 swift_allocObject 都是为了存储 num1 和 num2 的值。

比较有意思的是第三次调用 swift_allocObject,返回的实例被强制转换成了一个结构体指针:

<{ %swift.refcounted, %swift.refcounted*, %swift.refcounted* }>*
复制代码

注意看 getelementptr,第三次调用 swift_allocObject 后有两次 getelementptr。这两次的 getelementptr 在干什么,在把前两次 swift_allocObject 的结构体存储到 %13 这个结构体。

5.2. 还原闭包的结构

根据以上分析,我们将这个闭包的结构还原出来后如下:

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

struct MutiValue<T1,T2>{
    var object: HeapObject
    var value:  UnsafePointer<Box<T1>>
    var value1:  UnsafePointer<Box<T2>>
}

struct Box<T>{
    var object: HeapObject
    var value: T
}

struct HeapObject {
    var matedata: UnsafeRawPointer
    var refcount: Int
}
复制代码

测试代码如下:

//用结构体包裹一下闭包方便作指针的转换
struct ClosureStruct {
    var closure :(Int) -> Int
}

var fn = ClosureStruct(closure: getFn())
fn.closure(10)

//fn 初始化一个ClosureStruct类型指针
let ptr = UnsafeMutablePointer<ClosureStruct>.allocate(capacity: 1)
ptr.initialize(to: fn)

//内存重新绑定为 ClosureData<Box<Int>>
let ctx = ptr.withMemoryRebound(to: ClosureData<MutiValue<Int, Int>>.self, capacity: 1){
    $0.pointee
}
print("闭包的调用地址:",ctx.ptr)
print("堆空间地址:",ctx.object)
print("堆空间存储的值", ctx.object.pointee.value.pointee.value, ctx.object.pointee.value1.pointee.value)
ptr.deinitialize(count: 1)
ptr.deallocate()
复制代码
打印结果:
闭包的调用地址: 0x0000000100002840
堆空间地址: 0x000000010111f400
堆空间存储的值 10 11
复制代码

5.3.捕获单个值和多个值的闭包结构总结

根据以上的分析,捕获单个值和多个值的区别就在于:

  • 单个值中,ClosureData 内存储的堆空间地址直接就是这个值所在的堆空间。

  • 而对于捕获多个值,ClosureData 内存储的堆空间地址会变成一个可以存储很多个捕获值的结构。

总的来时捕获多个值比捕获单个值多了一层包装,那么总结的代码结构如下:

// 捕获单个值的 ClosureData
struct ClosureData<Box> {
    /// 函数地址
    var ptr: UnsafeRawPointer
    /// 存储捕获堆空间地址的值
    var object: UnsafePointer<Box>
}

// 捕获多个值的 ClosureData
struct ClosureData<MutiValue>{
    /// 函数地址
    var ptr: UnsafeRawPointer
    /// 存储捕获堆空间地址的值
    var object: UnsafePointer<MutiValue>
}

struct MutiValue<T1, T2, ......>{
    var object: HeapObject
    var value:  UnsafePointer<Box<T1>>
    var value1:  UnsafePointer<Box<T2>>
    // 更多的 value
    ......
}

struct Box<T>{
    var object: HeapObject
    var value: T
}

struct HeapObject {
    var matedata: UnsafeRawPointer
    var refcount: Int
}
复制代码

五、逃逸闭包

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

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

逃逸闭包1.png

  • 当闭包异步执行,导致函数完成时闭包生命周期被延长。 逃逸闭包2.png

  • 可选类型的闭包默认是逃逸闭包。 逃逸闭包3.png

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

func test() -> Int{
    var age = 10
    let completeHandler = {
        age += 10
    }

    completeHandler()
    return age
}
复制代码

逃逸闭包所需的条件:

  • 作为函数的参数传递。

  • 当前闭包在函数内部异步执行或者被存储。

  • 函数结束,闭包被调用,闭包的生命周期未结束。

六、自动闭包

@autoclosure 是一种自动创建的闭包,用于将参数包装成闭包。这种闭包不接受任何参数,当它被调用的时候,会返回传入的值。这种便利语法让你在调用的时候能够省略闭包的花括号。

什么意思呢,我们来看下面的代码:

// 如果第1个数大于0,返回第一个数。否则返回第2个数
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
    return v1 > 0 ? v1 : v2
}

print(getFirstPositive(10, 20))    // 10
print(getFirstPositive(-2, 20))    // 20
print(getFirstPositive(0, -4))     // -4
复制代码

用了一个三目运算符,判断返回的是 v1 还是 v2,接下来我添加一个测试函数,如下:

func getNum() -> Int {
    print("getNum")
    let a = 10
    let b = 20
    return a + b
}

print(getFirstPositive(10, getNum()))
复制代码
打印结果:
getNum
10
复制代码

注意看,我传一个 10 给 getFirstPositive 方法,确实也返回一个 10 了,可是却打印出了 getNum,但其实判断 v1 > 0 并不需要调用 getNum 函数,但编译器还是执行了。

这个时候我们可以把 v2 变成一个函数,也就是我们传一个函数进去,代码如下:

func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}

print(getFirstPositive(10, {
        print("test")
        return 30 }))
复制代码
打印结果:
10
复制代码

可以看到,调用 getFirstPositive 后并没有打印出 test,这就可以去优化我们的代码,避免不必要的代码执行。但是如果我们的代码比较简单,就像上面的例子,可以这么写:

print(getFirstPositive(10, {20}))
复制代码

可是这种写法就比较麻烦,每次都要加上一对花括号,那我们就可以用自动闭包去表达,如何使用自动闭包呢,代码如下:

func getFirstPositive(_ v1: Int, _ v2:@autoclosure () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}

print(getFirstPositive(10, 20))
复制代码

在 v2: 后面加上了 @autoclosure 就形成了自动闭包,我们在使用的时候也可以省略掉花括号。

  • @autoclosure 只支持 () -> T 格式的参数。

  • @autoclosure 并非只支持最后一个参数。

  • 空合并运算符(??)使用了 @autoclosure 技术。

  • 有 @autoclosure,无 @autoclosure 构成函数重载。

分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改