美文网首页
从零学习Swift 06:汇编分析闭包本质

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

作者: 小心韩国人 | 来源:发表于2020-04-26 13:46 被阅读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即可:

    自动闭包

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/vdyiihtx.html