Swift底层原理探索5----闭包

闭包表达式(Closure Expression)

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

//通过func关键字定义函数
func sum(_ v1: Int, _ v2: Int) -> Int{v1 + v2}
//通过闭包表达式定义函数
var fn = {
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}

fn(10, 20)
//闭包表达式结构
{
    (参数列表) -> 返回值类型 in  
    函数代码
}

这里的in 是用来区分 函数类型信息函数体代码的,上面的书写方法不是必须的,也可以不换行,都写在一行里面,但是这样不便于代码的阅读。

闭包表达式可以在定义的时候直接使用

{
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}(10, 20)

另外需要注意一点就是闭包表达式在使用的时候,不需要写上那些在定义里面的出现的参数标签。

闭包表达式的简写

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
})
如果函数题代码就是一个单一的表达式,可以省略return关键字
exec(v1: 10, v2: 20, fn: {
    v1, v2 in v1 + v2
})
省略参数名,通过$0,$1,$2...来获取按顺序获取参数
exec(v1: 10, v2: 20, fn: {
    $0 + $1
})
如果函数体的表达式比较简单,比如这里的+运算,那么可以直接写一个+号即可,编译器也可以推断来
exec(v1: 10, v2: 20, fn: +)

闭包表达式的简写需要我们根据实际情况,确定一个合理的书写简化成都,不能为了减少代码量而一味使用最简便写法,这样会降低代码的可读性,提高维护成本,凡事过犹不及。

尾随闭包

  • 如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性
    尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}
exec(v1: 10, v2: 20) {
    $0 + $1
}
  • 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后边写圆括号
func exec(fn: (Int, Int) -> Int) {
    print(fn(1, 2))
}

exec(fn: { $0 + $1})
exec(){ $0 + $1}
exec { $0 + $1}

示例 - 数组的排序

---> 数组排序方法的定义
func sort(by areInIncreaseingOrder: (Element, Element) -> Bool)

func cmp(i1: Int, i2: Int) -> Bool {
    return i1 > i2
}
var nums = [11,4,7,23,13,90]
nums.sort(by: cmp)


--->  还可以用以下的简单写法
nums.sorted(by: {
    (i1: Int, i2: Int) -> Bool in
    return i1 < i2
})

nums.sorted(by: {
    i1, i2 in return i1 < i2
})

nums.sorted(by: {
    i1, i2 in i1 < i2
})

nums.sorted(by: {
    $0 < $1
})

nums.sorted(by: <)

nums.sorted() { $0 < $1 }

nums.sorted { $0 < $1 }

忽略参数

func exec(fn: (Int, Int) -> Int) {
    print(fn(1,2))
}
//下面这种调用方法,表示忽略参数,不管传什么参数进来都不管
exec { _,_ in 10}
image

除非你给出类型


image

闭包(Closure)

1.关于闭包的严谨定义

  • 一个函数它所捕获的变量/常量环境组合起来,称为闭包
    1. 一般指定义在函数内部的函数
    2. 一般它捕获的是外层函数的局部变量/常量
typealias Fn = (Int) -> Int
func getFn() -> Fn {
    var num = 0 // 这里num是一个局部变量
    // 函数plus是函数getFn内部的一个函数
    func plus(_ i: Int) -> Int {
        //这里plus捕获(使用)了它外层的函数的局部变量 num
        num += i
        return num
    }
    
    return plus
}// 因此这个被返回的plus 和 被其捕获的num 就形成了一个闭包

2.闭包本质的推断

上面的代码演示了闭包产生的必要条件。那么闭包的本质是什么呢?为了探讨这个问题,首先将上面的代码扩充如下

typealias Fn = (Int) -> Int
func getFn() -> Fn {
    var num = 0
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    return plus
}
var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))

首先说明,上述的代码编译正确,不会报错。那么我们按照以前函数相关的知识先来分析一下这段代码。

  • 由于num是函数getFn()内部的局部变量,在var fn = getFn()执行完成,就应该被系统回收掉了。
  • print(fn(x))的成功编译,说明fn(x)这个用法是没有语法错误的。由于var fn = getFn()说明fn这个变量接受了getFn()的返回结果。
  • getFn()内部最后返回的是函数plus,我们暂且简单认为fn就是函数plus,那么fn(x)执行肯定会调用plus()函数,也就会用到num变量。
  • 由于刚才分析我们知道,getFn()里的那个num肯定是在该函数调用结束时被回收掉,而plus()执行的时候,肯定是在getFn()函数结束之后,那么它内部用到的这个num变量从哪里来呢?

一个程序内存分布就这么几个地方:代码段、数据段、堆区、栈区。其中代码段、数据段时编译之后就固定好的,栈区在num被声明的getFn()函数结束后就被回收了,我们知道只有堆区的空间时在程序运行阶段可以用来动态分配使用的,可以猜测,应该是在某个时刻,程序把栈上的那个num变量内容转移到了堆上面。这样在var fn = getFn()执行完之后,我们还可通过print(fn(x))来继续使用num就可以得到解释了。并且还可以进一步猜测一下,print(fn(1))~print(fn(4))所使用的num应该是堆上的同一个num,因为getFn()函数只执行了一次。那么按照程序,打印的结果应该是1、3、6、10运行程序结果也确实如此
另外,这里引申出了另外一个问题,

假设fn只是简单接收了getFn()所返回的plus函数,那么plus函数是如何找到堆上的这个num变量呢?

我们有理由怀疑getFn()应该不仅仅只是简单返回了一个plus函数这么简单,那么它返回的到底是个什么东东呢?至少我们能猜得到,这个返回值,能让我们关联到plus函数,并且能够找得到对上的那个num变量。

然而,光靠猜肯定是不靠谱的,检验语法糖的唯一标准是汇编,老规矩汇编代码走一波。

3.通过汇编来窥探闭包的本质

- 不存在变量捕获行为的简单情景

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        return i
    }
    return plus //断点处
}
var fn = getFn()

fn(1)
fn(2)
fn(3)
fn(4)

上面num制作为getFn的局部变量出现,并没有被plus函数所使用,在return plus处断点,运行并输出汇编代码如下

SwiftTest`getFn():
    0x100001390 <+0>:  pushq  %rbp
    0x100001391 <+1>:  movq   %rsp, %rbp
    0x100001394 <+4>:  movq   $0x0, -0x8(%rbp)
    0x10000139c <+12>: movq   $0xa, -0x8(%rbp)
->  0x1000013a4 <+20>: leaq   0x15(%rip), %rax          ; plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:388
    0x1000013ab <+27>: xorl   %ecx, %ecx
    0x1000013ad <+29>: movl   %ecx, %edx
    0x1000013af <+31>: popq   %rbp
    0x1000013b0 <+32>: retq

首先,第四条指令0x10000139c <+12>: movq $0xa, -0x8(%rbp)很明显是在getFn函数的栈空间划定一段8个字节作为局部变量num,并且赋值10,也就是立即数$0x0a,所以对应的是这句代码var num = 10

根据汇编常识,函数的返回值一般存放在寄存器rax里面,当前这条汇编指令leaq 0x15(%rip), %rax执行完之后,rax就存上了返回值,因为是leaq指令,说明往rax里面存入的是一个内存地址,很明显,这应该就是getFn所返回的plus函数地址。我们可以在LLDB里面打印一下rax

(lldb) si
(lldb) register read rax
     rax = 0x00000001000013c0  SwiftTest`plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:388
(lldb) 

看注释,提到了plus函数,到底是不是呢,此时我们可以在plus函数内部加一个断点

image

这样继续运行程序,就会来到plus函数的内部断点,汇编内容如下

image

很明显,plus函数的地址只就是0x1000013c0,也就寄存器rax里的那个值0x00000001000013c0。我们打印一下此时的fn

p fn
() -> () $R0 = 0x00000001000013c0 SwiftTest`plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:388
(lldb) 

结果很明显,说明当前的简单场景下(plus内部没有使用外部函数的局部变量),getFn()只是简单返回了plus函数的内存地址,并且被fn接收

-发生了变量捕获行为的情景

接下来,针对 捕获外层函数的局部变量情况 代码调整如下

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    return plus //断点处
}

var fn = getFn()

fn(1)
fn(2)
fn(3)
fn(4)

我们现在在函数plus内部使用了num,在 return plus 加断点看下此时getFn()函数的汇编

SwiftTest`getFn():
    0x100001210 <+0>:  pushq  %rbp
    0x100001211 <+1>:  movq   %rsp, %rbp
    0x100001214 <+4>:  subq   $0x20, %rsp
    0x100001218 <+8>:  leaq   0x4f19(%rip), %rdi
    0x10000121f <+15>: movl   $0x18, %esi
    0x100001224 <+20>: movl   $0x7, %edx
    0x100001229 <+25>: callq  0x100005474               ; symbol stub for: swift_allocObject
    0x10000122e <+30>: movq   %rax, %rdx
    0x100001231 <+33>: addq   $0x10, %rdx
    0x100001235 <+37>: movq   %rdx, %rsi
    0x100001238 <+40>: movq   $0xa, 0x10(%rax)
->  0x100001240 <+48>: movq   %rax, %rdi
    0x100001243 <+51>: movq   %rax, -0x8(%rbp)
    0x100001247 <+55>: movq   %rdx, -0x10(%rbp)
    0x10000124b <+59>: callq  0x1000054ec               ; symbol stub for: swift_retain
    0x100001250 <+64>: movq   -0x8(%rbp), %rdi
    0x100001254 <+68>: movq   %rax, -0x18(%rbp)
    0x100001258 <+72>: callq  0x1000054e6               ; symbol stub for: swift_release
    0x10000125d <+77>: movq   -0x10(%rbp), %rax
    0x100001261 <+81>: leaq   0x138(%rip), %rax         ; partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>
    0x100001268 <+88>: movq   -0x8(%rbp), %rdx
    0x10000126c <+92>: addq   $0x20, %rsp
    0x100001270 <+96>: popq   %rbp
    0x100001271 <+97>: retq   

可以看到,有一个最关键的变化就是,getFn()函数内增加了一段swift_allocObject函数的调用,看到alloc相关的函数通常就说明进行了堆空间的动态分配。这里,应该就是我们之前猜测的用来转移num内容的那块堆空间。看到这里,我们暂时用一个猜测回答了上一个猜测的问题,下面再深入的进行分析。

image

此时我们在LLDB输出一下寄存器rax(也就是swift_allocObject函数的返回值)

(lldb) register read rax
     rax = 0x0000000100532130

这个地址就是swift_allocObject函数所动态分配的那一段堆空间的起始地址。按照当前的理解,被plus函数捕获的num变量的值就存在这个段空间上,具体怎么存的还不清楚,但是fn(1)~fn(4)的调用结果我们看出,他们用的都是这个堆空间上的同一个num变量,因此我们可以在plus函数每次调用的时候,追踪一下num的值。首先因为代码中getFn函数的那个局部变量num值是10,因此被转移到当前堆空间上的值也应该是10。我们可以在LLDB里面看一下这段堆空间的内容,由于不确定这个堆空间的实际长度,所以我先看看前40个字节的内容

在这里插入图片描述

这里我们发现这段空间的17-24 这8个字节上面正好存了一个10(也就是0x0a),如果它就是那个传说中的num,那么fn(1)~fn(4)每次执行完之后,新的num值都放到这段空间上来。我们跑一遍看看
fn(1)执行完之后

(lldb) x/5xg 0x0000000100532130
0x100532130: 0x0000000100006138 0x0000000000000002
0x100532140: 0x000000000000000b 0x0000000000000004
0x100532150: 0x0000000000000000

17-24字节上的内容:0x0b -> 11

fn(2)执行完之后

(lldb) x/5xg 0x0000000100532130
0x100532130: 0x0000000100006138 0x0000000000000002
0x100532140: 0x000000000000000d 0x0000000000000004
0x100532150: 0x0000000000000000

17-24字节上的内容:0x0d -> 13

fn(3)执行完之后

(lldb) x/5xg 0x0000000100532130
0x100532130: 0x0000000100006138 0x0000000000000002
0x100532140: 0x0000000000000010 0x0000000000000004
0x100532150: 0x0000000000000000

17-24字节上的内容:0x10 -> 16

fn(4)执行完之后

(lldb) x/5xg 0x0000000100532130
0x100532130: 0x0000000100006138 0x0000000000000002
0x100532140: 0x0000000000000014 0x0000000000000004
0x100532150: 0x0000000000000000

17-24字节上的内容:0x14 -> 20

这样,就证明了这8个字节确实是我们猜测的那段用来存储num变量的堆空间。

-那么每次调用getFn都确实会重新申请一段堆空间给num用吗?

代码再调整一下

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    return plus  //加断点1
}

var fn = getFn()
fn(1)
fn(2)//加断点2
print("Debug stop")//加断点3

var fn1 = getFn()
fn1(1)
fn1(2)//加断点4
print("Debug stop")//加断点5

首先,执行var fn = getFn()getFn被第一次调用

SwiftTest`getFn():
    0x1000011b0 <+0>:  pushq  %rbp
    0x1000011b1 <+1>:  movq   %rsp, %rbp
    0x1000011b4 <+4>:  subq   $0x20, %rsp
    0x1000011b8 <+8>:  leaq   0x4f79(%rip), %rdi
    0x1000011bf <+15>: movl   $0x18, %esi
    0x1000011c4 <+20>: movl   $0x7, %edx
    0x1000011c9 <+25>: callq  0x10000545a               ; symbol stub for: swift_allocObject
    0x1000011ce <+30>: movq   %rax, %rdx
    0x1000011d1 <+33>: addq   $0x10, %rdx
    0x1000011d5 <+37>: movq   %rdx, %rsi
    0x1000011d8 <+40>: movq   $0xa, 0x10(%rax)
->  0x1000011e0 <+48>: movq   %rax, %rdi
    0x1000011e3 <+51>: movq   %rax, -0x8(%rbp)
    0x1000011e7 <+55>: movq   %rdx, -0x10(%rbp)
    .............

按照上面用过的方法,看一下这次swift_allocObject所分配的空间地址,以及内部的num变量的值

(lldb) register read rax
     rax = 0x0000000100707c60
  
//此时堆上的num对应的地址为0x100707c70,并且num=10   
(lldb) x/4xg 0x0000000100707c60
0x100707c60: 0x0000000100006138 0x0000000000000002
0x100707c70: 0x000000000000000a 0x0000000000000000

然后继续运行至断点2断点3,查看此时的对内存

//停留在断点2,fn(1)执行完成,num=11
(lldb) x/4xg 0x0000000100707c60
0x100707c60: 0x0000000100006138 0x0000000000000002
0x100707c70: 0x000000000000000b 0x0000000000000000

//停留在断点2,fn(2)执行完成,num=14
(lldb) x/4xg 0x0000000100707c60
0x100707c60: 0x0000000100006138 0x0000000000000002
0x100707c70: 0x000000000000000d 0x0000000000000000

继续运行程序,执行var fn1 = getFn()会再次调用getFn,并走到断点1

SwiftTest`getFn():
    0x1000011b0 <+0>:  pushq  %rbp
    0x1000011b1 <+1>:  movq   %rsp, %rbp
    0x1000011b4 <+4>:  subq   $0x20, %rsp
    0x1000011b8 <+8>:  leaq   0x4f79(%rip), %rdi
    0x1000011bf <+15>: movl   $0x18, %esi
    0x1000011c4 <+20>: movl   $0x7, %edx
    0x1000011c9 <+25>: callq  0x10000545a               ; symbol stub for: swift_allocObject
    0x1000011ce <+30>: movq   %rax, %rdx
    0x1000011d1 <+33>: addq   $0x10, %rdx
    0x1000011d5 <+37>: movq   %rdx, %rsi
    0x1000011d8 <+40>: movq   $0xa, 0x10(%rax)
->  0x1000011e0 <+48>: movq   %rax, %rdi
    0x1000011e3 <+51>: movq   %rax, -0x8(%rbp)
    0x1000011e7 <+55>: movq   %rdx, -0x10(%rbp)
    ..............

同样,查看新分配的堆空间地址

(lldb) register read rax
     rax = 0x00000001005108e0

//此时堆上的num对应的地址为0x1005108f0,并且num=10
(lldb) x/4xg 0x00000001005108e0
0x1005108e0: 0x0000000100006138 0x0000000000000002
0x1005108f0: 0x000000000000000a 0x0002000000000000

然后继续运行至断点4断点5,查看此时的对内存

//停留在断点2,fn1(1)执行完成,num=11
(lldb) x/4xg 0x00000001005108e0
0x1005108e0: 0x0000000100006138 0x0000000000000002
0x1005108f0: 0x000000000000000b 0x0002000000000000

//停留在断点2,fn1(1)执行完成,num=14
(lldb) x/4xg 0x00000001005108e0
0x1005108e0: 0x0000000100006138 0x0000000000000002
0x1005108f0: 0x000000000000000d 0x0002000000000000

这样我们就看出了规律,每调用一次getFn函数,就会分配一段新的堆空间来处理num的值,相互之间互不交叉,独立了开来。

-每次对变量进行捕获时到底每次分配了多少堆空间?

因为堆空间的分配,是在函数swift_allocObject内完成的,所以到底分配了多少,应该跟该函数所传入的参数有关联。在汇编里面,函数的参数一般是在调用该函数指令之前,存放在rdi、rsi、rdx、rcx、r8、r9等几个寄存器里面(如果还有更多的参数,编译器会使用栈上的空间来协助存储),那么我们来看一下swift_allocObject的参数

image

可以看到这里有个入参是0x18 (也就是24),而我们上面的分析结果告诉我们,num就存放在swift_allocObject函数所分配堆空间的17-24个字节上面,而前面16个字节里面,前8个字节存放类型信息,后8个字节存放引用计数,堆空间里面的东西其实本质上就是实例对象,那么就必然会有类型信息和引用计数,相信不难理解
image

因此这个参数0x18应该就是函数swift_allocObject用来告诉系统所需要申请的堆空间的大小。因为在iOS/OS X系统里面,分配堆空间至少是16的倍数,所以实际上swift_allocObject结束后得到的堆空间应该是32字节,只不过实际上只用到了其中的24个字节。实际上追踪swift_allocObject的调用堆栈结果如下

    frame #0: 0x00007fff6975ace0 libsystem_malloc.dylib`malloc
    frame #1: 0x00007fff68ec0ca9 libswiftCore.dylib`swift_slowAlloc + 25
    frame #2: 0x00007fff68ec0d27 libswiftCore.dylib`swift_allocObject + 39
    frame #3: 0x00000001000011ce SwiftTest`getFn() at <compiler-generated>:0
    frame #4: 0x0000000100000e09 SwiftTest`main at main.swift:392:10
    frame #5: 0x00007fff695a4cc9 libdyld.dylib`start + 1
    frame #6: 0x00007fff695a4cc9 libdyld.dylib`start + 1

最终实际上是通过libsystem_malloc.dylib库下的malloc函数来完成动态内存分配的,至于为什么分配的空间是16的倍数请看这里

4.闭包的内存结构

ok,到此为止,基于演示代码,我们弄清楚了这么几个问题:

  • plus函数内部所捕获的那个外层函数局部变量num,实际上是把它的值存储到了堆上动态申请的一段内存空间上。
  • 动态申请的内存大小是24字节,实际获得的内存大小是32字节,前16个字节存放类型描述信息和引用计数信息,接下来的8个字节用来存放num的值,剩下的暂时空闲。
  • getFn函数每调用一次,就会动态申请一段新的堆内存来存放全新的num值。

那么var fn = getFn()中的这个fn到底是什么?因为fn(x)的运行结果说明,plus函数被调用,并且能够使用堆内存上的num变量。下面我们就探索一下,它们之间是如何关联的。

- 没有变量捕获时的fn结构

func getFn() -> (Int, Int) -> Int {
    func sum(_ v1: Int, _ v2: Int) -> Int {
        v1 + v2 //加断点2
    }
    return sum
}
var fn = getFn()
fn(1, 2) //加断点1

如上的案例里面,我们阉割掉对外层函数局部变量捕获,只简单返回函数sum。运行程序来到断点1出,现输出一下此时变量fn获得的值

(lldb) p fn 
() -> () $R0 = 0x00000001000011e0 SwiftTest`sum #1 (Swift.Int, Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int, Swift.Int) -> Swift.Int at main.swift:412

可以看出就是函数sum的地址,我们还可以在断点2出查看sum汇编代码来进一步确认,

SwiftTest`sum #1 (_:_:) in getFn():
    0x1000011e0 <+0>:   pushq  %rbp
    0x1000011e1 <+1>:   movq   %rsp, %rbp
    0x1000011e4 <+4>:   subq   $0x40, %rsp
    0x1000011e8 <+8>:   xorl   %eax, %eax
    0x1000011ea <+10>:  leaq   -0x8(%rbp), %rcx
    0x1000011ee <+14>:  movq   %rdi, -0x18(%rbp)
    ..........

看得出,sum函数的地址的确就是0x1000011e0,而fn变量所占内存的大小可以借助MemoryLayout
确定

(lldb) p MemoryLayout.size(ofValue: fn)
(Int) $R4 = 16
(lldb) p MemoryLayout.stride(ofValue: fn)
(Int) $R10 = 16

可以得到变量fn被分配了16字节内存,并使用了其中16字节。

image

根据断点位置,我们可以判断var fn = getFn()已经执行完成,fn所处的位置说明它是一个全局变量,这样我们可以很容易定位这句代码所对应的汇编
image

图中绿框的两个地址对应的是连续的两端内存空间,一共16字节,实际上根据Swift注释也可以看出,这段空间就是fn变量的内存空间。因为movq一次只能操作8个字节,所以需要对这段16字节内存空间连续两次操作才能完成赋值。而且寄存器rax此时存的只就是sum函数的地址,下面的rdx暂时为0

(lldb) register read rax
     rax = 0x00000001000011e0  SwiftTest`sum #1 (Swift.Int, Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int, Swift.Int) -> Swift.Int at main.swift:412
(lldb) register read rdx
     rdx = 0x0000000000000000

所以此时fn的空间里面,存储的就是函数sum的地址,而且暂时空闲了8个字节。这就是fn的全部。

image

汇编阅读小技巧:

  • 0xXXXX(%rip) 寻址的结果通常是全局变量(数据段)的内存地址。
  • 0xXX(%rbp) 寻址的结果通常是函数局部变量(栈空间)的内存地址。
  • 0xXX(%rax) 寻址的结果通常是堆空间(通常通过alloc系列函数动态申请)的内存地址。
  • 函数的返回值一般放在rax、rdx寄存器里面


- 进行变量捕获情况下的fn结构

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    return plus //❕断点2❕
}

var fn = getFn()//❕断点1❕
fn(1)  //❕断点3❕
print("Debug stop")

上述代码,我们在plus函数内部使用(捕获)了外部的num变量。运行代码,来到断点1处的汇编代码

SwiftTest`main:
    0x100001160 <+0>:   pushq  %rbp
    0x100001161 <+1>:   movq   %rsp, %rbp
    0x100001164 <+4>:   pushq  %r13
    0x100001166 <+6>:   subq   $0x48, %rsp
    0x10000116a <+10>:  movl   %edi, -0x24(%rbp)
    0x10000116d <+13>:  movq   %rsi, -0x30(%rbp)
->  0x100001171 <+17>:  callq  0x100001200               ; SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:388
    0x100001176 <+22>:  leaq   0x6093(%rip), %rsi        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x10000117d <+29>:  xorl   %edi, %edi
    0x10000117f <+31>:  movl   %edi, %ecx
    0x100001181 <+33>:  movq   %rax, 0x6088(%rip)        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x100001188 <+40>:  movq   %rdx, 0x6089(%rip)        ; SwiftTest.fn : (Swift.Int) -> Swift.Int + 8
    0x10000118f <+47>:  movq   %rsi, %rdi
    0x100001192 <+50>:  leaq   -0x20(%rbp), %rsi
    0x100001196 <+54>:  movl   $0x20, %edx
    0x10000119b <+59>:  callq  0x100005470               ; symbol stub for: swift_beginAccess
    0x1000011a0 <+64>:  movq   0x6069(%rip), %rax        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x1000011a7 <+71>:  movq   0x606a(%rip), %rcx        ; SwiftTest.fn : (Swift.Int) -> Swift.Int + 8
    0x1000011ae <+78>:  movq   %rcx, %rdi
    0x1000011b1 <+81>:  movq   %rax, -0x38(%rbp)
    0x1000011b5 <+85>:  movq   %rcx, -0x40(%rbp)
    0x1000011b9 <+89>:  callq  0x1000054dc               ; symbol stub for: swift_retain
    0x1000011be <+94>:  leaq   -0x20(%rbp), %rdi
    0x1000011c2 <+98>:  movq   %rax, -0x48(%rbp)
    0x1000011c6 <+102>: callq  0x100005494               ; symbol stub for: swift_endAccess
    0x1000011cb <+107>: movl   $0x1, %edi
    0x1000011d0 <+112>: movq   -0x40(%rbp), %r13
    0x1000011d4 <+116>: movq   -0x38(%rbp), %rax
    0x1000011d8 <+120>: callq  *%rax
    0x1000011da <+122>: movq   -0x40(%rbp), %rdi
    0x1000011de <+126>: movq   %rax, -0x50(%rbp)
    0x1000011e2 <+130>: callq  0x1000054d6               ; symbol stub for: swift_release
    0x1000011e7 <+135>: xorl   %eax, %eax
    0x1000011e9 <+137>: addq   $0x48, %rsp
    0x1000011ed <+141>: popq   %r13
    0x1000011ef <+143>: popq   %rbp
    0x1000011f0 <+144>: retq   

根据之前的分析,我们已经知道var fn这个全局变量占用16个字节,赋值需要两次movq指令进行操作,也就是这里

image

说明getFn()被调用之后,将返回值,放在了raxrdx这两个寄存器里面,然后通过上图里面的这两句汇编指令,完成了var fn = getFn()这一句里面的赋值操作。那么raxrdx里面到底放了什么内容,就需要进入到getFn函数取观察。我们继续运行代码,即可来到断点2,汇编如下

SwiftTest`getFn():
    0x100001200 <+0>:  pushq  %rbp
    0x100001201 <+1>:  movq   %rsp, %rbp
    0x100001204 <+4>:  subq   $0x20, %rsp
    0x100001208 <+8>:  leaq   0x4f29(%rip), %rdi
    0x10000120f <+15>: movl   $0x18, %esi
    0x100001214 <+20>: movl   $0x7, %edx
    0x100001219 <+25>: callq  0x100005464               ; symbol stub for: swift_allocObject
    0x10000121e <+30>: movq   %rax, %rdx
    0x100001221 <+33>: addq   $0x10, %rdx
    0x100001225 <+37>: movq   %rdx, %rsi
    0x100001228 <+40>: movq   $0xa, 0x10(%rax)
->  0x100001230 <+48>: movq   %rax, %rdi
    0x100001233 <+51>: movq   %rax, -0x8(%rbp)
    0x100001237 <+55>: movq   %rdx, -0x10(%rbp)
    0x10000123b <+59>: callq  0x1000054dc               ; symbol stub for: swift_retain
    0x100001240 <+64>: movq   -0x8(%rbp), %rdi
    0x100001244 <+68>: movq   %rax, -0x18(%rbp)
    0x100001248 <+72>: callq  0x1000054d6               ; symbol stub for: swift_release
    0x10000124d <+77>: movq   -0x10(%rbp), %rax
    0x100001251 <+81>: leaq   0x138(%rip), %rax         ; partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>
    0x100001258 <+88>: movq   -0x8(%rbp), %rdx
    0x10000125c <+92>: addq   $0x20, %rsp
    0x100001260 <+96>: popq   %rbp
    0x100001261 <+97>: retq 

我们从下往上逆向追溯,可以看到raxrdx的赋值轨迹如下图

在这里插入图片描述

从图中的轨迹路线,可以判断:

  • getFn返回的时候,rax里面存放了一个叫partial apply forwarder for plus的函数的地址,不知道它是不是plus的地址,但至少是跟plus有关的一个函数。
  • rdx里面存放的是swift_allocObject函数动态申请的堆空间地址,这段堆空间的作用之前已经证明过了,里面的一段空间是用来存放从栈空间捕获过来的num变量的值的

为了验证我们的判断,我们可以在上图第9句汇编码处加一个断点,将程序重新运行到该断点,输出一下swift_allocObject申请到的堆空间地址

(lldb) register read rax
     rax = 0x0000000103204310

然后运行到第23句汇编码处,查看此时rdx的值(这个是函数返回钱,rdx最后一次完成赋值)

(lldb) register read rdx
     rdx = 0x0000000103204310

没错,就是堆空间的地址。同时看一下此时rax被赋予的partial apply forwarder for plus函数的地址值是多少

(lldb) register read rax
     rax = 0x0000000100001390  SwiftTest`partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>

所以当getFn返回之后,fn所收到的值到底是什么也就一清二楚了。

- plus函数在哪里?它是如如何被调用到的?

我们来分析一下fn(1)这句代码,我们知道fn里面头8个字节放了一个partial apply forwarder for plus函数地址,后8个字节放的是捕获变量的那段堆地址,那么这句代码要做的必然(并且也只能)是去调用partial apply forwarder for plus这个函数,但是跟直接通过函数名调用一个函数不同(通过函数名方式,是直接对函数地址调用,例如 call 0x10000476b),但这里的fn是一个全局变量,是一个变量哦,可以把它理解成一个盒子,里面可以装不同的东西,你要使用里面的内容,就必须打开盒子,所以这是一种间接调用,在汇编里面,间接函数调用是用 callq *[内存地址]这种格式来表示,例如callq *rax,表示根据寄存器rax里面存储的指针,找到指定内存,从里面读取8个字节的内容,作为目标函数地址,然后进行调用。

我们继续运行程序,来带断点3,根据间接调用的语法特征,我们可以看到partial apply forwarder for plus函数的调用在如下位置

image

我们运行程序至此,并且进入该函数
image

partial apply forwarder for plus函数的汇编代码可以清晰地看出,最后那句就是跳转到真正的plus函数,也就是说,plus函数的地址实际上是被包裹在了partial apply forwarder for plus函数函数内部,并且直接进行跳转的。
至此,我们就弄清楚了,最简单的闭包产生的条件,以及闭包的内存结构,通过下图总结一下
image

以上,我们弄清楚了一个闭包产生的条件,以及闭包内部的具体内容,那么不同闭包之间,内部的内容是否有重叠或者是共用的部分呢?我们把代码调整如下

typealias Fn = (Int) -> Int

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

var fn = getFn()
fn(1)  //断点1

var fn2 = getFn()
fn2(1) //断点2

运行代码,先来到断点1处,汇编如下

image

我们可以计算得到fn内存地址等于 0x100000eeb + 0x6325 = 0x100007210,我们便可以在控制台输入打印出fn内存的内容

(lldb) x/2xg 0x100007210
0x100007210: 0x0000000100001350 0x0000000101170a30

继续运行程序到断点2,并通过同样的方法计算得到fn2的内存地址为0x100007220,并且打印出它所存储的内容

(lldb) x/2xg 0x100007220
0x100007220: 0x0000000100001350 0x00000001006598f0

通过对比,我们看得出,fnfn2的前八个字节存放的内容相同,也就是partial apply forwarder for plus这个函数的地址,而他们后8个字节的内容不一样,说明他们各自动态申请了自己的堆空间,用来存放所捕获的外部变量,其实你应该能感觉到,这跟类的实例对象很相似,共享方法,各自管理自己的成员变量。也就是下图所示

image

- num是怎么被找到并且使用的?

再次回顾一下我们的案例代码

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }    
    return plus
}
var fn = getFn()
fn(1)  //断点1

运行程序至断点1,汇编如下

 SwiftTest`main:
    0x1000019c0 <+0>:   pushq  %rbp
    0x1000019c1 <+1>:   movq   %rsp, %rbp
    0x1000019c4 <+4>:   pushq  %r13
    0x1000019c6 <+6>:   subq   $0x98, %rsp
    0x1000019cd <+13>:  movl   %edi, -0x3c(%rbp)
    0x1000019d0 <+16>:  movq   %rsi, -0x48(%rbp)
    0x1000019d4 <+20>:  callq  0x100001de0               ; SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:393
    0x1000019d9 <+25>:  leaq   0x69a0(%rip), %rcx        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x1000019e0 <+32>:  xorl   %edi, %edi
    0x1000019e2 <+34>:  movl   %edi, %esi
    0x1000019e4 <+36>:  movq   %rax, 0x6995(%rip)        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x1000019eb <+43>:  movq   %rdx, 0x6996(%rip)        ; SwiftTest.fn : (Swift.Int) -> Swift.Int + 8
->  0x1000019f2 <+50>:  movq   %rcx, %rdi
    0x1000019f5 <+53>:  leaq   -0x20(%rbp), %rax
    0x1000019f9 <+57>:  movq   %rsi, -0x50(%rbp)
    0x1000019fd <+61>:  movq   %rax, %rsi
    0x100001a00 <+64>:  movl   $0x20, %edx
    0x100001a05 <+69>:  movq   -0x50(%rbp), %rcx
    0x100001a09 <+73>:  callq  0x100006322               ; symbol stub for: swift_beginAccess
    0x100001a0e <+78>:  movq   0x696b(%rip), %rax        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x100001a15 <+85>:  movq   0x696c(%rip), %rcx        ; SwiftTest.fn : (Swift.Int) -> Swift.Int + 8
    0x100001a1c <+92>:  movq   %rcx, %rdi
    0x100001a1f <+95>:  movq   %rax, -0x58(%rbp)
    0x100001a23 <+99>:  movq   %rcx, -0x60(%rbp)
    0x100001a27 <+103>: callq  0x10000638e               ; symbol stub for: swift_retain
    0x100001a2c <+108>: leaq   -0x20(%rbp), %rdi
    0x100001a30 <+112>: movq   %rax, -0x68(%rbp)
    0x100001a34 <+116>: callq  0x10000634c               ; symbol stub for: swift_endAccess
    0x100001a39 <+121>: movl   $0x1, %edi
    0x100001a3e <+126>: movq   -0x60(%rbp), %r13
    0x100001a42 <+130>: movq   -0x58(%rbp), %rax
    0x100001a46 <+134>: callq  *%rax
    0x100001a48 <+136>: movq   -0x60(%rbp), %rdi
    0x100001a4c <+140>: movq   %rax, -0x70(%rbp)
    0x100001a50 <+144>: callq  0x100006388               ; symbol stub for: swift_release
    0x100001a55 <+149>: callq  0x100001de0               ; SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:393
    0x100001a5a <+154>: leaq   0x692f(%rip), %rcx        ; SwiftTest.fn2 : (Swift.Int) -> Swift.Int
    0x100001a61 <+161>: xorl   %r8d, %r8d
    0x100001a64 <+164>: movl   %r8d, %esi
    0x100001a67 <+167>: movq   %rax, 0x6922(%rip)        ; SwiftTest.fn2 : (Swift.Int) -> Swift.Int
    0x100001a6e <+174>: movq   %rdx, 0x6923(%rip)        ; SwiftTest.fn2 : (Swift.Int) -> Swift.Int + 8
    0x100001a75 <+181>: movq   %rcx, %rdi
    0x100001a78 <+184>: leaq   -0x38(%rbp), %rax
    0x100001a7c <+188>: movq   %rsi, -0x78(%rbp)
    0x100001a80 <+192>: movq   %rax, %rsi
    0x100001a83 <+195>: movl   $0x20, %edx
    0x100001a88 <+200>: movq   -0x78(%rbp), %rcx
    0x100001a8c <+204>: callq  0x100006322               ; symbol stub for: swift_beginAccess
    0x100001a91 <+209>: movq   0x68f8(%rip), %rax        ; SwiftTest.fn2 : (Swift.Int) -> Swift.Int
    0x100001a98 <+216>: movq   0x68f9(%rip), %rcx        ; SwiftTest.fn2 : (Swift.Int) -> Swift.Int + 8
    0x100001a9f <+223>: movq   %rcx, %rdi
    0x100001aa2 <+226>: movq   %rax, -0x80(%rbp)
    0x100001aa6 <+230>: movq   %rcx, -0x88(%rbp)
    0x100001aad <+237>: callq  0x10000638e               ; symbol stub for: swift_retain
    0x100001ab2 <+242>: leaq   -0x38(%rbp), %rdi
    0x100001ab6 <+246>: movq   %rax, -0x90(%rbp)
    0x100001abd <+253>: callq  0x10000634c               ; symbol stub for: swift_endAccess
    0x100001ac2 <+258>: movl   $0x1, %edi
    0x100001ac7 <+263>: movq   -0x88(%rbp), %r13
    0x100001ace <+270>: movq   -0x80(%rbp), %rax
    0x100001ad2 <+274>: callq  *%rax
    0x100001ad4 <+276>: movq   -0x88(%rbp), %rdi
    0x100001adb <+283>: movq   %rax, -0x98(%rbp)
    0x100001ae2 <+290>: callq  0x100006388               ; symbol stub for: swift_release
    0x100001ae7 <+295>: xorl   %eax, %eax
    0x100001ae9 <+297>: addq   $0x98, %rsp
    0x100001af0 <+304>: popq   %r13
    0x100001af2 <+306>: popq   %rbp
    0x100001af3 <+307>: retq   

根据我们之前对fn(1)调用特点的分析,首先会通过0x100001ad2 <+274>: callq *%rax这句汇编调用partial apply forwarder for plus函数,所以除了实参参数1plus到时会用到的num肯定会也是从这里传进去的,我们来看一下callq *%rax之前的参数情况

image

image

到目前为止

  • edi = 1
  • r13 = num地址 - 0x10 (根据上图,可以看出num所在的位置,是r13所存放的地址开始跳过16个字节之后的那8个字节,这点药牢记)

我们继续往下走,进入partial apply forwarder for plus函数内部

SwiftTest`partial apply for plus #1 (_:) in getFn():
->  0x100001f70 <+0>: pushq  %rbp
    0x100001f71 <+1>: movq   %rsp, %rbp
    0x100001f74 <+4>: movq   %r13, %rsi
    0x100001f77 <+7>: popq   %rbp
    0x100001f78 <+8>: jmp    0x100001e70               ; plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.

这里很简单,就是把寄存器r13的值赋值给寄存器rsi,然后在跳入地址为0x100001e70的函数,后面的注释告诉我们,这个函数就是plus函数,那么更新一下当前即将被plus函数使用的参数的情况

  • edi = 1
  • rsi = num地址 - 0x10
    我们跟随0x100001f78 <+8>: jmp 0x100001e70指令来到plus函数,其完整汇编如下
SwiftTest`plus #1 (_:) in getFn():
->  0x100001e70 <+0>:   pushq  %rbp
    0x100001e71 <+1>:   movq   %rsp, %rbp
    0x100001e74 <+4>:   subq   $0x90, %rsp
    0x100001e7b <+11>:  xorl   %eax, %eax
    0x100001e7d <+13>:  movl   %eax, %ecx
    0x100001e7f <+15>:  xorl   %eax, %eax
    0x100001e81 <+17>:  leaq   -0x8(%rbp), %rdx
    0x100001e85 <+21>:  movq   %rdi, -0x48(%rbp)
    0x100001e89 <+25>:  movq   %rdx, %rdi
    0x100001e8c <+28>:  movq   %rsi, -0x50(%rbp)
    0x100001e90 <+32>:  movl   %eax, %esi
    0x100001e92 <+34>:  movl   $0x8, %edx
    0x100001e97 <+39>:  movq   %rdx, -0x58(%rbp)
    0x100001e9b <+43>:  movq   %rcx, -0x60(%rbp)
    0x100001e9f <+47>:  movl   %eax, -0x64(%rbp)
    0x100001ea2 <+50>:  callq  0x100006310               ; symbol stub for: memset
    0x100001ea7 <+55>:  leaq   -0x10(%rbp), %rcx
    0x100001eab <+59>:  movq   %rcx, %rdi
    0x100001eae <+62>:  movl   -0x64(%rbp), %esi
    0x100001eb1 <+65>:  movq   -0x58(%rbp), %rdx
    0x100001eb5 <+69>:  callq  0x100006310               ; symbol stub for: memset
    0x100001eba <+74>:  movq   -0x48(%rbp), %rcx
    0x100001ebe <+78>:  movq   %rcx, -0x8(%rbp)
    0x100001ec2 <+82>:  movq   -0x50(%rbp), %rdx
    0x100001ec6 <+86>:  addq   $0x10, %rdx
    0x100001ecd <+93>:  movq   %rdx, -0x10(%rbp)
    0x100001ed1 <+97>:  movq   %rdx, %rdi
    0x100001ed4 <+100>: leaq   -0x28(%rbp), %rsi
    0x100001ed8 <+104>: movl   $0x21, %r8d
    0x100001ede <+110>: movq   %rdx, -0x70(%rbp)
    0x100001ee2 <+114>: movq   %r8, %rdx
    0x100001ee5 <+117>: movq   -0x60(%rbp), %rcx
    0x100001ee9 <+121>: callq  0x100006322               ; symbol stub for: swift_beginAccess
    0x100001eee <+126>: movq   -0x48(%rbp), %rcx
    0x100001ef2 <+130>: movq   -0x50(%rbp), %rdx
    0x100001ef6 <+134>: addq   0x10(%rdx), %rcx
    0x100001efa <+138>: seto   %r9b
    0x100001efe <+142>: testb  $0x1, %r9b
    0x100001f02 <+146>: movq   %rcx, -0x78(%rbp)
    0x100001f06 <+150>: jne    0x100001f60               ; <+240> at main.swift:398:13
    0x100001f08 <+152>: movq   -0x70(%rbp), %rax
    0x100001f0c <+156>: movq   -0x78(%rbp), %rcx
    0x100001f10 <+160>: movq   %rcx, (%rax)
    0x100001f13 <+163>: leaq   -0x28(%rbp), %rdi
    0x100001f17 <+167>: callq  0x10000634c               ; symbol stub for: swift_endAccess
    0x100001f1c <+172>: xorl   %edx, %edx
    0x100001f1e <+174>: movl   %edx, %ecx
    0x100001f20 <+176>: leaq   -0x40(%rbp), %rax
    0x100001f24 <+180>: movl   $0x20, %edx
    0x100001f29 <+185>: movq   -0x70(%rbp), %rdi
    0x100001f2d <+189>: movq   %rax, %rsi
    0x100001f30 <+192>: movq   %rax, -0x80(%rbp)
    0x100001f34 <+196>: callq  0x100006322               ; symbol stub for: swift_beginAccess
    0x100001f39 <+201>: movq   -0x70(%rbp), %rax
    0x100001f3d <+205>: movq   (%rax), %rax
    0x100001f40 <+208>: movq   -0x80(%rbp), %rdi
    0x100001f44 <+212>: movq   %rax, -0x88(%rbp)
    0x100001f4b <+219>: callq  0x10000634c               ; symbol stub for: swift_endAccess
    0x100001f50 <+224>: movq   -0x88(%rbp), %rax
    0x100001f57 <+231>: addq   $0x90, %rsp
    0x100001f5e <+238>: popq   %rbp
    0x100001f5f <+239>: retq   
    0x100001f60 <+240>: ud2    

乍一看不太好分析,我们从关键点入手,我们知道,plus函数内部的操作如下

  • num += i
    1. 101相加得到11
    2. 11这个数存回到num的内存里
  • return num :返回此时num内存里存放的那个值,也就是11

有相加的操作,就有addq指令,我们能在plus函数里找到如下几句addq指令

0x100001ec6 <+86>:  addq   $0x10, %rdx
.
.
0x100001ef6 <+134>: addq   0x10(%rdx), %rcx
.
.
0x100001f57 <+231>: addq   $0x90, %rsp

因为numi都不是立即数,所以第一句第三句就可以排除掉,因为他们的操作数里面包含了立即数,那么结果就落到了第二句上面,我们在这一句加上断点,并运行至此处,首先看一下寄存器rcx的内容

(lldb) register read rcx
     rcx = 0x0000000000000001

说明寄存器rcx里面存放的就是i的值1,在看一下rdx的值

(lldb) register read rdx
     rdx = 0x0000000100604400

是一个堆空间地址,那么应该就是num相关的那个堆空间,这段堆空间里面的内容如下

(lldb) x/4gx 0x0000000100604400
0x100604400: 0x0000000100007150 0x0000000200000002
0x100604410: 0x000000000000000a 0x00000001003292a8

那么可以看到0x10(%rdx)所寻址到的那段内存地址的值(也就是第三段那8个字节)是0x0a,也就是10,说明他代表的就是num ,我们还可以逆向追逐一下rcxrdx是从哪里取到的值

image

我们看到rcx的值来自于edi这个寄存器,我们好像没有在前面的参数传递流程里看到edi,其实你看下图就明白了

image

处于系统兼容,rdi寄存器里面的低32位是edi寄存器,低16位是di寄存器,前面我们知道rdi存放的是i的值1,也就是 0x0000000000000001,因此它的低32位读出来就是0x00000001,也就是说edi此时表达的是i的值1

接着我们看一下rdx

image

所以rdx = rsi,而进入plus之前,我们已经确定rsi = num地址 - 0x10 ,因此0x10(%rdx) 代表寻址到 rdx + 0x10 = num地址 - 0x10 + 0x10 = num地址,所以0x10(%rdx) = num = 10,这样 0x100001ef6 <+134>: addq 0x10(%rdx), %rcx就完成了 num += i的操作。也就是说此时num的内存里面存放的是11

最后我们来看一下返回的值到底是不是num内存里的值,我们直接从里ret指令最近的那个rax开始看

image

这样,关于num的捕获,存放,以及如何被plus函数使用的所有细节就基本上看清楚了

- 请问闭包对于外界变量的捕获,到底发生在什么时候?

我们继续把代码变一下

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    
    num = 14
    
    return plus  //断点1
}

var fn = getFn()
fn(1)

你认为plus捕获的10还是14呢?运行之后结果显示,实际捕获的是14。我们将程序运行到断点1处,查看汇编情况如下

image

根据汇编情况分析,我们发现,编译器实际上是将return之前,将num的所有赋值都捕获一次,所以最终生成的闭包捕获到的有效值是numreturn之前的最后一次赋值,蛮有趣!接着再变一下

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    
    num = 14
    
    return {$0} //断点1
}

var fn = getFn()

我们将getFn的返回值替换成{$0}这个闭包表达式,也就是说返回的东西跟plus无关,来看看汇编情况如何

SwiftTest`getFn():
    0x100001390 <+0>:  pushq  %rbp
    0x100001391 <+1>:  movq   %rsp, %rbp
    0x100001394 <+4>:  movq   $0x0, -0x8(%rbp)
    0x10000139c <+12>: movq   $0xa, -0x8(%rbp)
    0x1000013a4 <+20>: movq   $0xe, -0x8(%rbp)
->  0x1000013ac <+28>: leaq   0xd(%rip), %rax           ; closure #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:397
    0x1000013b3 <+35>: xorl   %ecx, %ecx
    0x1000013b5 <+37>: movl   %ecx, %edx
    0x1000013b7 <+39>: popq   %rbp
    0x1000013b8 <+40>: retq 

好家伙,编译器直接不玩闭包这一套了,什么动态申请堆空间也没有了,也就是说,编译器认为既然返回的东西跟plus没有任何关系了,那就没有必要再去费劲把力的分配各种堆空间来给你准备一个闭包了,直接伸略,呵呵,好暴力,我喜欢~~

多变量捕获的情况

对于单一的变量捕获,我们在上文已经分析完毕,那么如果捕获了多个变量呢?首先调整一下代码

typealias Fn = (Int) -> (Int, Int)
func getFns() -> (Fn, Fn) {
    var num1 = 0
    var num2 = 0
    func plus(_ i: Int) -> (Int, Int) {
        num1 += i
        num2 += i << 1
        return (num1, num2)
    }
    func minus(_ i: Int) -> (Int, Int) {
        num1 -= i
        num2 -= i << 1
        return (num1, num2)
    }
    
    return (plus, minus) // 断点2
}

let (p, m) = getFns() //断点1

p(5)
m(4)
p(3)
m(2)

案例中,我们的闭包包含两个函数 plusminus, 捕获了两个变量num1num2,按照我们已掌握的堆闭包的认识,先来推断一下

每次调用getFns函数, num1num2会被捕获到堆空间中一次,并且只有一份,共本次getFns所返回的plus和minus函数使用,那么我们可以推测最后的运算结果

  • p(5) --> num1 += i 等价于 num1 = 0 + 5 = 5num2 += i << 1 等价于 num2 = 0 + 5 * 2 = 10
  • m(4) --> num1 -= i 等价于 num1 = 5 - 4 = 1num2 += i << 1 等价于 num2 = 10 - 4 * 2 = 2
  • p(3) --> num1 += i 等价于 num1 = 1 + 3 = 4num2 += i << 1 等价于 num2 = 2 + 3 * 2 = 8
  • m(2) --> num1 -= i 等价于 num1 = 4 - 2 = 2num2 += i << 1 等价于 num2 = 8 - 2 * 2 = 4

运行程序并且打印一下,可以得到结果是符合我们上述的推断的。现在我们通过汇编来看一下,num1num2被捕获之后,是如何在堆空间被管理的,我们将程序重新运行至断点1处并查看汇编

SwiftTest`main:
    0x100001600 <+0>:   pushq  %rbp
    0x100001601 <+1>:   movq   %rsp, %rbp
    0x100001604 <+4>:   pushq  %r13
    0x100001606 <+6>:   subq   $0xb8, %rsp
    0x10000160d <+13>:  movl   %edi, -0xc(%rbp)
    0x100001610 <+16>:  movq   %rsi, -0x18(%rbp)
->  0x100001614 <+20>:  callq  0x100001770               ; SwiftTest.getFns() -> ((Swift.Int) -> (Swift.Int, Swift.Int), (Swift.Int) -> (Swift.Int, Swift.Int)) at main.swift:437
    0x100001619 <+25>:  movq   %rax, 0x7be8(%rip)        ; SwiftTest.p : (Swift.Int) -> (Swift.Int, Swift.Int)
    0x100001620 <+32>:  movq   %rdx, 0x7be9(%rip)        ; SwiftTest.p : (Swift.Int) -> (Swift.Int, Swift.Int) + 8
    0x100001627 <+39>:  movq   %rcx, 0x7bea(%rip)        ; SwiftTest.m : (Swift.Int) -> (Swift.Int, Swift.Int)
    0x10000162e <+46>:  movq   %r8, 0x7beb(%rip)         ; SwiftTest.m : (Swift.Int) -> (Swift.Int, Swift.Int) + 8
    ...
    ...
    ...
    ...

我们可以发现,getFns返回的内容分别存放在rax、rdx、rcx、r8这四个寄存器中,并且根据注释信息,可以看出来,他们将分别给pm这两个闭包赋值。我们已经知道,闭包的前8个字节,存放的是一个跟闭包函数相关联的一个函数地址,后8个字节的内容是一段管理闭包捕获变量的堆空间地址,也就是rdxr8这两个寄存器,我们现将0x100001614 <+20>: callq 0x100001770指令走完,然后查看一下此时的rdxr8,根据上面的推断,这两个寄存器放的应该是同一段堆空间地址,里面管理了被捕获的num1num2,但是它们实际内容却是

(lldb) register read rdx
     rdx = 0x0000000100606530
(lldb) register read r8
      r8 = 0x0000000100606320

居然不是同一段堆空间,在看一下这两个堆空间里面的内容

(lldb) x/4xg 0x0000000100606530
0x100606530: 0x0000000100008178 0x0000000000000002
0x100606540: 0x0000000100604cb0 0x0000000100606510
(lldb) x/4xg 0x0000000100606530
0x100606530: 0x0000000100008178 0x0000000000000002
0x100606540: 0x0000000100604cb0 0x0000000100606510

居然是相同的,此时感觉有点凌乱了,没关系,如果你累了建议吃点零食,休息十分钟,然后我们继续。看来随着多变量的捕获,以及多闭包的产生,编译器的处理情况不是我们想的那么简单了,因此我们还是要尽到getFns里面去一探究竟,我们从新运行程序,过掉断点1,来到断点2处,此时getFns函数的汇编如下

SwiftTest`getFns():
->  0x100001770 <+0>:   pushq  %rbp
    0x100001771 <+1>:   movq   %rsp, %rbp
    0x100001774 <+4>:   subq   $0x70, %rsp
    0x100001778 <+8>:   leaq   0x69d1(%rip), %rax
    0x10000177f <+15>:  movl   $0x18, %ecx
    0x100001784 <+20>:  movl   $0x7, %edx
    0x100001789 <+25>:  movq   %rax, %rdi
    0x10000178c <+28>:  movq   %rcx, %rsi
    0x10000178f <+31>:  movq   %rdx, -0x8(%rbp)
    0x100001793 <+35>:  movq   %rax, -0x10(%rbp)
    0x100001797 <+39>:  movq   %rcx, -0x18(%rbp)
    0x10000179b <+43>:  callq  0x1000073e6               ; symbol stub for: swift_allocObject
    0x1000017a0 <+48>:  movq   %rax, %rcx
    0x1000017a3 <+51>:  addq   $0x10, %rcx
    0x1000017a7 <+55>:  movq   %rcx, %rdx
    0x1000017aa <+58>:  movq   $0x0, 0x10(%rax)
    0x1000017b2 <+66>:  movq   -0x10(%rbp), %rdi
    0x1000017b6 <+70>:  movq   -0x18(%rbp), %rsi
    0x1000017ba <+74>:  movq   -0x8(%rbp), %rdx
    0x1000017be <+78>:  movq   %rax, -0x20(%rbp)
    0x1000017c2 <+82>:  movq   %rcx, -0x28(%rbp)
    0x1000017c6 <+86>:  callq  0x1000073e6               ; symbol stub for: swift_allocObject
    0x1000017cb <+91>:  movq   %rax, %rcx
    0x1000017ce <+94>:  addq   $0x10, %rcx
    0x1000017d2 <+98>:  movq   %rcx, %rdx
    0x1000017d5 <+101>: movq   $0x0, 0x10(%rax)
    0x1000017dd <+109>: movq   -0x20(%rbp), %rdi
    0x1000017e1 <+113>: movq   %rax, -0x30(%rbp)
    0x1000017e5 <+117>: movq   %rcx, -0x38(%rbp)
    0x1000017e9 <+121>: callq  0x100007452               ; symbol stub for: swift_retain
    0x1000017ee <+126>: movq   -0x30(%rbp), %rdi
    0x1000017f2 <+130>: movq   %rax, -0x40(%rbp)
    0x1000017f6 <+134>: callq  0x100007452               ; symbol stub for: swift_retain
    0x1000017fb <+139>: leaq   0x6976(%rip), %rdi
    0x100001802 <+146>: movl   $0x20, %ecx
    0x100001807 <+151>: movq   %rcx, %rsi
    0x10000180a <+154>: movq   -0x8(%rbp), %rdx
    0x10000180e <+158>: movq   %rax, -0x48(%rbp)
    0x100001812 <+162>: movq   %rcx, -0x50(%rbp)
    0x100001816 <+166>: callq  0x1000073e6               ; symbol stub for: swift_allocObject
    0x10000181b <+171>: movq   -0x20(%rbp), %rcx
    0x10000181f <+175>: movq   %rcx, 0x10(%rax)
    0x100001823 <+179>: movq   -0x30(%rbp), %rdx
    0x100001827 <+183>: movq   %rdx, 0x18(%rax)
    0x10000182b <+187>: movq   %rcx, %rdi
    0x10000182e <+190>: movq   %rax, -0x58(%rbp)
    0x100001832 <+194>: callq  0x100007452               ; symbol stub for: swift_retain
    0x100001837 <+199>: movq   -0x30(%rbp), %rdi
    0x10000183b <+203>: movq   %rax, -0x60(%rbp)
    0x10000183f <+207>: callq  0x100007452               ; symbol stub for: swift_retain
    0x100001844 <+212>: leaq   0x6955(%rip), %rdi
    0x10000184b <+219>: movq   -0x50(%rbp), %rsi
    0x10000184f <+223>: movq   -0x8(%rbp), %rdx
    0x100001853 <+227>: movq   %rax, -0x68(%rbp)
    0x100001857 <+231>: callq  0x1000073e6               ; symbol stub for: swift_allocObject
    0x10000185c <+236>: movq   -0x20(%rbp), %rcx
    0x100001860 <+240>: movq   %rcx, 0x10(%rax)
    0x100001864 <+244>: movq   -0x30(%rbp), %rdx
    0x100001868 <+248>: movq   %rdx, 0x18(%rax)
    0x10000186c <+252>: movq   %rdx, %rdi
    0x10000186f <+255>: movq   %rax, -0x70(%rbp)
    0x100001873 <+259>: callq  0x10000744c               ; symbol stub for: swift_release
    0x100001878 <+264>: movq   -0x20(%rbp), %rdi
    0x10000187c <+268>: callq  0x10000744c               ; symbol stub for: swift_release
    0x100001881 <+273>: movq   -0x28(%rbp), %rax
    0x100001885 <+277>: movq   -0x38(%rbp), %rcx
    0x100001889 <+281>: leaq   0xc00(%rip), %rax         ; partial apply forwarder for plus #1 (Swift.Int) -> (Swift.Int, Swift.Int) in SwiftTest.getFns() -> ((Swift.Int) -> (Swift.Int, Swift.Int), (Swift.Int) -> (Swift.Int, Swift.Int)) at <compiler-generated>
    0x100001890 <+288>: leaq   0x17e9(%rip), %rcx        ; partial apply forwarder for minus #1 (Swift.Int) -> (Swift.Int, Swift.Int) in SwiftTest.getFns() -> ((Swift.Int) -> (Swift.Int, Swift.Int), (Swift.Int) -> (Swift.Int, Swift.Int)) at <compiler-generated>
    0x100001897 <+295>: movq   -0x58(%rbp), %rdx
    0x10000189b <+299>: movq   -0x70(%rbp), %r8
    0x10000189f <+303>: addq   $0x70, %rsp
    0x1000018a3 <+307>: popq   %rbp
    0x1000018a4 <+308>: retq 

首先我们知道,getFns函数的返回值放在raxrcxrdxr8这四个寄存器里面,并且返回之后,会被复制到pm这两个闭包的内存里面

image

image

看汇编码的最后几句我们可以发现raxrcx最后存放的是partial apply forwarder for pluspartial apply forwarder for minus这两个函数的地址,然后被返回之后,它们会分别被存入p和m的前8个字节,所以raxrcx这两个返回值的内容搞清楚了

我们根据现在的经验,知道了堆空间的动态申请,可以看有没有走swift_allocObject这个函数,并且在之前分析闭包对单变量捕捉的情况下,每次生成一个新的闭包,也就是没调用一次getFn函数,都会调用一次swift_allocObject函数来存放被捕捉的变量的值,在getFns函数的汇编代码,我们却发现,当闭包捕捉两个变量的时候,swift_allocObject函数被调用了4次,因此这4段堆空间应该都跟 rdxr8这两个寄存器里面的值有关,我们还是从函数返回的地方进行逆向追溯,下图分析了rdx的赋值过程

image

上图中有个细节,就是申请堆内存的大小问题,对于捕获变量num1或者num2所申请的内存大小(也就是步骤1和步骤2),和以前我们分析单变量捕获情况一样,也是向系统申请0x18(24)字节,系统实际分配了32字节(16倍数),而这里,我们知道对于闭包mp来说,编译器还为它们分别申请了一段堆内存,用于管理刚才为num1num2所申请的那两块内存,这个时候编译器申请的大小是0x20(32字节),为什么呢?

分析到这里,我们看出动态申请堆内存的特点,

  • 首先前8字节释放类型描述信息,
  • 接下来用8个字节存放引用计数,用于内存管理,
  • 然后就是存放内容的空间,并且实际获得大小肯定是16的倍数。

对于步骤3所申请的内存,因为接下来将要用它来存放步骤1、步骤2所申请的内存地址,因此他就需要至少32字节(16 + 8 * 2),如果我们需要捕获3个变量,那么便需要申请 16 + 8 * 3 = 40个字节的堆内存,如果需要捕获n个变量,那么就需要申请 16 + 8 * n 个字节大小的堆内存。

对于r8,整个赋值过程和rdx是相同的,所以函数getFns的所返回的闭包pm的内容布局总结如下图

image

自动闭包

看下面一个函数

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

这个函数比较简单,但是在有些场景下,会有些浪费:

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

func getNumber() -> Int {
    print("调用了getNumber()~~~~")
    let a = 10
    let b = 11
    return a + b
}

getFirstPositive(10, getNumber()) 
getFirstPositive(-2, getNumber()) 
getFirstPositive(0, getNumber()) 

************************控制台结果
调用了getNumber()~~~~
调用了getNumber()~~~~
调用了getNumber()~~~~
Program ended with exit code: 0

从结果看,我们每次调用getFirstPositive的时候,都触发了getNumber函数,但是getFirstPositive(10, getNumber()) 这一次调用,我们判断第一个参数大于0之后,直接返回第一个参数就可以了,所以此时getNumber()的调用是多余的,因为函数的每次调用都是要占用系统资源开销的,所以这样的现状显然不“经济环保

又一个办法可以将上面的问题解决,请看代码

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

func getNumber() -> Int {
    print("调用了getNumber()~~~~")
    let a = 10
    let b = 11
    return a + b
}

getFirstPositive(10, getNumber) 
getFirstPositive(-2, getNumber)
getFirstPositive(0, getNumber)
************************控制台结果
调用了getNumber()~~~~
调用了getNumber()~~~~
Program ended with exit code: 0

上面,我们将getFirstPositive的第二个参数v2类型改成了函数类型 () -> Int,因此我们传参的时候,传入的是函数getNumber本身,此时它并没有没有被触发调用,只有当v1<=0的时候,才会进行调用v2(),从运行结果可以看出,却是节省了一次函数调用。

其实这种模式有不少的应用场景,比如只有当满足某种情况的时候,才需要进一步的执行一些复杂的操作,例如文件读取、网络请求等

在上面的代码里面,getNumber函数里面就可以理解成所谓的复杂操作,在实际应用中,文件读取、网络请求等复杂操作就可以用一个函数来进行封装,但是如果我们需要的操作非常简单,那么就可能导致代码可读性降低,比如下面的代码中,我们直接用一个闭包表达式来代替getNumber函数

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

这种代码阅读起来,就会相当费劲。针对这种情况,Swift给我们提供一种语法糖----自动闭包

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

我们在参数v2的类型前面加上@autoclosure这个编译器指令,这样我们在传参数v2的时候,就可以非常精简getFirstPositive(10, 20),符合我们正常的阅读习惯,编译器根据@autoclosure,会将参数20自动编译成闭包表达式{20},这样它本质上还是和刚才getNumber一样,是一个函数,在不需要的情况下,不会进行调用,所以整个代码既保证了可读性,又保证经济环保性。香不香?

自动闭包注意点

  • @autoclosure会自动将20封装成{20}
  • @autoclosure只支持 () -> T格式的参数
  • @autoclosure并非要求必须是最后一个参数
  • @autoclosure可以构成函数重载
  • 空合并运算符 ?? 就是使用@autoclosure技术实现的

空合并运算符 ??@autoclosure

我们之前已经学习过空合并运算符的使用方法,Swift对于它的实现如下

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?

throws 和 rethrows这两个关键字先不考虑,简化如下

public func ?? <T>(optional: T?, defaultValue: @autoclosure ()  -> T?) -> T?

然后将范型T用Int取代,再次简化如下

public func ?? (optional: Int?, defaultValue: @autoclosure ()  -> Int?) -> Int?

再将optional类型Int?替换成基本类型Int,并且将函数名??换成一个正常的一个函数名funXxx

func funXxx (optional: Int, defaultValue: @autoclosure ()  -> Int) -> Int

---->对比刚才的getFirstPositive<----

func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int

相信看到这里就应该明白 ??的本质了吧,点到为止,自己体会。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容