Swift底层探索:闭包

闭包是可以在你的代码中被传递和引用的功能性独立代码块。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。Swift 中的闭包和 C 以及 Objective-C 中的 blocks 很像,还有其他语言中的匿名函数也类似。
闭包能够捕获和存储定义在其上下文中的任何常量和变量的引用,这也就是所谓的闭合并包裹那些常量和变量,因此被称为“闭包”,Swift 能够为你处理所有关于捕获的内存管理的操作。

一、闭包定义

闭包符合如下三种形式中的一种:

  • 全局函数是一个有名字但不会捕获任何值的闭包;
  • 内嵌函数是一个有名字且能从其上层函数捕获值的闭包;
  • 闭包表达式是一个轻量级语法所写的可以捕获其上下文中常量或变量值的没有名字的闭包。

简单点说:闭包是一个捕获了上下文的常量或者变量的函数

全局函数例子

func test() {
    print("test func")
}

test函数是一个全局函数,是特殊闭包,这里没有捕获值。

内嵌函数例子

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

makeIncrementer是一个官方的例子,incrementer称为内嵌函数,同时从上层函数makeIncrementer中捕获变量runningTotal

闭包表达式例子

比如:

{ (age: Int) in
    return age
}

这个就是我们熟知的闭包表达式,是一个匿名函数,而且从上下文中捕获变量和常量。

Swift 的闭包表达式拥有简洁的风格,鼓励在常见场景中实现简洁,无累赘的语法。常见的优化包括:

  • 利用上下文推断形式参数和返回值的类型;
  • 单表达式的闭包可以隐式返回(省略return关键字);
  • 简写实际参数名(比如 $0);
  • 尾随闭包语法。

二、闭包表达式

闭包表达式一般定义:

{ (parameters) -> (return type) in
    statements
  }

OC中的Block其实是一个匿名函数,同理闭包表达式要具备:

  • 作用域(也就是{});
  • 参数和返回值;
  • 函数体(in之后的代码);

闭包作为变量:

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

闭包声明为可选类型(需要表达式整体可选):

image.png

闭包声明为常量(赋值后不能改变):
image.png

闭包作为函数参数:

func test(param: () -> Int) {
    print(param())
}

var age = 18

test {() -> Int in
    age += 10
    return age
}

三、尾随闭包

当我们把闭包表达式作为函数的最后一个参数,如果闭包表达式很长,可以通过尾随闭包的书写方式提高代码的可读性。尾随闭包是一个被书写在函数形式参数的括号外面(后面)的闭包表达式,但它仍然是这个函数的实际参数。当你使用尾随闭包表达式时,不需要把第一个尾随闭包写对应的实际参数标签。函数调用可包含多个尾随闭包。

func test(_ a: Int, _ b: Int, _ c: Int, by: (_ num1: Int, _ num2 : Int, _ num3: Int) -> Bool) -> Bool {
    return by(a, b, c)
}

上面的test函数不使用尾随闭包,作为()中参数常规调用的情况下如下:

test(10, 20, 30, by: { (num1: Int, num2: Int, num3: Int) -> Bool in
    return (num1 + num2 < num3)
})

在这里我们找方法的调用比较困难,随着闭包参数增加以及嵌套会越来越难找。

我们在调用的时候编译器会默认格式化为尾随闭包:

test(10, 20, 30) { (num1, num2, num3) -> Bool in
    return (num1 + num2 < num3)
}

这里我们一看就知道是一个函数调用,后面是一个闭包表达式。{}放在了函数体外,增加了可读性。

假如我们要读一个数组排序sort:

var array = [3,1,2]
//普通调用方式
array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1 < item2 })
//闭包作为最后一个参数,有且仅有闭包一个参数,可以优化为尾随闭包并且省略小括号
array.sort{(item1 : Int, item2: Int) -> Bool in return item1 < item2 }
//省略参数类型,根据array推断类型
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
//return 是 比较表达式,返回值Bool省略
array.sort(by: {(item1, item2) in return item1 < item2 })
//单表达式省略return关键字
array.sort{(item1, item2) in item1 < item2 }
//简写实际参数
array.sort{ return $0 < $1 }
//省略return关键字
array.sort{ $0 < $1 }
//只传一个 < 默认比较 $0 和 $1,这里必须写by,by不知道传的是什么了。
array.sort(by: <)
//什么都不传,默认 <
array.sort()

个人习惯使用:

array.sort{ $0 < $1 }

四、捕获值

继续官方makeIncrementer例子,这里便于分析省略了参数。

func makeIncrementer() -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

let makeInc = makeIncrementer()

调用:

print(makeInc())
print(makeInc())
print(makeInc())
//输出
1
2
3

换一下调用方式:

print(makeIncrementer()())
print(makeIncrementer()())
print(makeIncrementer()())
//输出
1
1
1

第一种方式的输出结果也就意味者runningTotal不仅仅是一个局部变量了。分析下SIL:

image.png

  • alloc_box分配了一个变量给到了runningTotalrunningTotal存储到了堆上。
  • 闭包调用前进行了retain,调用后进行了release。

断点验证:


image.png

闭包捕获值的本质是在堆空间上开辟内存空间,将变量放到堆中。函数以及捕获的变量/常量构成闭包

总结:

  • 一个闭包能够偶从上下文捕获已被定义的常量和变量。即使定义这些常量和变量的原作用域已经不存在了,闭包仍能够在其函数体内引用和修改这些值;
  • 每次修改捕获值的时候,修改的都是堆区中的value;
  • 每次重新执行当前函数的时候,都会重新创建内存空间。

五、闭包是引用类型

在上面的例子中,将函数赋值给变量,那么这个时候变量中存储的是什么? 通过lldb打印不能很好地看出来存储的是什么, 从SIL中看不出返回值是什么。这个时候可以通过IR观察下数据的构成。

IR语法

数组

// 元素数量    数据类型 
[ x ]
//example,iN:多少位的整形
alloca [24 x i8], align 8   24个i8都是0,24字节

结构体

//结构体名称 结构体成员列表
%T = type {} // 这种和C语言结构体类似
//example
%swift.refcounted = type { %swift.type*, i64 }

指针

 *

//example
i64* //64位整形

getelementptr指令
LLVM中获取数组和结构体的成员,通过getelementptr,语法规则如下:

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

看一个LLVM官网当中的例子:

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[]) {
    // insert code here...
    munge(array);
    return 0;
}

为了方便阅读,注释掉main生成IR文件:

rm -rf ${SRCROOT}/main.ll
clang ${SRCROOT}/IRTest/main.c -emit-llvm -S -c >> ./main.ll && open main.ll

IR代码:

;结构体
%struct.munger_struct = type { i32, i32 }
;array 数量3 结构体类型
@array = common global [3 x %struct.munger_struct] zeroinitializer, align 16
; Function Attrs: noinline nounwind optnone ssp uwtable
define void @munge(%struct.munger_struct* %0) #0 {
  ;创建内存空间,存放结构体首地址。%2是一个二级指针
  %2 = alloca %struct.munger_struct*, align 8
  store %struct.munger_struct* %0, %struct.munger_struct** %2, align 8 
  %3 = load %struct.munger_struct*, %struct.munger_struct** %2, align 8
  ;第一个结构体
  %4 = getelementptr inbounds %struct.munger_struct, %struct.munger_struct* %3, i64 1
  ;第一个结构体第一个元素
  %5 = getelementptr inbounds %struct.munger_struct, %struct.munger_struct* %4, i32 0, i32 0
  %6 = load i32, i32* %5, align 4
  %7 = load %struct.munger_struct*, %struct.munger_struct** %2, align 8
  ;第二个结构体
  %8 = getelementptr inbounds %struct.munger_struct, %struct.munger_struct* %7, i64 2
  ;第二个结构体第二个元素
  %9 = getelementptr inbounds %struct.munger_struct, %struct.munger_struct* %8, i32 0, i32 1
  %10 = load i32, i32* %9, align 4
  %11 = add nsw i32 %6, %10
  ;取到结构体基地址,数组的首地址
  %12 = load %struct.munger_struct*, %struct.munger_struct** %2, align 8
  ;index = 0,基地址偏移0。0 *结构体大小。拿到结构体的第0个地址。
  %13 = getelementptr inbounds %struct.munger_struct, %struct.munger_struct* %12, i64 0
  ;i32 0 相对于自己偏移0。拿到结构体首地址,第二个i32 0 拿到结构体第一个元素
  %14 = getelementptr inbounds %struct.munger_struct, %struct.munger_struct* %13, i32 0, i32 0
  store i32 %11, i32* %14, align 4
  ret void
}
image.png
 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, i64 0

也就是下面这样的图:


image.png

第一个指针使用基本数据类型[4 * i32],返回的指针前进0*16字节,也就是数组首地址。第二个index,返回基本类型i32前进0字节,就是当前数组第一个元素。返回的指针类型为i32*

  • 第一个索引(%struct.munger_struct* %13, i32 0)不改变返回的指针类型(%14 = getelementptr inbounds %struct.munger_struct, %struct.munger_struct* %13, i32 0, i32 0%14最终存储i32)。ptrval(ptrval这里对应%13)前面的*对应什么类型(%struct.munger_struct)就返回什么类型。
  • 第一个索引的偏移量由第一个索引的值(i32 0)和第一个ty(%struct.munger_struct* %13)指定的基本类型共同确定。也就是%struct.munger_struct* %13, i32 0
  • 后面的索引都是在数组/结构体内进行索引。
  • 每增加一个索引,就会使得该索引使用的基本类型和返回的指针类型去掉一层。(也就是上面的例子,第一个索引是[4 * i32]去掉一层是i32)。

GEP作用于结构体时,索引一定要是常量。该指令只是返回一个偏移后的指针,并没有访问内存。

闭包IR分析

回到makeIncrementer的例子,分析下makeIncrementerIR

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  %3 = call swiftcc { i8*, %swift.refcounted* } @"main.makeIncrementer() -> () -> 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.makeInc : () -> Swift.Int", i32 0, i32 0), align 8
  store %swift.refcounted* %5, %swift.refcounted** getelementptr inbounds (%swift.function, %swift.function* @"main.makeInc : () -> Swift.Int", i32 0, i32 1), align 8
  ret i32 0
}
define hidden swiftcc { i8*, %swift.refcounted* } @"main.makeIncrementer() -> () -> Swift.Int"() #0 {
entry:
  %runningTotal.debug = alloca %TSi*, align 8
  %0 = bitcast %TSi** %runningTotal.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %0, i8 0, i64 8, i1 false)
  ;%1是swift_allocObject分配的内存空间
  %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
  ;8字节存值,按位转换创建出来的HeapObject
  %2 = bitcast %swift.refcounted* %1 to <{ %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** %runningTotal.debug, align 8
  ;_value是%TSi类型的地址空间
  %._value = getelementptr inbounds %TSi, %TSi* %4, i32 0, i32 0
  ;把0存储到 _value,也就是[8 x i8]连续的地址空间中。
  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
  ;往结构体中插入值。
  ;`bitcast (i64 (%swift.refcounted*)* @"partial apply forwarder for incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int" to i8*)` 
  ;bitcast 按位转换 把内嵌函数incrementer为void*存到`i8*`内存空间中。
  ;之后插入 %swift.refcounted* %1
  %6 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"partial apply forwarder for incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int" to i8*), %swift.refcounted* undef }, %swift.refcounted* %1, 1
  ;返回一个结构体`%6`,结构体包含一个 `i8* `和 `%swift.refcounted*`
  ret { i8*, %swift.refcounted* } %6
}

IRiN表示整数类型,i1表示布尔类型~i8*代表void*
bitcase在进行指向很转换的过程中sourceType的位大小和destType必须相同。如果sourceType为指针,则destType也必须是相同大小的指针,转换就好像是该值已存储到内存中并作为destType类型读取一样。类似于SwiftunsafeBitCast

%swift.refcounted*定义如下:

%swift.function = type { i8*, %swift.refcounted* }
;结构体,结构体包含{结构体,i64位整形}
%swift.refcounted = type { %swift.type*, i64 }; = {i64,i64} //heapObject
;结构体存放i64位整形
%swift.type = type { i64 }
%swift.full_boxmetadata = type { void (%swift.refcounted*)*, i8**, %swift.type, i32, i8* }
%TSi = type <{ i64 }>
  • makeIncrementer返回返回一个结构体%6,结构体包含一个 i8*%swift.refcounted*。这里的i8*可以理解为void*swift.refcounted*是一个结构体指针。
  • %swift.refcounted*是一个heapObject

还原下makeIncrementer返回值如下:

struct FunctionData {
    var ptr: UnsafeRawPointer
    var unKnown: UnsafeRawPointer
}

由于%swift.refcounted*是一个heapObject,还原后如下:

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

FunctionData结构体中的unKnown就是heapObjectindex1的地方插入了_value(就是捕获的值),所以unKnown的结构体如下:

struct Box {
    var refCounted:HeapObject
    var value: T
}
//function就变成了
struct FunctionData {
    var ptr: UnsafeRawPointer
    var captureValue: UnsafePointer
}

尝试绑定观察下:

image.png

改变下参数类型试试(() -> Int):

//改变下类型
let makeInc = makeIncrementer()
let ptr = UnsafeMutablePointer<() -> Int>.allocate(capacity: 1)
ptr.initialize(to: makeInc)
let funcationData = ptr.withMemoryRebound(to: FunctionData>.self, capacity: 1){ $0.pointee }
print(funcationData.ptr)
print(funcationData.captureValue)

查看下funcationData.ptr的地址

(lldb) cat address 0x00000001000029c0
&0x00000001000029c0,  partial apply forwarder for reabstraction thunk helper from @escaping @callee_guaranteed () -> (@unowned Swift.Int) to @escaping @callee_guaranteed () -> (@out Swift.Int) <+0> , ($sSiIegd_SiIegr_TRTA)SwiftClouse.__TEXT.__text

恢复下这个函数的符号,看到绑定的并不是内嵌函数:


image.png

所以最终对makeInc包装一层:

struct VoidIntFunc {
    var f:() -> Int
}

完整代码如下:

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

struct Box {
    var refCounted:HeapObject
    var value: T //捕获值
}

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

// 这里包装为了返回值不受影响。
struct VoidIntFunc {
    var f:() -> Int
}

func makeIncrementer() -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

let makeInc = VoidIntFunc(f: makeIncrementer())
//拿到包装的结构体地址
let ptr = UnsafeMutablePointer.allocate(capacity: 1)
//ptr指向f
ptr.initialize(to: makeInc)
//把f绑定到FunctionData结构体
let funcationData = ptr.withMemoryRebound(to: FunctionData>.self, capacity: 1){
    $0.pointee
}
//funcationData中第一个值ptr,也就是内嵌函数地址
print(funcationData.ptr)
//funcationData中第二个指针指向的值。也就是捕获值
print(funcationData.captureValue.pointee)

输出

0x0000000100002bd0
Box(refCounted: SwiftClouse.HeapObject(type: 0x0000000100004038, refCount1: 3, refcount2: 2), value: 0)

在终端搜索下0x0000000100002bd0对应的符号表:

nm -p /Users/**/Library/Developer/Xcode/DerivedData/SwiftClouse-fxzfztmmgwufmzgqbbajkncgvxrb/Build/Products/Debug/SwiftClouse | grep 0000000100002bd0

输出

0000000100002bd0 t _$s11SwiftClouse15makeIncrementerSiycyF11incrementerL_SiyFTA

可以看到0x0000000100002bd0就是内嵌函数的地址。

那么如果是捕获多个值呢?

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

IR代码:
返回值相同:

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

%12:

 %12 = insertvalue { i8*, %swift.refcounted* } { 
    ;强转内嵌函数地址放入 i8* 中
    i8*  bitcast (i64 (%swift.refcounted*)* @"partial apply forwarder for incrementer #1 () -> Swift.Int in main.makeIncrementer(forIncrement: Swift.Int) -> () -> Swift.Int" 
  to i8*),  %swift.refcounted* undef }, 
  ;%8 插入 index1 也就是 %swift.refcounted*
  %swift.refcounted* %8, 1

%8:

 %8 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* 
  ;类型是 full_boxmetadata。
  getelementptr inbounds (%swift.full_boxmetadata, 
  ;i32 0 相对于 %swift.full_boxmetadata 的偏移0。
  %swift.full_boxmetadata* @metadata.3, i32 0, 
  ; %swift.full_boxmetadata 地址 index2
  i32 2), 
  ;分配32字节
  i64 32, 
  ;8字节对齐
  i64 7) #2

%swift.full_boxmetadata

; index 2 取的是 %swift.type
%swift.full_boxmetadata = type { void (%swift.refcounted*)*, i8**, %swift.type, i32, i8* }
%swift.refcounted = type { %swift.type*, i64 }
;i64
%swift.type = type { i64 }

完整解读:

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  ret i32 0
}
define hidden swiftcc { i8*, %swift.refcounted* } @"main.makeIncrementer(forIncrement: Swift.Int) -> () -> Swift.Int"(i64 %0) #0 {
entry:
  %amount.debug = alloca i64, align 8
  %1 = bitcast i64* %amount.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %1, i8 0, i64 8, i1 false)
  %runningTotal.debug = alloca %TSi*, align 8
  %2 = bitcast %TSi** %runningTotal.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %2, i8 0, i64 8, i1 false)
  ;存储 amount 到 %amount.debug
  store i64 %0, i64* %amount.debug, align 8
  ;开辟空间
  %3 = 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) #2
  %4 = bitcast %swift.refcounted* %3 to <{ %swift.refcounted, [8 x i8] }>*
  %5 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %4, i32 0, i32 1
  %6 = bitcast [8 x i8]* %5 to %TSi*
  store %TSi* %6, %TSi** %runningTotal.debug, align 8
  ;获取(%swift.refcounted*  index0)地址给到_value
  %._value = getelementptr inbounds %TSi, %TSi* %6, i32 0, i32 0
  ;一、10存入_value(%swift.refcounted*  index0)也就是runningTotal
  store i64 10, i64* %._value, align 8
  %7 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %3) #2
  ;开辟空间,取swift Type 地址
  %8 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* 
  ;类型是 full_boxmetadata。
  getelementptr inbounds (%swift.full_boxmetadata, 
  ;i32 0 相对于 %swift.full_boxmetadata 的偏移0。
  %swift.full_boxmetadata* @metadata.3, i32 0, 
  ; %swift.full_boxmetadata 地址 index2
  i32 2), 
  ;分配32字节
  i64 32, 
  ;8字节对齐
  i64 7) #2
  %9 = bitcast %swift.refcounted* %8 to <{ %swift.refcounted, %swift.refcounted*, %TSi }>*
  ;index1也就是新开辟的(%swift.refcounted*)
  %10 = getelementptr inbounds <{ %swift.refcounted, %swift.refcounted*, %TSi }>, <{ %swift.refcounted, %swift.refcounted*, %TSi }>* %9, i32 0, i32 1
  ;%3存储到%10,新开辟的空间也就是%8。%3就是runningTotal 这里是**地址的地址。所以runningTotal获取要通过地址的地址。
  ;二、runningTotal存入到新开辟的 %swift.refcounted** index 1
  store %swift.refcounted* %3, %swift.refcounted** %10, align 8
  ;index2(%swift.refcounted)
  %11 = getelementptr inbounds <{ %swift.refcounted, %swift.refcounted*, %TSi }>, <{ %swift.refcounted, %swift.refcounted*, %TSi }>* %9, i32 0, i32 2
  ;取地址 index2的 %swift.refcounted 的index0
  %._value1 = getelementptr inbounds %TSi, %TSi* %11, i32 0, i32 0
  ;三、把amcount存储到_value1(新开辟的%8的index2的 %swift.refcounted 的index0),这里也就是runningTotal(**)在amcount(*)前
  store i64 %0, i64* %._value1, align 8
  call void @swift_release(%swift.refcounted* %3) #2
  ;四、index1 插入 %8 结构体(runningTotal(**)在amcount(*))
  %12 = insertvalue { i8*, %swift.refcounted* } { 
    ;强转内嵌函数地址放入 i8* 中
    i8*  bitcast (i64 (%swift.refcounted*)* @"partial apply forwarder for incrementer #1 () -> Swift.Int in main.makeIncrementer(forIncrement: Swift.Int) -> () -> Swift.Int" 
  to i8*),  %swift.refcounted* undef }, 
  ;%8(runningTotal + amount) 插入 index1 也就是 %swift.refcounted*
  %swift.refcounted* %8, 1
 ;返回值没有变
  ret { i8*, %swift.refcounted* } %12
}
  • 先存储runningTotal(**)再存储amcount(*)
  • 最终一起插入结构体%swift.refcounted*中包装返回{ i8*, %swift.refcounted* }
    再看下ptrcaptureValue验证下:
let makeInc = VoidIntFunc(f: makeIncrementer(forIncrement: 20))
let ptr = UnsafeMutablePointer.allocate(capacity: 1)
ptr.initialize(to: makeInc)
let funcationData = ptr.withMemoryRebound(to: FunctionData>.self, capacity: 1){
    $0.pointee
}
print(funcationData.ptr)
print(funcationData.captureValue)

2个捕获值的情况

1个捕获值的情况

再还原下结构体box:

struct Box {
    var refCounted:HeapObject
    var valueBox: UnsafePointer
    var value: T2 //捕获
}

调用修改:

let funcationData = ptr.withMemoryRebound(to: FunctionData>.self, capacity: 1){
    $0.pointee
}

验证:


image.png

多捕获几个值看看:

func makeIncrementer(_ amount: Int, _ amount1: Int) -> () -> Int {
    var runningTotal = 1
    var runningTota1 = 2
    func incrementer() -> Int {
        runningTotal += amount
        runningTota1 += amount1
        return runningTotal
    }
    return incrementer
}

let makeInc = VoidIntFunc(f: makeIncrementer(10,11))
let ptr = UnsafeMutablePointer.allocate(capacity: 1)
ptr.initialize(to: makeInc)
let funcationData = ptr.withMemoryRebound(to: FunctionData>.self, capacity: 1){
    $0.pointee
}
print(funcationData.ptr)
print(funcationData.captureValue)
image.png
  • 按捕获的顺序存储,局部变量是指针的指针,参数是指针。

总结:

  • 捕获值原理的本质是在堆区开辟一块空间,把当前捕获的值放在开辟的空间上。相当于把值用box包装了一层。
  • 修改捕获值的时候去堆上把变量拿出来
  • 闭包是引用类型(地址传递),闭包的底层结构是结构体:FuncationData存储了函数地址和捕获值地址

函数也是引用类型

func makeIncrementer(amount: Int) -> Int {
    let runningTotal = 10
    return runningTotal + amount
}
var makeInc = makeIncrementer

对应的IR:

;i8*和 %swift.refcounted*
%swift.function = type { i8*, %swift.refcounted* }
define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  ;函数地址转换为void* 放到 %swift.function 结构体第一个元素 index0
  store i8* bitcast (i64 (i64)* @"main.makeIncrementer(amount: Swift.Int) -> Swift.Int" to i8*), i8** getelementptr inbounds (%swift.function, %swift.function* @"main.makeInc : (Swift.Int) -> Swift.Int", i32 0, i32 0), align 8
  ;index1 存 null
  store %swift.refcounted* null, %swift.refcounted** getelementptr inbounds (%swift.function, %swift.function* @"main.makeInc : (Swift.Int) -> Swift.Int", i32 0, i32 1), align 8

代码验证:

struct FunctionData {
    var ptr: UnsafeRawPointer //内嵌函数地址
    var captureValue: UnsafeRawPointer? //捕获值地址
}

// 这里包装为了返回值不受影响
struct VoidIntFunc {
    var f:(Int) -> Int
}

func makeIncrementer(amount: Int) -> Int {
    let runningTotal = 10
    return runningTotal + amount
}

let makeInc = VoidIntFunc(f: makeIncrementer)
let ptr = UnsafeMutablePointer.allocate(capacity: 1)
ptr.initialize(to: makeInc)
let funcationData = ptr.withMemoryRebound(to: FunctionData.self, capacity: 1){
    $0.pointee
}
print(funcationData.ptr)
print(funcationData.captureValue)
image.png

函数的本质传递的是结构体{函数地址,null},结构体只有指针,捕获值为null。所以函数是引用类型。

六、逃逸闭包

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

非逃逸闭包

func makeIncrementer(amount: Int, handler: (Int) -> Void) {
    handler(amount)
}

对应的SIL

image.png

非逃逸闭包(@noescaping
1.函数体内执行;
2.函数执行完之后,闭包表达式消失。

  • 非逃逸闭包不会产生循环引用;
  • 非逃逸闭包编译器会做优化,省略掉一些内存管理调用;
  • 非逃逸闭包上下文会优化保存在栈上而不是堆上;(这个没有验证出来)

逃逸闭包

存储调用

class HotpotCat {
    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 hp = HotpotCat()
hp.doSomething()
hp.complitionHandler?(10)
10

这里hp并没有释放。

class HotpotCat {    
    func makeIncrementer(amount: Int, handler: @escaping (Int) -> Void) {
        var runningTotal = 0
        runningTotal += amount
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            handler(runningTotal)
        }
    }
    
    func doSomething() {
        self.makeIncrementer(amount: 10) {
            print($0)
        }
    }
    deinit {
        print("LGTeaher deinit")
    }
}

//didfinishlaunch中调用(runloop)
 var hp = HotpotCat()
 hp.doSomething()
10
LGTeaher deinit

这里hp释放了。

逃逸闭包(@escaping
1.闭包作为参数传递给函数;
2.函数返回之后调用。(延迟调用/存储,后续调用)
由于逃逸闭包很耗资源,除了上面两种情况尽量不要声明为逃逸闭包。逃逸闭包显示引用self提示有可能造成循环引用,自己注意解决。

七、自动闭包

自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值。好处在于通过写普通表达式代替显式闭包而使你省略包围函数形式参数的括号。

滥用自动闭包会导致你的代码难以读懂。上下文和函数名应该写清楚求值是延迟的。如果你想要自动闭包允许逃逸,就同时使用 @autoclosure 和 @escaping 标志。

func myDebugPrint(_ condition: Bool , _ message: String){
    if condition {
        print("debug:\(message)")
    }
}

myDebugPrint(true, "Application Error Occured")

上面例子比如条件控制打印一些log信息。条件为true的时候打印。message是一个字符串,那么如果message是别的功能模块生成的呢?

func myDebugPrint(_ condition: Bool , _ message: String){
    if condition {
        print("debug:\(message)")
    }
}

func test() -> String {
    return "Application Error Occured"
}

myDebugPrint(true, test())

这个时候如果条件为falsetest函数依然会调用,本质上就造成了浪费。
message改成闭包在条件为true的时候执行闭包

func myDebugPrint(_ condition: Bool , _ message: () -> String){
    if condition {
        print(message())
    }
}

func test() -> String {
    return "Application Error Occured"
}

myDebugPrint(true, test)

这个时候基本完美了,但如果这块模块是一个公用模块,是不是应该兼容messageString的情况?
这个时候自动闭包就派上用场了

func myDebugPrint(_ condition: Bool , _ message: @autoclosure () -> String){
    if condition {
        print(message())
    }
}

func test() -> String {
    return "Application Error Occured"
}

myDebugPrint(false, test())
myDebugPrint(true, "Network Error")
  • 这里test()要带()和普通闭包不同,只会在conditiontrue的情况下调用test()
  • myDebugPrint(true, "Network Error") 参数相当于用闭包包裹字符串直接return:
{
    return "Network Error"
}

官方文档:闭包

你可能感兴趣的:(Swift底层探索:闭包)