Swift闭包

293 阅读12分钟

1、函数类型

  • 函数由 形式参数 类型,返回类型 组成,在Swift中是 引用类型 image.png

1.1、函数结构

  • 我们在源码的 Metadata.h 文件中找到 TargetFunctionTypeMetadataimage.png image.png

    • TargetFunctionTypeMetadata 继承于 TargetMetadata,那么它的结构中包含 Kind,源码中看到它自身又拥有FlagsResultTypegetParameters 函数
      • FlagsTargetFunctionTypeFlags 类型,获取其中数据需要和 掩码操作 image.png 看源码知道基本都是通过 Data & 对应掩码的方式获取数据,这里的 Data 就是 Flagsimage.png
      • ResultType :是返回值类型的元数据
      • getParameters :这个函数通过 reinterpret_cast(this + 1) 强制转换成 Parameter * 类型,注意!它返回的是指针类型。所以这个函数返回的是一块连续的内存空间,这一块连续的内存空间存储的是 Parameter 类型的数据。
  • 还原函数结构

    struct TargetFunctionTypeMetadata {
        var Kind: Int
        var Flags: Int
        var ResultType: Any.Type
        var parameters: ParametersBuffer<Any.Type>
    
        func getNumParameters() -> Int { self.Flags & 0x0000FFFF }
    }
    
    struct ParametersBuffer<Element>{
        var element: Element
        mutating func buffer(n: Int) -> UnsafeBufferPointer<Element> {
            return withUnsafePointer(to: &self) {
            let ptr = $0.withMemoryRebound(to: Element.self, capacity: 1) { start in
                return start
            }
                return UnsafeBufferPointer(start: ptr, count: n)
            }
        }
    
        mutating func index(of i: Int) -> UnsafeMutablePointer<Element> {
            return withUnsafePointer(to: &self) {
                return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: Element.self).advanced(by: i))
            }
        }
    }
    

1.2、通过还原函数结构打印参数内容

  • 我们这里对参数进行调整,获取参数个数和类型
    func addTwoNum(_ a: Double, _ b: Int) -> Double {
        return a + Double(b)
    }
    
    let functionType = unsafeBitCast(type(of: addTwoNum) as Any.Type, to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)
    
        let numParameters = functionType.pointee.getNumParameters()
    
        print("函数参数的个数:\(numParameters)")
    
        for i in 0..<numParameters {
            print("第\(i)个参数的类型:\(functionType.pointee.parameters.index(of: i).pointee)")
        }
        print("函数参数的返回值类型:\(functionType.pointee.ResultType)")
    

    函数参数的个数:2
    第0个参数的类型:Double
    第1个参数的类型:Int
    函数参数的返回值类型:Double

2、闭包

  • 闭包是一个 捕获了上下文的常量或者是变量的函数,是特殊的函数(就是所谓的闭合并包裹那些常量和变量,因此被称为“闭包image.png makeIncrementer 函数在 return 之后就会连带参数 runningTotal 也被释放,incrementer 函数中需要捕获并使用 runningTotalincrementer函数与runningTotal组成了一个闭包

  • 闭包有三种形式:

    • 全局函数:是一个有名字但不会捕获任何值的特殊闭包
    • 内嵌函数:上边的例子就是内嵌函数
    • 闭包表达式:是一个轻量级语法所写的可以捕获其上下文中常量或变量值的没有名字的闭包

2.1、闭包表达式

  • 闭包的常用书写:作用域(也就是大括号)、参数列表返回值函数体in 之后的代码)

    { 
        (参数列表) -> 返回值类型 in 
        do something 
    }
    
    //示例,可做变量也可做参数
    var closure: (Int,Int) -> (Int) = {
        (a:Int ,b:Int) -> Int in
        return a + b
    }
    var i = closure(10,20)
    print(i)
    
  • 闭包可以 通过let声明为常量可以作为变量可以作为参数、也 可以为可选类型

    • 闭包常量
      image.png
    • 可选闭包 image.png
    • 闭包参数 image.png

2.2、闭包表达式的简写

  • 我们定义一个函数 exec,这个函数接收三个参数,第一个参数和第二个参数的类型为 Int 类型,第三个参数的类型为函数类型。这个函数类型传两个 Int 类型的参数,返回 Int 类型的参数。
    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
    })
    // 第三种
    exec(v1: 10, v2: 20, fn: { v1, v2 in
        v1 + v2
    })
    // 第四种
    exec(v1: 10, v2: 20, fn: { $0 + $1 })
    // 第五种
    exec(v1: 10, v2: 20, fn: +)
    
    • 第一种写法没有任何的简写,闭包表达式的参数名、参数类型、返回值类型和函数体全部写上

    • 第二种写法省略了 参数类型返回值类型

    • 第三种写法又省略了 return,在 Swift 中,如果函数中返回值的代码只有一句的话可以省略 return

    • 第四种写法相对于第三种写法 省略了参数名in,因为函数体比较简单,所以可以直接用 $0$1 分别代表 v1 和 v2 两个参数

    • 第五种写法相对于第四种写法省略了函数体,因为函数体的实现过于简单,只是两个参数相加,所以可以直接用 + 表示

2.3、尾随闭包

  • 如果将一个很长的闭包表达式作为 函数的最后一个实参,使用尾随闭包可以增强函数的可读性
    • 虽然是函数的一个参数,但不与参数列表写在一起;第 2 种简写的 exec 函数改为尾随闭包方式:
      exec(v1: 10, v2: 20) { v1, v2 in
          return v1 + v2
      }
      
    • 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数后面加圆括号
      func exec(fn: (Int, Int) -> Int) {
          print(fn(10, 20))
      }
      
      exec(fn: {$0 + $1})
      exec() {$0 + $1}    //省略了return
      exec {$0 + $1}
      

3、闭包捕获值

  • Block分 全局(不使用外部变量,或使用静态变量、全局变量)、(使用了局部变量、OC属性或赋值给了强引用)、(使用了局部变量、OC属性但没有赋值给强引用),但闭包不分(存储metadata,操作地址)
  1. 首先我们准备一段代码 image.png
  2. 我们将它编译成 SIL 文件,看到通过alloc_box(SIL官网解释是 在堆区创建足够容纳变量的内存空间)在堆区创建全局变量 i 并捕获地址 image.png
  3. 向下查找定义的闭包 closure 名字,发现在闭包中对 i 使用的是debug_value_addr,即是对地址进行操作 image.png

总结

  • 如果捕获的是全局变量,那么就不会向堆区捕获,而是改为通过global_addr直接获取变量地址操作,但不捕获变量严格意义上称不上是闭包,而只是一个内部函数

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

    • 1、每次 修改捕获值 :本质修改的是堆区中的value值
    • 2、每次 重新执行当前函数 ,会重新创建新的内存空间
  • 捕获值原理:本质是在堆区开辟内存空间,并将捕获值存储到这个 内存空间

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

4、闭包的本质

  • 要了解本质,SIL文件已经不够我们分析,我们需要向下看 IR 文件,因此我们先简单了解一下IR的语法

4.1、IR语法

  1. 数组

    [<elementnumber> x <elementtype>//example 
    alloca [24 x i8], align 8 24个i8都是0 
    
    alloca [4 x i32] === array
    
    • i32:32位的整型
    • i8:代表的比较多,可能是 Int8,也可能是void *
  2. 结构体

    %swift.refcounted = type { %swift.type*, i64 } 
    
    //表示形式
    %T = type {<type list>} //这种和C语言的结构体类似
    
    • swift.refcounted:结构体名称
    • %swift.type*:swift.type指针类型
    • i64:64位整型 - 8字节
  3. 指针类型

    <type> * 
    
    //example
    i64* //64位的整形
    
  4. getelementptr 指令

  • LLVM中我们 获取数组和结构体的成员 的指令,语法规则如下:

    <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;
    }
    
    int main(int argc, const char * argv[]) {
        int array[4] = {1, 2, 3, 4};
        int a = array[0];
        return 0;
    }
    
    - [4 x i32]* array:数组首地址
    - 第一个索引值0:相对于数组自身的偏移,即偏移0字节 0 * 4字节
    - 第二个索引值0:相对于数组元素的偏移,即结构体第一个成员变量 0 * 4字节
    - 如果数组中嵌套了数组,那么还需要再加一个索引
    */
    

    其中 int a = array[0] 这句对应的LLVM代码应该是这样的:

    a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i32 0, i32 0 image.png

总结:
  • 第一个索引不会改变返回的指针的类型,也就是说ptrval前面的*对应什么类型,返回就是什么类型 
  • 第一个索引的偏移量的是由第一个索引的值和第一个ty指定的基本类型共同确定的
  • 后面的索引是在数组或者结构体内进行索引 
  • 每增加一个索引,就会使得该索引使用的基本类型和返回的指针的类型去掉一层

4.2、分析IR代码

  • 编译如下代码为 IR 格式:

    func getNewNum() -> (Int) -> Int {
        var num = 0
        func plus(_ i: Int) -> Int {
            num += i
            return num
        }
        return plus
    }
    let fn = getNewNum()
    
    • 添加脚本语言:

      swiftc -emit-ir ${SRCROOT}/项目名/main.swift > ./main.ll && open main.ll image.png

  • main函数分析 image.png

  • getNewNum方法分析 image.pngswift.refcounted转换为<{ %swift.refcounted, [8 x i8] }>*结构体(即Box)

4.3、闭包的结构

  • 数据结构:闭包的执行地址 + 捕获变量堆空间的地址

    struct ClosureData<Box> {
        /// 函数地址
        var ptr: UnsafeRawPointer
        /// 存储捕获堆空间地址的值
        var object: UnsafePointer<Box>
    }
    
    struct Box<T>{
        var heapObject: HeapObject
        // 捕获变量/常量的值
        var value: T
    }
    
    struct HeapObject {
        var matedata: UnsafeRawPointer
        var refcount: Int
    }
    
    • 闭包本质上就是一个 { i8*, %swift.refcounted* } 这样的结构
      • i8* 存储的是函数的地址
      • %swift.refcounted* 存储的是一个 box *({ %swift.refcounted, [8 x i8] })
    • box * 里有 HeapObject * 和一个 64 位的 value。
    • HeapObject * 分别存储 metadatarefcount
  • 代码验证

    //用结构体包裹一下闭包方便作指针的转换
    struct ClosureStruct {
        var closure :(Int) -> Int
    }
    
    var fn = ClosureStruct(closure: getNewNum())
    fn.closure(20)
    
    //fn 初始化一个ClosureStruct类型指针
    let ptr = UnsafeMutablePointer<ClosureStruct>.allocate(capacity: 1)
    ptr.initialize(to: fn)
    
    //内存重新绑定为 ClosureData<Box<Int>>
    let ctx = ptr.withMemoryRebound(to: ClosureData<Box<Int>>.self, capacity: 1){
        $0.pointee
    }
    print("闭包的调用地址:",ctx.ptr)
    print("堆空间地址:",ctx.object)
    print("堆空间存储的值", ctx.object.pointee.value)
    ptr.deinitialize(count: 1)
    ptr.deallocate()
    
    //打印内容
    闭包的调用地址: 0x00000001000058b0
    堆空间地址: 0x00000001010603c0
    堆空间存储的值 20
    

    image.png

4.4、捕获多个值

  • 捕获单个值和多个值的区别就在于:

    • 捕获单个值时,ClosureData 内存储的堆空间地址,就是 这个值所在的堆空间

    • 捕获多个值时,ClosureData 内存储的堆空间地址,会变成 一个可以存储很多个捕获值的结构,取值时会从这个结构体中获取

  • 捕获多个值比捕获单个值时多一层结构,结构如下:

    // 捕获单个值的 ClosureData
    struct ClosureData<Box> {
        /// 函数地址
        var ptr: UnsafeRawPointer
        /// 存储捕获堆空间地址的值
        var object: UnsafePointer<Box>
    }
    
    // 捕获多个值的 ClosureData
    struct ClosureData<MutiValue>{
        /// 函数地址
        var ptr: UnsafeRawPointer
        /// 存储捕获堆空间地址的值
        var object: UnsafePointer<MutiValue>
    }
    
    struct MutiValue<T1, T2, ......>{
        var object: HeapObject
        var value1:  UnsafePointer<Box<T1>>
        var value2:  UnsafePointer<Box<T2>>
        var valueX:  UnsafePointer<Box<TX>>
        ......
    }
    
    struct Box<T>{
        var object: HeapObject
        var value: T
    }
    
    struct HeapObject {
        var matedata: UnsafeRawPointer
        var refcount: Int
    }
    

5、OC 与 Swift 调用

5.1、Swift调用OC的Block

  • 创建OC文件,定义方法 image.png image.png

    • 对文件进行编译后,可以看到转化的内容 image.png
  • Swift文件中调用 image.png

5.2、OC调用Swift闭包

  • OC想要调swift代码 必须 继承自NSObject 并添加@objc
    class LZPerson: NSObject@objc static var closure: (() -> ())? 
    } 
    
    + (void)test{ 
        LZPerson.closure = ^NSLog(@"end");
        };
    }
    
    LZTest.test()
    LZPerson.closure!()
    

补充

  • @convention :用于修饰函数类型 
    • 修饰 Swift 中的函数类型,调用 C 的函数时候,可以传入修饰过@convention(c)的函数类型,匹配 C 函数参数中的函数指针

      //C文件.h
      #ifndef TestC_h
      #define TestC_h
      
      #include <stdio.h>
      
      int TestCFUnction(int (callBack)(int a, int b));
      
      #endif /* TestC_h */
      
      //C文件.c
      #include "TestC.h"
      int TestCFUnction(int (callBack)(int a, int b)){
          return callBack(10, 20);
      }
      

      image.png

    • 修饰 Swift 中的函数类型,调用 Objective-C 的方法时候,可以传入修饰过@convention(block)的函数类型,匹配 Objective-C 方法参数中的 block 参数

      • 要在 Swift 中调用一个含有 block 的 Objective-C 的方法时,需要使用@convention(block)定义 Swift 变量才能传入到 Objective-C 的方法中。当然也可以直接使用闭包,这里我们举一个动画方法的例子
      [UIView animateWithDuration:2 animations:^{ 	
          NSLog(@"start"); 
      } completion:^(BOOL finished){  
          NSLog(@"completion"); 
      }];
      

      以上代码使用了 2 个 block,直接使用闭包转换成 Swift 代码:

      UIView.animateWithDuration(2, animations: { 	
          NSLog("start") 
      }, completion: { 	
          (Bool completion) in 		
              NSLog("completion") 
          })
      

      等价使用@convention(block)的代码如下:

      let animationsBlock : @convention(block) () -> () = { 	
          NSLog("start") 
      } 
      let completionBlock : @convention(block) (Bool) -> () = { 
          (Bool completion) in 		
              NSLog("start") 
      } 
              
      UIView.animateWithDuration(2, animations: animationsBlock, completion: completionBlock)
      

6、defer(延迟执行)

  • defer {} 里的代码会在当前代码块返回的时候执行,无论当前代码块是从哪个分支 return 的,即使程序抛出错误也会执行。 

  • 如果多个 defer 语句出现在同一作用域中,则它们 出现的顺序与它们执行的顺序相反,也就是先出现的后执行。

    func f() { 
        defer { print("1 defer") } 
        defer { print("2 defer") } 
        print("End") 
    } 
    f()
    
    //打印结果
    End
    2
    1
    

用途

  • 主要写在开头进行统一的资源管理,尽量不要做逻辑操作

  • 利用延迟执行、并一定会执行的特性,可以将例如 分支判断后统一执行的步骤放入defer{}中,可以有效减少代码冗余

  • 在进行网络请求的时候,可能有不同的分支进行回调函数的执行

    func netRquest(completion: () -> Void) { 
        deferself.isLoading = false 
            completion() 
        }
        guard error == nil else { return } 
    }
    

7、逃逸闭包

  • 逃逸闭包的定义:当闭包作为一个实际参数传递给一个函数的时候,并且是在函数返回之后调用,我们就说这个闭包逃逸了;当我们声明一个接受闭包作为形参的函数时,你可以在形参前写 @escaping 来明确闭包是允许逃逸的。
    • 作为函数的参数传递 
    • 当前闭包在函数内部异步执行或者被存储 
    • 函数结束 -> 闭包被调用 -> 生命周期结束逃逸闭包条件就是闭包生命周期比所在方法长
  • 方法执行的过程中不会等待闭包执行完成后再执行,而是直接返回,所以闭包的生命周期要比方法长
注意:可选类型默认是逃逸闭包!

逃逸闭包的两种使用情况

  1. 作为属性存储,在后面进行调用 image.png

  2. 延迟调用 image.png

非逃逸闭包相比逃逸闭包的好处

  • 不会产生循环引用,上下文保存在栈上,编译器性能优化
建议
  • 如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的,特殊情况时再使用逃逸闭包(基本只有 作为函数参数异步 两种情况)

8、自动闭包

  • 是一种用来把实际参数传递给函数表达式打包的闭包,不接受任何实际参数,当其调用时,返回内部表达式的值;关键字@autoclosure
    //将当前参数修改成一个闭包,并使用@autoclosure声明成一个自动闭包
    func debugOutPrint(_ condition: Bool, _ message: @autoclosure () -> String){
        if condition {
            print("debug: \(message())")
        }
    }
    
    func doSomething() -> String{
        print("doSomething")
        return "Network Error Occured"
    }
    
    //---使用1:传入函数
    debugOutPrint(true, doSomething())
    
    //---使用2:传入字符串
    debugOutPrint(true, "Application Error Occured")
    
    //打印结果
    doSomething
    debug: Network Error Occured
    debug: Application Error Occured
    
    • @autoclosure 只支持 () -> T 格式的参数
    • 可以使传入的参数在不确定的情况下,既可以是闭包,也可以是闭包的返回值