Swift学习(七)闭包

348 阅读3分钟

和Object-C的Block一样,Swift中也有闭包与之对应。

函数类型

函数的类型由函数的返回值类型和参数类型共同组织,而函数类型和函数参数共同确定函数的唯一性。

image.png 如果有同名函数,在赋值的时候需要指定类型(很少有人这么写,swift也不推荐这样编码)。这里只是像从侧面证明函数其实也是引用类型。

//函数类型

    func func1() {
        func add(_ a: Int, _ b: Int) -> Int {
            return a + b
        }
    
        let a: (Int, Int) -> Int = add
        
        let b = a(10, 20)
        
        print(b)
        
        func add(_ a: Double, _ b: Double) -> Double {
            return a + b
        }
        
        let c: (Double, Double) -> Double  = add
    
        let d = c(10.0, 20.0)
        
        print(d)
    }

那我们根据上一遍文章的方法,可以从源码中寻找TargetFunctionTypeMetadata,来还原数据结构。

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 func1() {
        
        func add(_ a: Int, _ b: Int) -> Int {
            return a + b
        }
    
        let a: (Int, Int) -> Int = add
        
        let b = a(10, 20)
        
        print(b)
        
        func add(_ a: Double, _ b: Double) -> Double {
            return a + b
        }
        
        let c: (Double, Double) -> Double  = add
    
        let d = c(10.0, 20.0)
        print(d)
        
        print("end")
        
        
        let cType = type(of: c)
        getFunctionInfo(cType as Any.Type)
        
        let aType = type(of: a)
        getFunctionInfo(aType as Any.Type)
        
    }
    
    func getFunctionInfo(_ type: Any.Type) {
        let funcTypeC = unsafeBitCast(type, to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)

        let number = funcTypeC.pointee.numberArguments()        
        print("函数有\(number)个参数,返回值类型是")

        for i in 0..<number {
            let argument = funcTypeC.pointee.arguments.index(of: i).pointee
            let value = customCast(type: argument)
            print(value)
        }
    }

其中customCast方法如下:

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 {
    var container = BrigeProtocolMetadata(type: type, witness: 0)
    var protocolType = BrigeProtocol.Type.self
    var cast = unsafeBitCast(container, to: BrigeProtocol.Type.self)
    return cast
}

得到输出:

image.png 类似的方法还可以获取函数的返回值。

认识闭包

闭包是一个可以捕获上下文的常量或者变量的函数!

我们可以先看一看官方给的例子:

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、函数体构成。

闭包可以作为变量、和参数使用。

作为变量:

var closure: (Int) -> Int = { (a: Int) -> Int in
        return a + 100
}

作为参数:

func func3(_ someThing: @escaping (() -> Void)) {
        print("让子弹飞一会!")
        DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: someThing)
    }

尾随闭包

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

func func9() {
        var age = 10
        let a =  withUnsafePointer(to: &age) {
            $0.pointee
        }
        
        let b =  withUnsafePointer(to: &age, {ptr in
            return ptr.pointee
        })
        
        print(a, b)
        
        func8(18, 2) { a1, a2 in
            return a1 + a2
        }
        
        func8(18, 2, { a1, a2 in
            return a2 + a1
        })
    }
    
    func func8(_ a: Int, _ b: Int, _ complation: (Int, Int) -> Int) {
        let a = complation(a, b)
        print(a)
    }

可以看到尾随闭包让更清晰,但尾随闭包并没有实际意义,只是为了代码美观,增加可读性。

逃逸闭包

当闭包作为一个实际参数传递给一个函数的时候,并且是在函数返回之后调用,我们就说这个闭包逃逸了。当我们声明一个接受闭包作为形式参数的函数时,可以在形式参数前写 @escaping 来明确闭包是允许逃逸的。

func func3(_ someThing: @escaping (() -> Void)) {
        print("让子弹飞一会!")
        DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: someThing)
 }
class OSDeviceOffLineView: UIView {

    var sureClosure: (() -> Void)?
    
    var cancelClosure: (() -> Void) = {}
    
    func youGiveMeTwoClosure(_ sure: @escaping () -> Void, _ cancel: @escaping () -> Void) {
        print("set closure")
        self.sureClosure = sure
        self.cancelClosure = cancel
    }

逃逸闭包的特点有:

  • 作为函数的参数传递
  • 当前闭包在函数内部异步执行或者被存储
  • 函数结束,闭包被调用,生命周期结束

如果函数执行完成,闭包也执行完成,这时候就是一个非逃逸闭包,对于非逃逸闭包来说,闭包不需要将函数中的上下文(函数中的变量)捕获到堆区,因为他们的生命周期是一致的,函数执行完成,大家都释放了。但如果是逃逸闭包是需要把捕获变量捕获到堆区的,因为有可能变量已经释放,但闭包还是需要使用。 注意:可选类型默认是逃逸闭包!!!所以总结非逃逸闭包的特点如下:

  • 不会产生循环引用,函数作用域内释放
  • 编译器更多性能优化 (retain, relsase)
  • 上下文的内存保存再栈上,不是堆上

Block

我们知道Object-C中的Block是分为3中类型----GlobelBlock、StackBlock、MallocBlock。

- (void)fun2 {
    NSLog(@"%@",^{});
    
    void(^block1)(void) = ^{
        
    };
    
    NSInteger i = 1;
    void(^block2)(void) = ^{
        NSLog(@"block %ld", i);
    };
    
    void(^__weak block3)(void) = ^{
        NSLog(@"block %ld", i);
    };
    
    NSLog(@"%@",block1);
    NSLog(@"%@",block2);
    NSLog(@"%@",block3);
}

image.png

可以总结,区别大致如下。

类别区别
全局Block没有捕获局部变量
栈Block捕获局部变量,在堆区没有强引用
堆Block捕获局部变量,在堆区有强引用
注意如果有捕获变量,在从栈区引用到堆区的过程中,捕获对象的引用计数会+1.经常会有类似的面试题。
- (void)func3 {
    NSObject *o = [[NSObject alloc] init];
    NSLog(@"CFGetRetainCount print");
    NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
    //1
    
    void(^strongBlock)(void) = ^ {
        NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
    };
    strongBlock();
    //3  在栈区引用一次,在堆区又引用一次
    
    void(^ __weak weakBlock)(void) = ^{
        NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
    };
    weakBlock();
    //4栈区引用一次
    
    void(^copyBlock)(void) = [strongBlock copy];
    copyBlock();
    // 4 本来就在栈上不用+1。
    
    void(^copyBlock1)(void) = [weakBlock copy];
    copyBlock1();
    // 5
    NSLog(@"CFGetRetainCount print");
}

输出:

 CFGetRetainCount print**
 1**
 3**
 4**
 4**
 5**
 CFGetRetainCount print**

解析:创建对象赋值给o,引用计数为1,被Block捕获引用计数为2,Block被赋值在堆上的strongBlock,应用计数再次加1,所有第二次打印为3,又被Block捕获,引用计数加1,第三次打印为4,而第一个Block在堆中已经有引用了,所以引用计数不会变,所以第四次打印还是4。而第二个Block在堆中没有引用,再次给它在堆中进行引用后,引用计数再次加1,最后一次打印为5

闭包是没有这种分类的。

闭包捕获变量

func OSClosureSIL() {
    var i: Int = 1
    let clousure: ()->Int = {
        i = i+1
        print(i)
        return i
    }
    
    let _ = clousure()
}

讲以上代码通过swiftc -emit-sil转换为SIL代码,来看看闭包捕获外部变量的过程。 image.png 可以看到是通过project_box来存放 i的查看官方文档

image.png

image.png

image.png 说白了,做的操作就是在堆区开辟空间来存储它。

为了更好得理解这一过程,我们可以通过结构体来还原闭包的结构,并对它进行内存绑定,来获取闭包捕获的值。

//还原闭包的数据类型
    func func5() {
        var i: Int = 1
        var j: Int = 10
        let closure = { () -> Int in
            print("closure\(i)")
            return i+j
        }
        
        struct NoMeanStruct {
            var f: () -> Int
        }
        //转换一下、NoMeanStruct结构和f的结构完全一样。
        var f = NoMeanStruct(f: closure)
        //新建指针
        let ptr = UnsafeMutablePointer<NoMeanStruct>.allocate(capacity: 1)
        //初始化
        ptr.initialize(to: f)
        //类型转换
        let ctx = ptr.withMemoryRebound(to: ClosureData<Box1<Int,Int>>.self, capacity: 1) {
            $0.pointee
        }
        
        print(ctx.ptr)
        print(ctx.object)
        print(ctx.object.pointee.value1, ctx.object.pointee.value2)
        
    }

闭包的数据结构:

//闭包的数据结构: 函数地址 + 捕获变量
struct ClosureData<T> {
    //函数地址
    var ptr: UnsafeRawPointer
    var object: UnsafeMutablePointer<T>
}

//捕获1个Int变量
struct Box<T> {
    var object: HeapObject
    var value: T
}

// 捕获2个Int变量
struct Box1<T1, T2> {
    var object: HeapObject
    var value1: T1
    var value2: T2
}

struct HeapObject{
    var metadate: UnsafeRawPointer
    //代表1个64位的refCount
    var refCount1: UInt32
    var refCount2: UInt32
}

输出和调试如下:

image.png 可以看到捕获的变量已经找到,还可以通过Mach-o文件来找对应的函数地址,来证实我们的猜想。 我们需要借助nm -p命令。

$ nm -p <mach-o path> | gerp <函数地址(不带0x)>

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

image.png 已经找到对应字符串了,继续使用swift-demangle命令来还原字符串。

$ xcrun swift-demangle <string>

image.png 可以看到通过字符串也可以对应到函数名称。以上还原过程如果想试,最好用一个mac的Single Line工程尝试,笔者用混编的ios工程没有找对应的函数地址,新的mac工程是可以的。