从零学习Swift 06:汇编分析闭包本质

2020-04-26  本文已影响0人  小心韩国人
总结

上一篇我们已经了解了闭包表达式和闭包.今天我们就通过汇编分析一下闭包的本质.

我们通过普通的函数类型的变量和闭包对比,看看他们的变量内存地址中的数据有何不同:

先用汇编窥探一下普通函数类型变量fn中的内存:

普通函数类型变量内存数据

rax中存储的sum函数地址:

sum函数地址

结合上面两张图,总结一下:普通函数类型的变量,占用16个字节,它的前8个字节直接存放的就是函数地址,后8个字节是0

接下来我们再用汇编窥探一下闭包变量的内存:

首先调用testClosure()向堆空间申请内存:

向堆空间申请内存

然后会将一个函数地址和堆空间的内存分别放入closure1的前8个字节和后8个字节:

第三步就是传参调用函数:

进入rax 中存储的 0x0000000100000f00 函数地址:

有上图可以看到,在rax存放的函数内部,会直接调用sum函数,进入sum函数:

sum 函数的内部运算

现在我们就通过汇编语言分析了闭包的底层是如何捕获变量,以及如何访问堆空间地址的.总结如下:

调用testClosure函数会向堆空间申请一段内存用来存放捕获的变量/常量,testClosure函数的返回值占用16个字节,其中前8个字节存放的是一个函数地址,这个函数内部会直接调用sum函数;后8个字节存放的是向堆空间申请的内存地址;当我们通过闭包调用函数时,会传入两个参数.第一个参数就是调用方法传入的常规参数;第二个参数就是堆空间的内存地址.闭包就是通过传入的堆空间的内存地址访问变量的.

到目前为止我们搞清楚了闭包的本质以及闭包是如何捕获变量,如何访问堆空间内存的.现在我们思考一下,闭包捕获外层函数的变量时,是什么时候开始捕获的?

看看一下代码,num的值是什么时候被捕获的?


func testClosure() -> (Int) ->  Int{
    
    var num = 0
    
    func sum(_ a: Int) -> Int{
        num += a
        return num
    }
    return sum
}

var closure1 = testClosure()
closure1(1)
closure1(2)

我们在函数返回之前,修改一下num的值,看看结果:

最后的结果是11 , 13.说明最后捕获的是num = 10的值.也就是说在函数返回之前会捕获num的值.为什么会这样呢?我们先看一下它的汇编:

闭包会向堆空间申请内存

看闭包的汇编语言会发现,闭包会向堆空间申请内存,并且捕获num的值.

现在我们把testClosure()的返回值修改如下:


func testClosure() -> (Int) ->  Int{
    
    var num = 0
    
    func sum(_ a: Int) -> Int{
        num += a
        return num
    }
    
    num = 10
    return {
        (v1: Int) -> Int in
        return v1
    }
}

testClosure ()函数不返回sum函数,在来看看它的汇编:

没有向堆空间申请内存捕获变量

通过上面的对比可以发现,只有当返回的函数访问了外层函数的变量时,才会捕获变量.所以捕获变量的根本就是看函数的返回值.所以,现在可以下结论:闭包什么时候捕获变量? 当函数返回之前捕获.

看看下面代码运行结果是什么:


typealias Fn = (Int) -> (Int, Int)

func getFn() -> (Fn, Fn){
    var num1: Int = 0
    var num2: Int = 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)
}

var (plus, minus) = getFn()

print(plus(2))//2,4
print(minus(4))//-2, -4
print(plus(6))//4, 8
print(minus(3))//1,2

从结果上可以看到,plus(), minus()都共用一个num1, num2.我们看看它的汇编:

从上面两幅图可以看到,调用getFn()会向堆空间申请两块内存分别存放num1,num2.并且把num1,num2的地址一起放到另一个内存中,然后再存放到闭包对象中.之前闭包只有一个返回值的时候,闭包对象的内存中直接存放的就是用来捕获的堆空间地址.

自动闭包
//自动闭包

func getNum() -> Int{
    var num = 10
    num -= 1
    
    print("getNum")
    return num
}

//获取第一个大于0的书
func getPositiveNum(_ num1: Int, _ num2: Int) -> Int{
    return num1 > 0 ? num1 : num2
}

print(getPositiveNum(10, getNum()))


//打印结果

getNum
10

像上面的代买,获取第一个大于0的数,如果第一个参数符合条件,那么第二个参数的函数其实就无需执行了,但是按照上面的写法,第二个参数的函数每次都会执行.

那怎么才能让第二个参数在有需要的时候才去执行呢?只需要把第二个参数定义成函数类型的参数即可:

当然也可使用闭包表达式的写法来实现,别忘了闭包表达式也是定义函数的一种方式:

但是像这种闭包表达式,每次都要我们手动写大括号{},过于繁琐.编译器考虑到程序员的烦恼,就有了自动闭包这种语法糖:

使用自动闭包实现只需要在参数标签后面加上autoclosure即可:

自动闭包
上一篇 下一篇

猜你喜欢

热点阅读