美文网首页
Swift 六、闭包(上)

Swift 六、闭包(上)

作者: 常在士心 | 来源:发表于2022-01-28 18:28 被阅读0次
    函数&闭包.png

    函数类型

    之前在代码的书写过程中,我们已经或多或少的接触过函数,函数本身也有自己的类型,它由形式参数类型,返回值类型组成。

    func addTwoInts(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
    var a = addTwoInts
    a(10, 20)
    

    addTwoInts就代表了我们当前函数的类型。
    那么如果在当前的项目代码当中,出现了相同的函数名称,不过它们的函数参数不同,这个时候我们应该如何去区分哪?

    func addTwoInts(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
    
    func addTwoInts(_ a: Double, _ b: Double) -> Double {
        return a + b
    }
    var a = addTwoInts
    
    image.png

    这里两个addTwoInts函数名相同,参数不同,它们是两个不同的函数,在这个过程中,编译器并不能识别addTwoInts到底是哪个函数类型。
    所以如果我们想要调用对应的函数方法,需要在变量中指定要赋值的类型。

    func addTwoInts(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
    func addTwoInts(_ a: Double, _ b: Double) -> Double {
        return a + b
    }
    var a: (Double, Double) -> Double = addTwoInts
    a(10, 20)
    var b = a
    b(20, 30)
    
    QQ20220126-105152@2x.png
    image.png
    这里的0x301其实存储的是我们的kind, 所以说我们当前的函数在我们的swift里面也是一个引用类型。
    下面我们通过源码来了解一下函数。
    metadata.h文件中搜索Function,我们找到了TargetFunctionTypeMetadata,这个就是我们当前的函数类型的源数据。
    image.png
    我们看到这里有一个TargetFunctionTypeFlags,那么这个类型里究竟都有什么东西哪,我们来看一下。
    image.png
    了解了当前函数的内部数据结构,我们就可以做一下对应的还原。
    struct TargetFunctionTypeMetadata {
        var kind: Int
        var flags: Int
        var arguments: ArgumentsBuffer<Any.Type>
        func numberArguments() -> Int {
            return self.flags & 0x0000FFFF
        }
    }
    
    struct ArgumentsBuffer<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))
            }
        }
    }
    

    我们可以通过这个数据结构,可以动态获取函数的相关信息。

    func funcInfo() {
        func add(_ a: Int, _ b: Int) -> Int {
            return a + b
        }
        let a: (Int, Int) -> Int = add
        let a1 = a(10, 20)
        print(a1)
        
        func add(_ a: Double, _ b: Double) -> Double {
            return a + b
        }
        
        let b: (Double, Double) -> Double = add
        let b1 = b(10, 20)
        print(b1)
        
        print("end")
        
        let a1Type = type(of: a)
        getFunctionInfo(a1Type as Any.Type)
        
        let b1Type = type(of: b)
        getFunctionInfo(b1Type as Any.Type)
        
    }
    
    func getFunctionInfo(_ type: Any.Type) {
        let funcType = unsafeBitCast(type, to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)
        let number = funcType.pointee.numberArguments()
        print("该函数有\(number)个参数,返回值类型是")
        for i in 0..<number {
            let argument = funcType.pointee.arguments.index(of: i).pointee
            let value = customCast(type: argument)
            print(value)
        }
    }
    
    protocol BrigeProtocol {}
    
    extension BrigeProtocol {
        static func get(from pointer: UnsafeRawPointer) -> Any {
            pointer.assumingMemoryBound(to: Self.self).pointee
        }
    }
    
    struct BrigeProtocolMetadata {
        let type: Any.Type
        let witness: Int
    }
    
    func customCast(type: Any.Type) -> BrigeProtocol.Type {
        let container = BrigeProtocolMetadata(type: type, witness: 0)
        let protocolType = BrigeProtocol.Type.self
        let cast = unsafeBitCast(container, to: protocolType)
        return cast
    }
    
    funcInfo()
    

    lldb输出打印结果:

    30
    30.0
    end
    该函数有2个参数,返回值类型是
    Int
    Int
    该函数有2个参数,返回值类型是
    Double
    Double
    

    什么是闭包

    闭包是一个捕获了上下文的常量或者是变量的函数。
    我们可以看一个官方给的案例:

    ///返回值为函数() -> Int,这个函数() -> Int的返回值是Int
    func makeIncrementer() -> () -> Int {
        var runningTotal = 10
        func incrementer() -> Int {
            runningTotal += 1
            return runningTotal
        }
        return incrementer
    }
    

    这里incrementer作为一个闭包,显然他是一个函数,其次为了保证其执行,要捕获外部变量runningTotal 到内部,所以闭包的关键就有捕获外部变量或常量函数

    闭包表达式

    { (param type) -> (return type) in
        ///do somethings
    }
    

    闭包在语法上有这样的标准结构: {(参数列表) -> 返回值 in 闭包体}

    • 作用域(也就是大括号)
    • 参数和返回值
    • 函数体in之后的代码
    1. 闭包即可以当做变量,也可以当做参数传递,这里我们来看一下下面的例子熟悉一下:
    var closure: (Int) -> Int = { (age: Int) in
        return age
    }
    
    1. 同样的我们也可以把我们的闭包声明成一个可选类型:
    ///错误的写法
    var closure: (Int) -> Int?
    closure = nil
    
    ///正确的写法
    var closure: ((Int) -> Int)?
    closure = nil
    
    1. 还可以通过 let 关键字将闭包声明为一个常量(也就意味着一旦赋值之后就不能改变了)
    let closure: (Int) -> Int
    closure = { (age: Int) in
        return age
    }
    

    4.同时也可以作为函数的参数

    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) {
    }
    

    使用闭包表达式能更简洁的传达信息。当然闭包表达式的好处有很多:

    • 利用上下文推断参数和返回值类型
    var array = [1, 2, 3]
    array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1 < item2 })
    array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
    
    • 单表达式可以隐式返回,既省略 return关键字
    var array = [1, 2, 3]
    array.sort(by: {(item1, item2) in return item1 < item2 })
    array.sort{(item1, item2) in item1 < item2 }
    
    • 参数名称的简写(比如我们的$0)
    var array = [1, 2, 3]
    array.sort{ return $0 < $1 }
    array.sort{ $0 < $1 }
    
    • 尾随闭包表达式
    var array = [1, 2, 3]
    array.sort(by: <)
    

    捕获值

    在讲闭包捕获值的时候,我们先来回顾一下 OC中Block 捕获值的情形。

    - (void)testBlock {
        NSInteger i = 1;
        void(^block) (void) = ^{
            NSLog(@"block %ld:", i);
        };
        i += 1;
        NSLog(@"before block %ld:", i);///2
        block();///1
        NSLog(@"after block %ld:", i);///2
    }
    

    那么如果我们想要外部的修改能够影响当前 block 内部捕获的值,我们只需要对当前的 i 添加 __block 修饰符。

    - (void)testBlock {
      __block  NSInteger i = 1;
        void(^block) (void) = ^{
            NSLog(@"block %ld:", i);
        };
        i += 1;
        NSLog(@"before block %ld:", i);///2
        block();///2
        NSLog(@"after block %ld:", i);///2
    }
    

    那么我们把上面的代码翻译成swift代码来看一下它会发生什么样的变化。

    var i = 1
    let closure = {
        print("closure \(i)")
    }
    i += 1
    print("before closure \(i)")
    closure()
    print("after closure \(i)")
    

    那么我们编译成SIL文件来看一下闭包捕获外部变量的过程。

    image.png
    可以看到是通过project_box来存放 i的查看官方文档
    var i = 10
    var closure = {
        print("closure \(i)")
    }
    
    image.png
    通过上图很明显看出,前8个字节存储的是metadata,在swift当中,其实并没有堆栈全局Block这种区别。
    func makeIncrementer() -> () -> Int {
        var runningTotal = 10
        func incrementer() -> Int {
            runningTotal += 1
            return runningTotal
        }
        return incrementer
    }
    
    let makeInc = makeIncrementer()
    
    print(makeInc())
    
    print(makeInc())
    
    print(makeInc())
    
    
    func makeIncrementer() -> () -> Int {
        var runningTotal = 10
        func incrementer() -> Int {
            runningTotal += 1
            return runningTotal
        }
        return incrementer
    }
    
    let makeInc = makeIncrementer()
    
    print(makeIncrementer()())
    
    print(makeIncrementer()())
    
    print(makeIncrementer()())
    

    我们看到,如果我们调用内部的闭包函数,返回的值是累加的,它返回的其实是runningTotal,而下面的代码,我们其实是调用的makeIncrementer,返回的是incrementer。

    OC Block 和 Swift闭包相互调用

    我们在OC中定义的Block,在Swift中是如何调用的那?我们来看一下。

    typedef void(^ResultBlock)(NSError *error);
    
    @interface ZGTest : NSObject
    
    + (void)testBlockCall:(ResultBlock)block;
    
    @end
    
    @implementation ZGTest
    
    + (void)testBlockCall:(ResultBlock)block {
        NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:400 userInfo:nil];
        block(error);
    }
    
    @end
    

    在 Swift 中我们可以这么使用

    ZGTest.testBlockCall { error in
        let errorcast = error as NSError
        print(errorcast)
    }
    
    func test(_ block: ResultBlock) {
        let error = NSError.init(domain: NSURLErrorDomain, code: 400, userInfo: nil)
        block(error)
    }
    

    同样的,比如我们在 Swift里这么定义,在OC中也是可以使用的。

    class ZGTeacher: NSObject {
        @objc static var closure: (() -> ())?
    }
    

    在OC中我们可以这么调用

    + (void)test {
        ZGTeacher.closure = ^{
            NSLog(@"end");
        };
    }
    

    闭包的本质

    • 闭包的核心是在其中使用的局部变量会被额外地复制或引用,使这些变量脱离其作用域后依然有效。
    • 每次修改捕获值的时候其实是修改堆区当中的value。
    • 当我们每次执行当前函数的时候其实每次都会创建新的内存空间。
      为了更好得理解这一过程,我们可以通过结构体来还原闭包的结构,并对它进行内存绑定,来获取闭包捕获的值。
      为了更清晰探知它内部的调用,我们将代码编译成IR文件,swift代码如下:
    func makeIncrementer() -> () -> Int {
        var runningTotal = 10
        func incrementer() -> Int {
            runningTotal += 1
            return runningTotal
        }
        return incrementer
    }
    let makeInc = makeIncrementer()
    

    swift 还原闭包的结构体

    在看具体的内容的时候,我们先来熟悉一下简单的 IR语法

    • 数组
    [<elementnumber> x <elementtype>]
    //example
    alloca [24 x i8], align 8 24个i8都是0
    alloca [4 x i32] === array
    
    • 结构体
    %swift.refcounted = type { %swift.type*, i64 }
    //表示形式
    %T = type {<type list>} //这种和C语言的结构体类似
    
    • 指针类型
    <type> *
    //example
    i64* //64位的整形
    
    • getelementptr 指令
      LLVM中我们获取数组和结构体的成员,通过 getelementptr ,语法规则如下:
    <result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
    <result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
    
    image.png

    总结:

    • 第一个索引不会改变返回的指针的类型,也就是说ptrval前面的*对应什么类型,返回就是什么类型。
    • 第一个索引的偏移量的是由第一个索引的值和第一个ty指定的基本类型共同确定的。
    • 后面的索引是在数组或者结构体内进行索引
    • 每增加一个索引,就会使得该索引使用的基本类型和返回的指针的类型去掉一层。
      还原闭包的数据类型
    func makeIncrementer() -> () -> Int {
        var runningTotal = 10
        func incrementer() -> Int {
            runningTotal += 1
            return runningTotal
        }
        return incrementer
    }
    
    ///{ i8*, %swift.refcounted* }
    ///数据结构:当前闭包的执行地址 + 捕获变量堆空间的地址
    struct ClosureData<Box> {
        var ptr: UnsafeRawPointer
        var object: UnsafePointer<Box>
    }
    
    ///实例对象的内存地址
    struct HeapObject {
        var metadata: UnsafeRawPointer
        var refcount1: Int32
        var refcount2: Int32
    }
    
    struct Box<T> {
        var object: HeapObject
        var value: T
    }
    
    struct NoMeanStruct {
        var f: () -> Int
    }
    
    var f = NoMeanStruct(f: makeIncrementer())
    let ptr = UnsafeMutablePointer<NoMeanStruct>.allocate(capacity: 1)
    ptr.initialize(to: f)
    
    let ctx = ptr.withMemoryRebound(to: ClosureData<Box<Int>>.self, capacity: 1) {
        $0.pointee
    }
    print(ctx.ptr)
    print(ctx.object)
    print("end")
    

    输出和调试如下:

    QQ20220128-160758@2x.png
    可以看到捕获的变量已经找到,还可以通过Mach-o文件来找对应的函数地址,来证实我们的猜想。 这里我们需要借助nm -p命令。
    $ nm -p <mach-o path> | grep <函数地址(不带0x)>
    

    注意这里函数地址是不加0x的。


    image.png
    QQ20220128-161723@2x.png

    可以看到通过字符串也可以对应到函数名称。

    @convention

    @convention :用于修饰函数类型

    • 修饰Swift中的函数类型(调用C函数的时候)
      C文件


      image.png
      image.png

      Swift文件


      image.png
    • 调用OC方法时,修饰Swift函数类型

    defer

    定义:defer {} 里的代码会在当前代码块返回的时候执行,无论当前代码块是从哪个分支 return 的,即使程序抛出错误,也会执行。
    如果多个 defer 语句出现在同一作用域中,则它们出现的顺序与它们执行的顺序相反,也就是先出现的后执行。
    defer能做什么?
    这里我们看一个简单的例子:

    func f() {
        defer {
            print("First defer")
        }
        defer {
            print("Second defer")
        }
        
        defer {
            print("End of function")
        }
    }
    
    f()
    

    lldb打印输出结果,验证了先出现的后执行。

    End of function
    Second defer
    First defer
    

    下面案例这里有时候如果当前方法中多次出现 closeFile ,那么我们就可以使用 defer

    func append(string: String, terminator: String = "\n", toFileAt url: URL) throws {
        let data = (string + terminator).data(using: .utf8)!
        let fileHandle = try FileHandle(forUpdating: url)
        defer {
            fileHandle.closeFile()
        }
        guard FileManager.default.fileExists(atPath: url.path) else {
            try data.write(to: url)
            return
        }
        
        fileHandle.seekToEndOfFile()
        fileHandle.write(data)
       
    }
    
    let url = URL(fileURLWithPath: NSHomeDirectory() + "/Desktop/swift.txt")
    try append(string: "iOS面试突击", toFileAt: url)
    try append(string: "swift", toFileAt: url)
    

    我们在使用指针的时候

    let count = 2
    let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
    pointer.initialize(repeating: 0, count: count)
    defer {
        pointer.deinitialize(count: count)
        pointer.deallocate()
    }
    

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

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

    defer本质其实是为了管理我们的代码块。

    相关文章

      网友评论

          本文标题:Swift 六、闭包(上)

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