美文网首页swiftSwift学习
Swift-进阶 09:闭包(一)使用&捕获原理

Swift-进阶 09:闭包(一)使用&捕获原理

作者: Style_月月 | 来源:发表于2021-01-20 18:03 被阅读0次

    Swift 进阶之路 文章汇总

    本文主要分析闭包以及闭包捕获变量的原理

    闭包

    闭包是一个捕获了全局上下文的常量或者变量的函数,通俗来讲,闭包可以是常量也可以是函数

    • 【全局函数是一种特殊的闭包】:定义一个全局函数,只是当前的全局函数并不捕获值
    func test(){
        print("test")
    }
    
    • 【函数闭包】:下面的函数是一个闭包,函数中的incrementer是一个内嵌函数,可以从makeIncrementer中捕获变量runningTotal
    func makeIncrementer() -> () -> Int{
        var runningTotal = 10
        //内嵌函数,也是一个闭包
        func incrementer() -> Int{
            runningTotal += 1
            return runningTotal
        }
        return incrementer
    }
    
    • 【闭包表达式 / 匿名函数】:下面是一个闭包表达式,即一个匿名函数,而且是从上下文中捕获变量和常量
    //闭包表达式
    { (param) -> ReturnType in
        //方法体
    }
    

    使用闭包的好处

    • 1、利用上下文推断参数和返回值类型

    • 2、单表达式可以隐式返回,即省略return关键字

    • 3、参数名称的简写,例如 $0表示第一个参数

    • 4、尾随闭包表达式

    闭包表达式

    OC与swift的对比

    • OC中的Block其实是一个匿名函数,需要具备以下特点:

      • 1、作用域 {}

      • 2、参数和返回值

      • 3、函数体(in)之后的代码

    • swift中的闭包,可以当做变量,也可以当做参数传递

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

    闭包表达式的使用方式

    • 【可选类型的闭包表达式】1、将闭包表达式声明成一个可选类型
    //声明一个可选类型的闭包
    <!--错误写法-->
    var clourse: (Int) -> Int?
    clourse = nil
    
    <!--正确写法-->
    var clourse: ((Int) -> Int)?
    clourse = nil
    
    • 【闭包常量】2、通过let将闭包声明成一个常量(即一旦赋值之后就不能更改
    //2、通过let将闭包声明为一个常量,即一旦赋值后就不能改变了
    let clourse: (Int) -> Int
    clourse = {(age: Int) in
        return age
    }
    //报错:Immutable value 'clourse' may only be initialized once
    clourse = {(age: Int) in
        return age
    }
    
    修改闭包常量报错
    • 【闭包参数】3、将闭包作为 函数的参数
    //3、将闭包作为函数的参数
    func test(param: () -> Int){
        print(param())
    }
    var age = 10
    test { () -> Int in
        age += 1
        return age
    }
    

    尾随闭包

    当闭包作为函数的最后一个参数,如果当前的闭包表达式很长,我们可以通过尾随闭包的书写方法来提高代码的可读性

    //闭包表达式作为函数的最后一个参数
    func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool{
        return by(a, b, c)
        
    }
    //常规写法
    test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
            return (item1 + item2 < item3)
    })
    //尾随闭包写法
    test(10, 20, 30) { (item1, item2, item3) -> Bool in
        return (item1 + item2 < item3)
    }
    
    • 我们平常使用的array.sorted其实就是一个尾随闭包,且这个函数就只有一个参数,如下所示
    //array.sorted就是一个尾随闭包
    var array = [1, 2, 3]
    //1、完整写法
    array.sorted { (item1: Int, item2: Int) -> Bool in return item1 < item2}
    //2、省略参数类型:通过array中的参数推断类型
    array.sorted { (item1, item2) -> Bool in return item1 < item2}
    //3、省略参数类型 + 返回值类型:通过return推断返回值类型
    array.sorted { (item1, item2) in return item1 < item2}
    //4、省略参数类型 + 返回值类型 + return关键字:单表达式可以隐士表达,即省略return关键字
    array.sorted { (item1, item2) in item1 < item2}
    //5、参数名称简写
    array.sorted {return $0 < $1}
    //6、参数名称简写 + 省略return关键字
    array.sorted {$0 < $1}
    //7、最简:直接传比较符号
    array.sorted (by: <)
    

    捕获一个变量

    下面代码的打印结果是什么?

    func makeIncrementer() -> () -> Int{
        var runningTotal = 10
        //内嵌函数,也是一个闭包
        func incrementer() -> Int{
            runningTotal += 1
            return runningTotal
        }
        return incrementer
    }
    let makeInc = makeIncrementer()
    print(makeInc())
    print(makeInc())
    print(makeInc())
    
    <!--打印结果-->
    11
    12
    13
    

    打印结果如下,从结果中可以看出,每次的结构都是在上次函数执行的基础上累加的,但是我们所知的runningTotal是一个临时变量,按理说每次进入函数都是10,这里为什么会每次累加呢? 主要原因:内嵌函数捕获了runningTotal,不再是单纯的一个变量了

    • 如果是下面这种方式调用呢?
    print(makeIncrementer()())
    print(makeIncrementer()())
    print(makeIncrementer()())
    
    <!--打印结果-->
    11
    11
    11
    

    为什么这种方式每次打印的结果就是同一个呢?

    1、SIL分析

    将上述代码通过SIL分析:

    • 1、通过alloc_box申请了一个堆上的引用计数,并将引用计数地址给了RunningTotal,将变量存储到堆上
    • 2、通过project_box从堆上取出变量
    • 3、将取出的变量交给闭包进行调用
      捕获一个变量的SIL分析
      结论:所以,捕获值的本质是 将变量存储到堆上

    2、断点验证

    • 也可以通过断点来验证,在makeIncrementer方法内部调用了swift_allocObject方法
      捕获一个变量的断点分析

    总结

    • 一个闭包能够从上下文捕获已经定义的常量和变量,即使这些定义的常量和变量的原作用域不存在,闭包仍然能够在其函数体内引用和修改这些值

    • 当每次修改捕获值时,修改的是堆区中的value值

    • 当每次重新执行当前函数时,都会重新创建内存空间

    所以上面的案例中我们知道:

    • makeInc是用于存储makeIncrementer函数调用的全局变量,所以每次都需要依赖上一次的结果

    • 而直接调用函数时,相当于每次都新建一个堆内存,所以每次的结果都是不关联的,即每次结果都是一致的

    闭包是引用类型

    这里还要一个疑问,makeInc存储的到底是什么?个人猜测存储的是runningTotal的堆区地址,下面我们通过分析来验证是否如此

    但是此时我们发现,通过SIL并没有办法分析出什么,那么可以将SIL降一级,通过IR代码来观察数据的构成

    在分析之前,首先来了解下IR的基本语法

    IR基本语法

    • 通过以下命令将代码转换为IR文件
    swiftc -emit-ir 文件名 > ./main.ll && code main.ll
    
    例如:
    - cd 文件所在路径
    - swiftc -emit-ir main.swift > ./main.ll && open main.ll
    
    • 数组
    /*
    - elementnumber 数组中存放数据的数量
    - elementtype 数组中存放数据的类型
    */
    [<elementnumber> x <elementtype>]
    
    <!--举例-->
    /*
    24个i8都是0
    - iN:表示多少位的整型,即8位的整型 - 1字节
    */
    alloca [24 x i8], align 8
    
    • 结构体
    /*
    - T:结构体名称
    - <type list> :列表,即结构体的成员列表
    */
    //和C语言的结构体类似
    %T = type {<type list>}
    
    
    <!--举例-->
    /*
    - swift.refcounted:结构体名称
    - %swift.type*:swift.type指针类型
    - i64:64位整型 - 8字节
    */
    %swift.refcounted = type { %swift.type*, i64}
    
    • 指针类型
    <type> *
    
    <!--举例-->
    //64位的整型 - 8字节
    i64*
    
    • getelementptr指令
      在LLVM中获取数组和结构体的成员时通过getelementptr,语法规则如下:
    <result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*
    
    <result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
    
    <!--举例-->
    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[]) {
        
        munge(array);
        
        return 0;
    }
    

    通过下面的命令将c/c++编译成IR

    clang -S -emit-llvm 文件名 > ./main.ll && code main.ll
    
    <!--举例-->
    clang -S -emit-llvm ${SRCROOT}/06-EnumTestC/main.c > ./main.ll && code main.ll
    
    IR代码分析
    • 第一个索引:%struct.munger_struct* %13, i32 0 等价于 第一个索引类型 + 第一个索引值 ==》 共同决定 第一个索引的偏移量
    • 第二个索引:i32 0

    再结合图来理解

    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代码应该是这样的:
    /*
    - [4 x i32]* array:数组首地址
    - 第一个0:相对于数组自身的偏移,即偏移0字节 0 * 4字节
    - 第二个0:相对于数组元素的偏移,即结构体第一个成员变量 0 * 4字节
    */
    a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i64 0
    
    • 可以看到其中的第一个0,使用基本类型[4 x i32],因此返回的指针前进0 * 16字节,即当前数组首地址
    • 第二个index,使用基本类型 i32,返回的指针前进0字节,即当前数组的第一个元素,返回的指针类型是 i32*
      IR代码-指针类型图示

    总结

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

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

    • 后面的索引是在数组或者结构体内进行索引

    • 每增加一个索引,就会使得该索引使用基本类型和返回的指针类型去掉一层(例如 [4 x i32] 去掉一层是 i32)

    IR分析

    分析IR代码

    • 查看makeIncrementer方法
      • 1、首先通过swift_allocObject创建swift.refcounted结构体
      • 2、然后将swift.refcounted转换为<{ %swift.refcounted, [8 x i8] }>*结构体(即Box)
      • 3、取出结构体中index等于1的成员变量,存储到[8 x i8]*连续的内存空间中
      • 4、将内嵌函数的地址存储到i8即void地址中
      • 5、最后返回一个结构体
    makeIncrementer函数-IR分析1

    其结构体定义如下


    makeIncrementer函数-结构体定义

    仿写

    通过上述的分析,仿写其内部的结构体,然后构造一个函数的结构体,将makeInc的地址绑定到结构体中

    struct HeapObject {
        var type: UnsafeRawPointer
        var refCount1: UInt32
        var refCount2: UInt32
    }
    
    //函数返回值结构体
    //BoxType 是一个泛型,最终是由传入的Box决定的
    struct FunctionData<BoxType>{
        //内嵌函数地址
        var ptr: UnsafeRawPointer
        var captureValue: UnsafePointer<BoxType>
    }
    
    //捕获值的结构体
    struct Box<T> {
        var refCounted: HeapObject
        var value: T
    }
    
    //封装闭包的结构体,目的是为了使返回值不受影响
    struct VoidIntFun {
        var f: () ->Int
    }
    
    //下面代码的打印结果是什么?
    func makeIncrementer() -> () -> Int{
        var runningTotal = 10
        //内嵌函数,也是一个闭包
        func incrementer() -> Int{
            runningTotal += 1
            return runningTotal
        }
        return incrementer
    }
    let makeInc = VoidIntFun(f: makeIncrementer())
    
    let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
    //初始化的内存空间
    ptr.initialize(to: makeInc)
    //将ptr重新绑定内存
    let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
         $0.pointee
    }
    print(ctx.ptr)
    print(ctx.captureValue.pointee)
    
    <!--打印结果-->
    0x0000000100002bc0
    Box<Int>(refCounted: _7_Clourse.HeapObject(type: 0x0000000100004038, refCount1: 3, refCount2: 2), value: 10)
    
    • 终端命令查找0000000100002bc0(其中0x0000000100002bc0内嵌函数的地址
    nm -p /Users/chenjialin/Library/Developer/Xcode/DerivedData/07、Clourse-bsccpnlhsrkbzkdglsojfgisewnx/Build/Products/Debug/07、Clourse | grep 0000000100002bc0
    

    其中s10_7_Clourse15makeIncrementerSiycyF11incrementerL_SiyFTA是内嵌函数的地址对应的符号

    内嵌函数对应的符号

    结论:所以当我们var makeInc2 = makeIncrementer()使用时,相当于给makeInc2就是FunctionData结构体,其中关联了内嵌函数地址,以及捕获变量的地址,所以才能在上一个的基础上进行累加

    捕获两个变量的情况

    上面的案例中,我们分析了闭包捕获一个变量的情况,如果是将捕获一个变量更改为捕获两个变量呢?如下所示修改makeIncrementer函数

    func makeIncrementer(forIncrement amount: Int) -> () -> Int{
        var runningTotal = 0
        //内嵌函数,也是一个闭包
        func incrementer() -> Int{
            runningTotal += amount
            return runningTotal
        }
        return incrementer
    }
    
    • 查看其IR代码


      捕获两个变量的IR分析

    内部结构仿写

    根据捕获一个变量的仿写,继续仿写捕获两个变量的情况

    //2、闭包捕获多个值的原理
    struct HeapObject {
        var type: UnsafeRawPointer
        var refCount1: UInt32
        var refCount2: UInt32
    }
    
    //函数返回值结构体
    //BoxType 是一个泛型,最终是由传入的Box决定的
    struct FunctionData<BoxType>{
        var ptr: UnsafeRawPointer//内嵌函数地址
        var captureValue: UnsafePointer<BoxType>
    }
    
    //捕获值的结构体
    struct Box<T> {
        var refCounted: HeapObject
        var value: T
    }
    
    //封装闭包的结构体,目的是为了使返回值不受影响
    struct VoidIntFun {
        var f: () ->Int
    }
    
    //下面代码的打印结果是什么?
    func makeIncrementer(forIncrement amount: Int) -> () -> Int{
        var runningTotal = 0
        //内嵌函数,也是一个闭包
        func incrementer() -> Int{
            runningTotal += amount
            return runningTotal
        }
        return incrementer
    }
    var makeInc = makeIncrementer(forIncrement: 10)
    var f = VoidIntFun(f: makeInc)
    
    let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
    //初始化的内存空间
    ptr.initialize(to: f)
    //将ptr重新绑定内存
    let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
         $0.pointee
    }
    print(ctx.ptr)
    print(ctx.captureValue)
    
    <!--打印结果-->
    0x0000000100002910
    0x00000001040098e0
    
    • 通过终端命令查看第一个地址是否是内嵌函数的地址

      经过包装的内嵌函数地址
      注:(函数必须使用VoidIntFun包装下,否则转换后的地址不是内嵌函数的地址),如下所示
      未经过包装
    • 通过cat查看 第一个地址,即内嵌函数的地址

      cat查看内嵌函数地址
      • x/8g 第二个地址


        查看内嵌函数地址内存情况-1
      • 继续查看内存情况


        查看内存情况-2

    如果将runningTotal改成12呢?来验证是否如我们猜想的一样。事实证明,确实是存储的runningTotal

    修改后的再次验证
    所以,闭包捕获两个变量时,Box结构体内部发生了变化,修改后的仿写代码如下:
    //2、闭包捕获多个值的原理
    struct HeapObject {
        var type: UnsafeRawPointer
        var refCount1: UInt32
        var refCount2: UInt32
    }
    
    //函数返回值结构体
    //BoxType 是一个泛型,最终是由传入的Box决定的
    struct FunctionData<BoxType>{
        var ptr: UnsafeRawPointer//内嵌函数地址
        var captureValue: UnsafePointer<BoxType>
    }
    
    //捕获值的结构体
    struct Box<T> {
        var refCounted: HeapObject
        //valueBox用于存储Box类型
        var valueBox: UnsafeRawPointer
        var value: T
    }
    
    //封装闭包的结构体,目的是为了使返回值不受影响
    struct VoidIntFun {
        var f: () ->Int
    }
    
    //下面代码的打印结果是什么?
    func makeIncrementer(forIncrement amount: Int) -> () -> Int{
        var runningTotal = 12
        //内嵌函数,也是一个闭包
        func incrementer() -> Int{
            runningTotal += amount
            return runningTotal
        }
        return incrementer
    }
    
    var makeInc = makeIncrementer(forIncrement: 10)
    var f = VoidIntFun(f: makeInc)
    
    let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
    //初始化的内存空间
    ptr.initialize(to: f)
    //将ptr重新绑定内存
    let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int, Int>>.self, capacity: 1) {
         $0.pointee
    }
    print(ctx.ptr)
    print(ctx.captureValue.pointee)
    print(ctx.captureValue.pointee.valueBox)
    
    <!--打印结果-->
    0x0000000100002b30
    Box<Int>(refCounted: _7_Clourse.HeapObject(type: 0x0000000100004090, refCount1: 3, refCount2: 4), valueBox: 0x00000001006094a0, value: 10)
    0x00000001006094a0
    

    疑问:如果是捕获3个变量呢?

    • 如下所示,是捕获三个值的内存情况


      捕获三个变量的内存分析
    • 通过IR文件发现,从返回值倒推

    <!--返回值-->
    ret { i8*, %swift.refcounted* } %15
    
    <!--%15-->
    %15 = insertvalue { i8*, %swift.refcounted* }
    { i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementer12forIncrement7amount2SiycSi_SitF11incrementerL_SiyFTA" to i8*),
        %swift.refcounted* undef }, %swift.refcounted* %10, 1
    
    <!--%10-->
    //与捕获两个变量相比,区别在于 i64 32 变成了 i64 40
    %10 = call noalias %swift.refcounted* @swift_allocObject(
    %swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata.3, i32 0, i32 2),
    i64 40, i64 7) #1
    

    所以Box结构体改为

    //捕获值的结构体
    struct Box<T> {
        var refCounted: HeapObject
        //这也是一个HeapObject
        var valueBox: UnsafeRawPointer
        var value1: T
        var value2: T
    }
    

    最终完整的仿写代码为

    //3、捕获3个值
    struct HeapObject {
        var type: UnsafeRawPointer
        var refCount1: UInt32
        var refCount2: UInt32
    }
    
    //函数返回值结构体
    //BoxType 是一个泛型,最终是由传入的Box决定的
    struct FunctionData<BoxType>{
        var ptr: UnsafeRawPointer//内嵌函数地址
        var captureValue: UnsafePointer<BoxType>
    }
    
    //捕获值的结构体
    struct Box<T> {
        var refCounted: HeapObject
        //valueBox用于存储Box类型
        var valueBox: UnsafeRawPointer
        var value1: T
        var value2: T
        
    }
    
    //封装闭包的结构体,目的是为了使返回值不受影响
    struct VoidIntFun {
        var f: () ->Int
    }
    //下面代码的打印结果是什么?
    func makeIncrementer(forIncrement amount: Int, amount2: Int) -> () -> Int{
        var runningTotal = 1
        //内嵌函数,也是一个闭包
        func incrementer() -> Int{
            runningTotal += amount
            runningTotal += amount2
            return runningTotal
        }
        return incrementer
    }
    var makeInc = makeIncrementer(forIncrement: 10, amount2: 2)
    var f = VoidIntFun(f: makeInc)
    
    let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
    //初始化的内存空间
    ptr.initialize(to: f)
    //将ptr重新绑定内存
    let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
         $0.pointee
    }
    print(ctx.ptr)
    print(ctx.captureValue.pointee.value1)
    print(ctx.captureValue.pointee.value2)
    
    <!--打印结果-->
    10
    2
    

    从打印结果可以看出,正好是传入的两个参数值

    总结

    • 1、捕获值原理:在堆上开辟内存空间,并将捕获的值放到这个内存空间里

    • 2、修改捕获值时:实质是修改堆空间的值

    • 3、闭包是一个引用类型(引用类型是地址传递),闭包的底层结构(是结构体:函数地址 + 捕获变量的地址 == 闭包

    • 4、函数也是一个引用类型(本质是一个结构体,其中只保存了函数的地址),例如还是以makeIncrementer函数为例

    func makeIncrementer(inc: Int) -> Int{
        var runningTotal = 1
        return runningTotal + inc
    }
    
    var makeInc = makeIncrementer
    

    分析其IR代码,函数在传递过程中,传递的就是函数的地址

    函数是引用类型分析-1
    将仿写的FunctionData进行修改
    struct FunctionData{
        var ptr: UnsafeRawPointer//内嵌函数地址
        var captureValue: UnsafePointer<BoxType>
    }
    

    然后改版后的结构仿写如下

    //函数也是引用类型
    struct FunctionData{
        //函数地址
        var ptr: UnsafeRawPointer
        var captureValue: UnsafeRawPointer?
    }
    
    //封装闭包的结构体,目的是为了使返回值不受影响
    struct VoidIntFun {
        var f: (Int) ->Int
    }
    
    func makeIncrementer(inc: Int) -> Int{
        var runningTotal = 1
        return runningTotal + inc
    }
    
    var makeInc = makeIncrementer
    var f = VoidIntFun(f: makeInc)
    
    let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
    //初始化的内存空间
    ptr.initialize(to: f)
    //将ptr重新绑定内存
    let ctx = ptr.withMemoryRebound(to: FunctionData.self, capacity: 1) {
         $0.pointee
    }
    
    print(ctx.ptr)
    print(ctx.captureValue)
    
    <!--打印结果-->
    0x0000000100003370
    nil
    

    通过cat命令查看该地址,地址就是makeIncrementer函数的地址

    函数是引用类型分析-2

    总结

    • 一个闭包能够从上下文中捕获已经定义的常量/变量,即使其作用域不存在了,闭包仍然能够在其函数体内引用、修改

      • 1、每次修改捕获值:本质修改的是堆区中的value值

      • 2、每次重新执行当前函数,会重新创建新的内存空间

    • 捕获值原理:本质是在堆区开辟内存空间,并将捕获值存储到这个内存空间

    • 闭包是一个引用类型(本质是函数地址传递),底层结构为:闭包 = 函数地址 + 捕获变量的地址

    • 函数也是引用类型(本质是结构体,其中保存了函数的地址)

    相关文章

      网友评论

        本文标题:Swift-进阶 09:闭包(一)使用&捕获原理

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