14. 闭包-closure

139 阅读12分钟

前言

闭包(closure)其实是一个捕获了上下文的常量或者是变量的匿名函数。Swift中的闭包有逃逸闭包,尾随闭包等,它既可以当做变量,也可以当做参数传递。需要注意的是Swift中的闭包与OC中的Block是有差异的。

一、值捕获

OC-Block

首先回顾一下OCBlock的值捕获:

#import <Foundation/Foundation.h>

int main() {
    
    NSInteger i = 1;
    void(^block)(void) = ^{
        NSLog(@"block %ld", i);
    };
    i += 1;
    
    NSLog(@"before block %ld", i);
    block();
    NSLog(@"after block %ld", i);

    return  0;;
}

/* 执行结果
 2022-02-11 13:17:28.951306+0800 TestDemo[8952:298860] before block 2
 2022-02-11 13:17:28.951575+0800 TestDemo[8952:298860] block 1
 2022-02-11 13:17:28.951609+0800 TestDemo[8952:298860] after block 2
 Program ended with exit code: 0
 */

通过执行结果可以看出,外部变量i的修改并没有影响Block内部捕获的值,如果需要外部变量的修改能影响到Block内部捕获的值则需要在变量前添加__block修饰。

#import <Foundation/Foundation.h>

int main() {
    
    __block NSInteger i = 1;
    void(^block)(void) = ^{
        NSLog(@"block %ld", i);
    };
    i += 1;
    
    NSLog(@"before block %ld", i);
    block();
    NSLog(@"after block %ld", i);

    return  0;;
}

/* 执行结果
 2022-02-11 13:27:04.707466+0800 TestDemo[9113:306082] before block 2
 2022-02-11 13:27:04.707748+0800 TestDemo[9113:306082] block 2
 2022-02-11 13:27:04.707796+0800 TestDemo[9113:306082] after block 2
 Program ended with exit code: 0
 */

另外,OCBlock有全局、栈、堆之分。

1. 全局Block(GlobalBlock)

当一个Block未使用外部变量,或者使用全局变量或静态变量时,它就是全局Block

- (void)testGlobal {
    NSLog(@"未使用外部变量 => %@", [^(void){
    } class]);
    
    NSLog(@"使用全局变量 => %@", [^(void){
        NSInteger num = globalNum;
    } class]);
    
    static NSInteger temp = 1;
    NSLog(@"使用静态变量 => %@", [^(void){
        NSInteger num = temp;
    } class]);
    
    self.copyBlock = ^{
    };
    NSLog(@"copy全局block => %@", [self.copyBlock class]);
    
    void(^strongBlock)(void) = ^{
    };
    NSLog(@"全局block赋值给强引用 => %@", [strongBlock class]);
    
    void(^__weak weakBlock)(void) = ^{
    };
    NSLog(@"全局block赋值给弱引用 => %@", [weakBlock class]);
}

/* 执行结果
 2022-02-12 10:38:22.178538+0800 TestDemo[19177:589737] 未使用外部变量 => __NSGlobalBlock__
 2022-02-12 10:38:22.178826+0800 TestDemo[19177:589737] 使用全局变量 => __NSGlobalBlock__
 2022-02-12 10:38:22.178881+0800 TestDemo[19177:589737] 使用静态变量 => __NSGlobalBlock__
 2022-02-12 10:38:22.178917+0800 TestDemo[19177:589737] copy全局block => __NSGlobalBlock__
 2022-02-12 10:38:22.178942+0800 TestDemo[19177:589737] 全局block赋值给强引用 => __NSGlobalBlock__
 2022-02-12 10:38:22.178971+0800 TestDemo[19177:589737] 全局block赋值给弱引用 => __NSGlobalBlock__
 Program ended with exit code: 0
 */

这里我们执行案例代码可以得出以下结果:

Block使用方式结果
未使用外部变量全局block
使用全局变量全局block
使用静态变量全局block
copy全局block全局block
全局block赋值给强引用全局block
全局block赋值给弱引用全局block
2. 栈Block(StackBlock)

当一个Block使用局部变量,或者使用OC属性时,它就是栈Block

- (void)testStack {
    NSInteger number = 1;
    NSLog(@"使用局部变量 => %@", [^(void){
        NSInteger num = number;
    } class]); // 局部变量
    
    NSLog(@"使用值类型属性 => %@", [^(void){
        NSInteger num = self.num;
    } class]); // 属性
    
    NSLog(@"使用对象类型属性 => %@", [^(void){
        id num = self.obj;
    } class]);
    
    void(^__weak weakBlock)(void) = ^{
        NSInteger num = self.num;
    };
    NSLog(@"栈block赋值给弱引用 => %@", [weakBlock class]);
}

/* 执行结果
 2022-02-12 10:43:06.048296+0800 TestDemo[19249:592789] 使用局部变量 => __NSStackBlock__
 2022-02-12 10:43:06.048609+0800 TestDemo[19249:592789] 使用值类型属性 => __NSStackBlock__
 2022-02-12 10:43:06.048693+0800 TestDemo[19249:592789] 使用对象类型属性 => __NSStackBlock__
 2022-02-12 10:43:06.048772+0800 TestDemo[19249:592789] 栈block赋值给弱引用 => __NSStackBlock__
 Program ended with exit code: 0
 */

这里我们执行案例代码可以得出以下结果:

Block使用方式结果
使用局部变量栈block
使用值类型属性栈block
使用对象类型属性栈block
栈block赋值给弱引用栈block
3. 堆Block(MallocBlock)

当一个栈Blockcopy或赋值给强引用时,结果就是一个堆Block

- (void)testMalloc {
        /// 循环引用
    self.copyBlock = ^{
        NSInteger num = self.num;
    };
    NSLog(@"copy栈block => %@", [self.copyBlock class]);
    
    void(^strongBlock)(void) = ^{
        NSInteger num = self.num;
    };
    NSLog(@"栈block赋值给强引用 => %@", [strongBlock class]);
    
    void(^__weak weakBlock)(void) = strongBlock;
    NSLog(@"堆block赋值给弱引用 => %@", [weakBlock class]);
}

/* 执行结果
 2022-02-12 10:44:37.516593+0800 TestDemo[19294:594462] copy栈block => __NSMallocBlock__
 2022-02-12 10:44:37.517097+0800 TestDemo[19294:594462] 栈block赋值给强引用 => __NSMallocBlock__
 2022-02-12 10:44:37.517157+0800 TestDemo[19294:594462] 堆block赋值给弱引用 => __NSMallocBlock__
 Program ended with exit code: 0
 */

这里我们执行案例代码可以得出以下结果:

Block使用方式结果
copy栈block堆block
栈block赋值给强引用堆block
堆block赋值给弱引用堆block

OC中需要注意堆Block使用不当导致的循环引用。

4. 完整代码
#import <Foundation/Foundation.h>

NSInteger globalNum = 1;

@interface TestBlock : NSObject
@property (nonatomic, assign) NSInteger num;
@property (nonatomic, strong) id obj;
@property (nonatomic, copy) void (^copyBlock)(void);
@end

@implementation TestBlock

- (void)testGlobal {
    NSLog(@"未使用外部变量 => %@", [^(void){
    } class]);
    
    NSLog(@"使用全局变量 => %@", [^(void){
        NSInteger num = globalNum;
    } class]);
    
    static NSInteger temp = 1;
    NSLog(@"使用静态变量 => %@", [^(void){
        NSInteger num = temp;
    } class]);
    
    self.copyBlock = ^{
    };
    NSLog(@"copy全局block => %@", [self.copyBlock class]);
    
    void(^strongBlock)(void) = ^{
    };
    NSLog(@"全局block赋值给强引用 => %@", [strongBlock class]);
    
    void(^__weak weakBlock)(void) = ^{
    };
    NSLog(@"全局block赋值给弱引用 => %@", [weakBlock class]);
}

- (void)testStack {
    NSInteger number = 1;
    NSLog(@"使用局部变量 => %@", [^(void){
        NSInteger num = number;
    } class]); // 局部变量
    
    NSLog(@"使用值类型属性 => %@", [^(void){
        NSInteger num = self.num;
    } class]); // 属性
    
    NSLog(@"使用对象类型属性 => %@", [^(void){
        id num = self.obj;
    } class]);
    
    void(^__weak weakBlock)(void) = ^{
        NSInteger num = self.num;
    };
    NSLog(@"栈block赋值给弱引用 => %@", [weakBlock class]);
}

- (void)testMalloc {
        /// 循环引用
    self.copyBlock = ^{
        NSInteger num = self.num;
    };
    NSLog(@"copy栈block => %@", [self.copyBlock class]);
    
    void(^strongBlock)(void) = ^{
        NSInteger num = self.num;
    };
    NSLog(@"栈block赋值给强引用 => %@", [strongBlock class]);
    
    void(^__weak weakBlock)(void) = strongBlock;
    NSLog(@"堆block赋值给弱引用 => %@", [weakBlock class]);
}
@end

int main() {
    TestBlock *obj = [TestBlock new];
    [obj testGlobal];
    [obj testStack];
    [obj testMalloc];
    return 0;
}

Swift-Closure

同样,我们先看看Swift中闭包的值捕获:

import Foundation

var num = 1
let closure = {
    print("closure", num)
}
num += 1

print("before closure", num)
closure()
print("after closure", num)

/* 执行结果
 before closure 2
 closure 2
 after closure 2
 Program ended with exit code: 0
 */

通过执行结果可以发现,闭包在捕获变量num时并没有直接捕获num的值1,所以后面num += 1影响了闭包表达式中的结果。我们对代码稍作修改:

import Foundation

var num = 1
let closure = { [num] in
    print("closure", num)
}

num += 1

print("before closure", num)
closure()
print("after closure", num)

/* 执行结果
 before closure 2
 closure 1
 after closure 2
 Program ended with exit code: 0
 */

通过执行结果可以发现,当我们使用[num]时,后面num += 1并不会影响了闭包表达式中的结果。在闭包中使用[num]的方式称作捕获列表,它可以捕获外部变量的值。Swift中闭包避免循环引用的方式就是使用捕获列表:

import Foundation

class TestClosure {
    let num = 1
    var closure: (() -> Void)?

    deinit {
        print("TestClosure deinit")
    }
}

do {
    let obj = TestClosure()
    obj.closure = { [weak obj] in
        print(obj?.num)
    }
}

/* 执行结果
 TestClosure deinit
 Program ended with exit code: 0
 */

这里obj.closure捕获的是weak obj,所以do执行后obj才能deinit。此外,Swift的闭包并没有OC-Block的全局、栈、堆之分,那么它是如何捕获值呢?这里我们可以分析一下。

1. 捕获全局变量

这里我们直接添加代码:

import Foundation

var num = 10
let closure = {
    num += 1
}
closure()

这里我们通过IR分析,命令如下:

swiftc -emit-ir -Onone -target x86_64-apple-ios14.2-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ${SRCROOT}/TestDemo/main.swift > ./main.ir && open main.ir

执行该命令后截取片段:

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  /// 将全局变量10存储到Int类型的结构体中
  store i64 10, i64* getelementptr inbounds (%TSi, %TSi* @"$s4main3numSivp", i32 0, i32 0), align 8
  /// 这里是把 closure 的地址转成了 i8* 并存储。 可以看出 closure 其实就是 swift.function(%swift.function = type { i8*, %swift.refcounted* }),swift.function的结构是 i8* 和 refcounted* 两个指针。
  store i8* bitcast (void ()* @"$s4mainyycfU_" to i8*), i8** getelementptr inbounds (%swift.function, %swift.function* @"$s4main7closureyycvp", i32 0, i32 0), align 8
  /// 此时 swift.function 中 refcounted* 存储 null 
  store %swift.refcounted* null, %swift.refcounted** getelementptr inbounds (%swift.function, %swift.function* @"$s4main7closureyycvp", i32 0, i32 1), align 8
  /// 加载function地址 
  %3 = load i8*, i8** getelementptr inbounds (%swift.function, %swift.function* @"$s4main7closureyycvp", i32 0, i32 0), align 8
  /// 加载捕获变量refcounted*,其实是null
  %4 = load %swift.refcounted*, %swift.refcounted** getelementptr inbounds (%swift.function, %swift.function* @"$s4main7closureyycvp", i32 0, i32 1), align 8
  %5 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %4) #1
  %6 = bitcast i8* %3 to void (%swift.refcounted*)*
  /// 调用function并传参
  call swiftcc void %6(%swift.refcounted* swiftself %4)
  call void @swift_release(%swift.refcounted* %4) #1
  ret i32 0
}

/// closure
define internal swiftcc void @"$s4mainyycfU_"() #0 {
entry:
  %access-scratch = alloca [24 x i8], align 8
  %0 = bitcast [24 x i8]* %access-scratch to i8*
  call void @llvm.lifetime.start.p0i8(i64 -1, i8* %0)
  call void @swift_beginAccess(i8* bitcast (%TSi* @"$s4main3numSivp" to i8*), [24 x i8]* %access-scratch, i64 33, i8* null) #1
  /// 直接访问全局变量10
  %1 = load i64, i64* getelementptr inbounds (%TSi, %TSi* @"$s4main3numSivp", i32 0, i32 0), align 8
  /// 10 + 1
  %2 = call { i64, i1 } @llvm.sadd.with.overflow.i64(i64 %1, i64 1)
  %3 = extractvalue { i64, i1 } %2, 0
  %4 = extractvalue { i64, i1 } %2, 1
  %5 = call i1 @llvm.expect.i1(i1 %4, i1 false)
  br i1 %5, label %8, label %6

通过IR可以分析出闭包并不会捕获全局变量,而是在执行闭包是直接访问全局变量的值

2. 捕获局部变量

这里我们直接添加代码:

import Foundation

do {
    var num = 10
    let closure = {
        num += 1
    }
    closure()
}

截取IR片段分析:

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %num.debug = alloca %TSi*, align 8
  %2 = bitcast %TSi** %num.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %2, i8 0, i64 8, i1 false)
  %closure.debug = alloca %swift.function, align 8
  %3 = bitcast %swift.function* %closure.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %3, i8 0, i64 16, i1 false)
  %4 = bitcast i8** %1 to i8*
  /// swift.refcounted* 是在堆区申请内存
  %5 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #1
  %6 = bitcast %swift.refcounted* %5 to <{ %swift.refcounted, [8 x i8] }>*
  %7 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %6, i32 0, i32 1
  %8 = bitcast [8 x i8]* %7 to %TSi*
  store %TSi* %8, %TSi** %num.debug, align 8
  %._value = getelementptr inbounds %TSi, %TSi* %8, i32 0, i32 0
  ///10存储到%._value,%._value其实就是堆区空间(%._value < %TSi* < [8 x i8]* < %swift.refcounted*)。
  store i64 10, i64* %._value, align 8
  %9 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %5) #1
  %10 = bitcast %swift.function* %closure.debug to i8*
  call void @llvm.lifetime.start.p0i8(i64 16, i8* %10)
  %closure.debug.fn = getelementptr inbounds %swift.function, %swift.function* %closure.debug, i32 0, i32 0
  store i8* bitcast (void (%swift.refcounted*)* @"$s4mainyycfU_TA" to i8*), i8** %closure.debug.fn, align 8
  %closure.debug.data = getelementptr inbounds %swift.function, %swift.function* %closure.debug, i32 0, i32 1
  store %swift.refcounted* %5, %swift.refcounted** %closure.debug.data, align 8
  %11 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %5) #1
  /// 调用closure并传参,参数其实就是堆空间中存的10.
  call swiftcc void @"$s4mainyycfU_TA"(%swift.refcounted* swiftself %5)
  call void @swift_release(%swift.refcounted* %5) #1
  call void @swift_release(%swift.refcounted* %5) #1
  call void @swift_release(%swift.refcounted* %5) #1
  ret i32 0
}

通过IR可以分析出闭包捕获局部变量时会将局部变量转移到堆区,在执行闭包时是从堆区取值

3. 捕获引用类型

这里我们直接添加代码:

import Foundation

do {
    var num = 10
    let closure = {
        num += 1
    }
    closure()
}

截取IR片段分析:

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %p.debug = alloca %T4main6PersonC*, align 8
  %2 = bitcast %T4main6PersonC** %p.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %2, i8 0, i64 8, i1 false)
  %closure.debug = alloca %swift.function, align 8
  %3 = bitcast %swift.function* %closure.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %3, i8 0, i64 16, i1 false)
  %4 = bitcast i8** %1 to i8*
  %5 = call swiftcc %swift.metadata_response @"$s4main6PersonCMa"(i64 0) #6
  %6 = extractvalue %swift.metadata_response %5, 0
  %7 = call swiftcc %T4main6PersonC* @"$s4main6PersonCACycfC"(%swift.type* swiftself %6)
  store %T4main6PersonC* %7, %T4main6PersonC** %p.debug, align 8
  %8 = bitcast %T4main6PersonC* %7 to %swift.refcounted*
  %9 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %8) #4
  /// %10存储了Person的实例地址
  %10 = bitcast %T4main6PersonC* %7 to %swift.refcounted*
  %11 = bitcast %swift.function* %closure.debug to i8*
  call void @llvm.lifetime.start.p0i8(i64 16, i8* %11)
  %closure.debug.fn = getelementptr inbounds %swift.function, %swift.function* %closure.debug, i32 0, i32 0
  store i8* bitcast (void (%swift.refcounted*)* @"$s4mainyycfU_TA" to i8*), i8** %closure.debug.fn, align 8
  %closure.debug.data = getelementptr inbounds %swift.function, %swift.function* %closure.debug, i32 0, i32 1
  /// 将%10存到闭包的捕获数据中
  store %swift.refcounted* %10, %swift.refcounted** %closure.debug.data, align 8
  /// retain操作(强引用%10)
  %12 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %10) #4
  /// 调用闭包并传入%10
  call swiftcc void @"$s4mainyycfU_TA"(%swift.refcounted* swiftself %10)
  call void @swift_release(%swift.refcounted* %10) #4
  call void @swift_release(%swift.refcounted* %10) #4
  call void bitcast (void (%swift.refcounted*)* @swift_release to void (%T4main6PersonC*)*)(%T4main6PersonC* %7) #4
  ret i32 0
}

通过IR可以分析出闭包捕获引用类型时会存储一次引用类型的地址(强引用),因为引用类型本身已经在堆区,所以执行闭包时从堆区取值即可

4. 捕获多个值

这里我们直接添加代码:

import Foundation

class Person {
    var age = 10
}

do {
    var num = 10
    let p = Person()
    let closure = {
        p.age += 1
        num += 1
    }

    closure()
}

截取IR片段分析:

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %num.debug = alloca %TSi*, align 8
  %2 = bitcast %TSi** %num.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %2, i8 0, i64 8, i1 false)
  %p.debug = alloca %T4main6PersonC*, align 8
  %3 = bitcast %T4main6PersonC** %p.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %3, i8 0, i64 8, i1 false)
  %closure.debug = alloca %swift.function, align 8
  %4 = bitcast %swift.function* %closure.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %4, i8 0, i64 16, i1 false)
  %5 = bitcast i8** %1 to i8*
  /// %6申请堆区内存
  %6 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #1
  %7 = bitcast %swift.refcounted* %6 to <{ %swift.refcounted, [8 x i8] }>*
  %8 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %7, i32 0, i32 1
  %9 = bitcast [8 x i8]* %8 to %TSi*
  store %TSi* %9, %TSi** %num.debug, align 8
  %._value = getelementptr inbounds %TSi, %TSi* %9, i32 0, i32 0
  /// 10存到堆区
  store i64 10, i64* %._value, align 8
  %10 = call swiftcc %swift.metadata_response @"$s4main6PersonCMa"(i64 0) #6
  %11 = extractvalue %swift.metadata_response %10, 0
  %12 = call swiftcc %T4main6PersonC* @"$s4main6PersonCACycfC"(%swift.type* swiftself %11)
  
  store %T4main6PersonC* %12, %T4main6PersonC** %p.debug, align 8
  %13 = bitcast %T4main6PersonC* %12 to %swift.refcounted*
  %14 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %13) #1
  %15 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %6) #1

  /// %16申请堆区内存
  %16 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata.3, i32 0, i32 2), i64 32, i64 7) #1
  /// 堆区内存转成结构体 <{ %swift.refcounted, %T4main6PersonC*, %swift.refcounted* }>*
  %17 = bitcast %swift.refcounted* %16 to <{ %swift.refcounted, %T4main6PersonC*, %swift.refcounted* }>*
  /// %18获取结构体中的%T4main6PersonC*
  %18 = getelementptr inbounds <{ %swift.refcounted, %T4main6PersonC*, %swift.refcounted* }>, <{ %swift.refcounted, %T4main6PersonC*, %swift.refcounted* }>* %17, i32 0, i32 1
  /// %12(Person实例)存到%18
  store %T4main6PersonC* %12, %T4main6PersonC** %18, align 8
  /// %19获取结构体中的%swift.refcounted*
  %19 = getelementptr inbounds <{ %swift.refcounted, %T4main6PersonC*, %swift.refcounted* }>, <{ %swift.refcounted, %T4main6PersonC*, %swift.refcounted* }>* %17, i32 0, i32 2
  /// %6(堆区中的10)存到%19
  store %swift.refcounted* %6, %swift.refcounted** %19, align 8
  %20 = bitcast %swift.function* %closure.debug to i8*
  call void @llvm.lifetime.start.p0i8(i64 16, i8* %20)
  %closure.debug.fn = getelementptr inbounds %swift.function, %swift.function* %closure.debug, i32 0, i32 0
  store i8* bitcast (void (%swift.refcounted*)* @"$s4mainyycfU_TA" to i8*), i8** %closure.debug.fn, align 8
  %closure.debug.data = getelementptr inbounds %swift.function, %swift.function* %closure.debug, i32 0, i32 1
  store %swift.refcounted* %16, %swift.refcounted** %closure.debug.data, align 8
  %21 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %16) #1
  /// 调用闭包并传入%16
  call swiftcc void @"$s4mainyycfU_TA"(%swift.refcounted* swiftself %16)
  call void @swift_release(%swift.refcounted* %16) #1
  call void @swift_release(%swift.refcounted* %16) #1
  call void bitcast (void (%swift.refcounted*)* @swift_release to void (%T4main6PersonC*)*)(%T4main6PersonC* %12) #1
  call void @swift_release(%swift.refcounted* %6) #1
  ret i32 0
}

通过IR可以分析出闭包捕获多个值时会在堆区申请一段连续的内存空间来存储(存储的是具体的值或指针),调用闭包时将这段空间的首地址传入即可。这里我们可以通过伪代码来还原闭包的结构并验证该结论:

import Foundation

class Person {
    var age = 10
}

func test() -> () -> Void {
    let p = Person()
    let num = 20
    let closure: () -> Void = {
        p.age += num
    }
    return closure
}

struct TwoParamsClosureData<T1, T2> {
    var object: HeapObject
    var value1: UnsafePointer<T1>
    var value2: T2
}

struct ClosureData<T> {
    var ptr: UnsafeRawPointer
    var capatureValue: UnsafePointer<T>
}

struct HeapObject {
    var metadata: UnsafeRawPointer
    var refcount1: Int32
    var refcount2: Int32
}

struct Box<T> {
    var object: HeapObject
    var value: T
}

struct NoMeanStruct {
    var f: () -> Void
}

var f = NoMeanStruct(f: test())

let ptr = UnsafeMutablePointer<NoMeanStruct>.allocate(capacity: 1)
ptr.initialize(to: f)

let ctx = ptr.withMemoryRebound(to: ClosureData<TwoParamsClosureData<Person, Int>>.self, capacity: 1) {
    $0.pointee
}

print(ctx.ptr)
print(ctx.capatureValue.pointee.value1)
print(ctx.capatureValue.pointee.value2)

print("end")

image-20220218154841416

通过调试可以看到打印的结果就是闭包的捕获值,这也符合之前的结论。

二、闭包的类型

1. 逃逸闭包(@escaping)

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

这里我们先看一下正常的闭包(非逃逸闭包)

import Foundation

func testNoEscaping(_ closure: () -> Void) {
    closure()
}

func test() {
    var num = 20
    testNoEscaping {
        num += 10
    }
}

test()

截取IR片段分析:

/// testNoEscaping函数
define internal swiftcc void @"$s4main4testyyFyyXEfU_TA"(%swift.refcounted* swiftself %0) #0 {
entry:
  %1 = bitcast %swift.refcounted* %0 to <{ %swift.refcounted, %TSi* }>*
  %2 = getelementptr inbounds <{ %swift.refcounted, %TSi* }>, <{ %swift.refcounted, %TSi* }>* %1, i32 0, i32 1
  %3 = load %TSi*, %TSi** %2, align 8
  /// nocapture可以看出该闭包是非逃逸的
  tail call swiftcc void @"$s4main4testyyFyyXEfU_"(%TSi* nocapture dereferenceable(8) %3)
  ret void
}

/// test函数
define hidden swiftcc void @"$s4main4testyyF"() #0 {
entry:
  %0 = alloca %TSi, align 8
  %1 = bitcast %TSi* %0 to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %1, i8 0, i64 8, i1 false)
  %2 = bitcast %TSi* %0 to i8*
  call void @llvm.lifetime.start.p0i8(i64 8, i8* %2)
  %._value = getelementptr inbounds %TSi, %TSi* %0, i32 0, i32 0
  /// 存值
  store i64 20, i64* %._value, align 8
  %3 = alloca i8, i64 24, align 16
  /// %4 < %swift.opaque* < %3
  %4 = bitcast i8* %3 to %swift.opaque*
  /// %5 = <{ %swift.refcounted, %TSi* }>* < %4
  %5 = bitcast %swift.opaque* %4 to <{ %swift.refcounted, %TSi* }>*
  /// %6 = %TSi*
  %6 = getelementptr inbounds <{ %swift.refcounted, %TSi* }>, <{ %swift.refcounted, %TSi* }>* %5, i32 0, i32 1
  /// %0(局部变量num的值20)存到%6
  store %TSi* %0, %TSi** %6, align 8
  /// 调用闭包传入%4(里面存的是20)
  call swiftcc void @"$s4main14testNoEscapingyyyyXEF"(i8* bitcast (void (%swift.refcounted*)* @"$s4main4testyyFyyXEfU_TA" to i8*), %swift.opaque* %4)
  %7 = bitcast %TSi* %0 to i8*
  call void @llvm.lifetime.end.p0i8(i64 8, i8* %7)
  ret void
}

这里可以看出**非逃逸闭包捕获值时并没有将该值转移到堆区,而是直接访问栈中的值。注意这里Swift做了编译优化,因为非逃逸闭包在编译时有明确的生命周期,testNoEscaping函数执行结束时,闭包也执行结束了,这种情况没有必要将捕获值转移到堆区。**由此我们得出非逃逸闭包的特点:

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

那如果是逃逸闭包呢?我们添加如下代码:

import Foundation

func testEscaping(_ closure: @escaping () -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        closure()
    }
}

func test() {
    var num = 20
    testEscaping {
        num += 10
    }
}

test()

截取IR片段分析:

define hidden swiftcc void @"$s4main4testyyF"() #0 {
entry:
  %num.debug = alloca %TSi*, align 8
  %0 = bitcast %TSi** %num.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %0, i8 0, i64 8, i1 false)
  /// 在堆区申请内存
  %1 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata.3, i32 0, i32 2), i64 24, i64 7) #4
  %2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*
  %3 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %2, i32 0, i32 1
  %4 = bitcast [8 x i8]* %3 to %TSi*
  store %TSi* %4, %TSi** %num.debug, align 8
  %._value = getelementptr inbounds %TSi, %TSi* %4, i32 0, i32 0
  /// 存值
  store i64 20, i64* %._value, align 8
  %5 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %1) #4
  /// 调用testEscaping函数
  call swiftcc void @"$s4main12testEscapingyyyycF"(i8* bitcast (void (%swift.refcounted*)* @"$s4main4testyyFyycfU_TA" to i8*), %swift.refcounted* %1)
  call void @swift_release(%swift.refcounted* %1) #4
  call void @swift_release(%swift.refcounted* %1) #4
  ret void
}

这里可以看出逃逸闭包的捕获值转移到了堆区。由此我们得出逃逸闭包的几个特点:

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

注意:可选类型闭包默认是逃逸闭包!!!

2. 自动闭包(@autoclosure)

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。当我们声明一个接受闭包作为形式参数的函数时,你可以在闭包前写@autoclosure 来标明该闭包是自动闭包。

那么自动闭包有什么应用场景呢?比如下面这样的:

import Foundation

func debugInfo(_ condition: Bool, message: String) {
    if condition {
        print(message)
    }
}

func doSomething() -> String {
    /// 耗时操作
    "An error occurred"
}

debugInfo(true, message: doSomething())

这段代码是用来输出日志,设计思路是当conditiontrue时打印函数doSomething的执行结果。代码执行没有任何异常,但存在一个问题,当conditionfalse时,函数doSomething也会执行。如果doSomething为耗时计算操作,那么此段代码的就会有较大的性能损耗。那么有没有其它方式避免这种情况?只有当conditiontruedoSomething才会执行,而且不影响代码的可读性。自动闭包是一个非常好的方式:

import Foundation

func debugInfo(_ condition: Bool, message: @autoclosure () -> String) {
    if condition {
        print(message())
    }
}

func doSomething() -> String {
    /// 耗时操作
    "An error occurred"
}

debugInfo(true, message: doSomething())

这里我们将形式参数message修改成自动闭包类型,然后print闭包的执行结果即可避免函数doSomething的非必要调用情况。由此我们可以得出自动闭包的几个特点:

  • 自动闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。
  • 函数调用时,可以省略闭包的花括号,用一个普通的表达式来代替显式的闭包,增加了代码的可读性。
  • 自动闭包让你能够延迟求值,直到你调用这个闭包,代码段才会被执行,优化了性能。

注意:自动闭包可以逃逸,同时使用 @autoclosure 和 @escaping 标记即可。

3. 尾随闭包

当你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数参数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签。

我们先看正常的闭包作为函数参数的使用方式:

import Foundation

func debugInfo(_ condition: Bool, message: () -> String) {
    if condition {
        print(message())
    }
}

debugInfo(true, message: { "An error occurred" })

这里可以看到函数debugInfo被调用时,闭包作为参数是被包含在小括号()里面的,而且要写参数标签message。如果使用尾随闭包,它是这样的:

import Foundation

func debugInfo(_ condition: Bool, message: () -> String) {
    if condition {
        print(message())
    }
}

debugInfo(true) {
    "An error occurred"
}

很明显此时闭包是在小括号()外的,并且参数标签message也被省略了,这种方式提高了代码的可读性。

三、Block与Closure相互调用

这里我们直接看代码:

  1. Block.hBlock.m
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void(^ResultBlock)(NSString *message);

@interface Block : NSObject
@property (nonatomic, copy) ResultBlock block;

+ (void)testClosure;
@end

NS_ASSUME_NONNULL_END

  
#import "Block.h"
#import "TestDemo-Swift.h"

@implementation Block

/// 调用Swift-Closure
+ (void)testClosure {
    Closure *c = [[Closure alloc] init];
    c.closure = ^(NSString * _Nonnull message) {
        NSLog(@"%@", message);
    };
    c.closure(@"OC调Swift-Closure: hello world");
}

@end
  1. mian.swift
import Foundation

class Closure: NSObject {
    @objc var closure: ((String) -> Void)?

    /// 调用OC-Block
    static func testBlock() {
        let b = Block()
        b.block = { msg in
            print(msg)
        }
        b.block("Swift调OC-Block: hello world")
    }
}

Closure.testBlock()
Block.testClosure()

/* 执行结果
 Swift调OC-Block: hello world
 2022-02-19 15:26:13.589289+0800 TestDemo[17458:4110501] OC调Swift-Closure: hello world
 Program ended with exit code: 0
 */

桥接文件配置无误后两者就可以互相调用了。其实SwiftOC比较简单,需要注意的是OCSwift。声明Swift类时,该类必须继承自NSObject,其次它的成员属性前需要加上@objc,这样OC才能正常调。

四、总结

闭包作为Swift中的常见类型在开发中使用频率非常高,我们不仅要掌握它的基本用法,更要了解它的本质。它与OCBlock有相似之处,也有更多的不同之处。此外,合理运用闭包,避免循环引用导致的内存泄漏。