一、Block概要

1. Block的定义

在 iOS 开发中,Block 是一种闭包(Closure)的概念,可以将一段代码块作为一个对象进行传递和存储,类似于函数指针。Block 可以捕获其定义时所在范围内的变量,并在需要的时候执行这段代码块。Block 的使用可以方便地实现回调、异步操作、事件处理等功能。

2. 为什么要引入Block

Block其本质也是对象,是一段代码块。iOS之所以引入Block,是因为使用Block可以精简代码,减少耦合。

二、Block的用法

2.1 作为变量使用
int (^multiply)(int, int) = ^(int a, int b) {return a * b;};int result = multiply(3, 5); // 调用 Block
2.2 作为方法参数
- (void)performOperationWithBlock:(void (^)(void))block {block();}[self performOperationWithBlock:^{NSLog(@"执行 Block");}];
2.3 作为属性或实例变量
@property (nonatomic, copy) void (^completionBlock)(void);self.completionBlock = ^{NSLog(@"完成操作");};self.completionBlock();

三、Block与外界变量

在 iOS 开发中,Block 可以捕获其定义时所在范围内的外部变量,这种特性称为变量捕获(Variable Capture)。当 Block 内部引用外部变量时,Block 会自动捕获这些变量的值或引用,以便在 Block 执行时使用。在 Block 中使用外部变量可以实现数据共享和传递,但需要注意一些细节和注意事项。

变量类型类型捕获到block内部访问方式
局部变量auto可以值传递
局部变量static可以指针传递
全局变量不可以直接访问
3.1 局部变量 auto

局部变量 auto (自动变量) ,我们平时写的局部变量,默认就有 auto (自动变量,离开作用域就销毁)。比如:

int age = 20; // 等价于 auto int age = 20;
3.2 局部变量 static

static 修饰的局部变量,不会被销毁。由下面代码可以看得出来,Block 外部修改static 修饰的局部变量,依然能影响 Block 内部的值:

static int height= 30;int age = 20;void (^block)(void) =^{ NSLog(@"age is %d height = %d",age,height);};age = 25;height = 35;block();// 执行结果age is 20 height = 35

这是为什么会改变呢?
因为 age 是直接值传递,height 传递的是 *height,也就是说直接把内存地址传进去进行修改了。大家可以根据上面讲解内容尝试编译成 cpp 文件,去了解详情。缺点是会永久存储,内存开销大。

3.3 全局变量

全局变量不能也无需捕获到 Block 内部,因为全局变量是存放在全局静态区的,直接访问就完事了。缺点也是内存开销大。

3.4 在Block内如何修改Block外部变量

在 Block 内部修改 Block 外部变量通常需要使用 __block 关键字来声明外部变量,以便在 Block 内部对外部变量进行修改。使用 __block 关键字可以使外部变量在 Block 内部变为可变,允许对其进行赋值操作。

下面是一个示例代码,演示如何在 Block 内部修改外部变量:

__block int externalVariable = 10;void (^block)(void) = ^{externalVariable = 20;NSLog(@"内部修改后的外部变量值:%d", externalVariable);};block(); // 输出:内部修改后的外部变量值:20NSLog(@"外部变量值:%d", externalVariable); // 输出:外部变量值:20

在上面的示例中,通过使用 __block 关键字声明外部变量 externalVariable,使得在 Block 内部可以修改外部变量的值。当调用 Block 后,外部变量的值被成功修改,并且在 Block 外部也可以访问到修改后的值。

需要注意的是,在使用 __block 关键字声明外部变量时,需要注意内存管理的问题,特别是在 ARC(Automatic Reference Counting)环墨下,避免出现循环引用或内存泄漏的情况。确保在适当的时候解除对 Block 的强引用,以避免出现循环引用问题。

总的来说,使用 __block 关键字可以在 Block 内部修改外部变量,实现对外部变量的可变性,但需要注意内存管理和避免循环引用等问题,以确保代码的正确性和稳定性。

四、Block的分类

根据存储位置分类:

  • 栈上的 Block:默认情况下,Block 存储在栈上,当 Block 离开作用域时会被销毁。
  • 堆上的 Block:通过调用 copy 方法,可以将栈上的 Block 复制到堆上,以延长 Block 的生命周期。

在 iOS 开发中,Block 可以存储在栈上或堆上,具有不同的生命周期和内存管理方式。下面详细介绍栈上的 Block 和堆上的 Block 的特点和区别:

4.1 栈上的 Block
  1. 特点

    • 默认情况下,Block 存储在栈上。
    • 栈上的 Block 在定义它的作用域内有效,当作用域结束时,Block 会被销毁。
    • 栈上的 Block 不能跨作用域使用,一旦超出作用域范围,Block 就会失效。
  2. 内存管理

    • 栈上的 Block 不需要手动管理内存,由系统自动管理。
    • 当 Block 被复制到堆上时,系统会自动将其从栈上复制到堆上,以延长 Block 的生命周期。
  3. 注意事项

    • 将栈上的 Block 传递给异步执行的方法时,需要注意避免在 Block 执行过程中访问已经释放的栈上变量,可以通过将 Block 复制到堆上来避免此问题。
4.2 堆上的 Block
  1. 特点

    • 通过调用 copy 方法,可以将栈上的 Block 复制到堆上。
    • 堆上的 Block 的生命周期可以延长,不会受到作用域的限制。
    • 堆上的 Block 可以在不同的作用域中传递和使用,不会因为作用域结束而失效。
  2. 内存管理

    • 堆上的 Block 需要手动管理内存,需要在不再需要使用 Block 时手动释放。
    • 在 ARC 环境下,系统会自动管理 Block 的内存,无需手动调用 copyrelease
  3. 注意事项

    • 当 Block 捕获外部变量时,如果 Block 存储在堆上,需要注意避免循环引用问题,可以使用 __weak 修饰符来避免循环引用。

总的来说,栈上的 Block 在作用域内有效,适合用于临时的、局部的逻辑处理;而堆上的 Block 可以延长生命周期,适合在不同作用域中传递和复用。开发者在使用 Block 时,需要根据实际情况选择合适的存储位置,以确保内存管理和逻辑执行的正确性。

4.3 区分栈上的 Block与堆上的 Block

在 iOS 开发中,可以通过一些方法来区分 Block 是存储在栈上还是堆上的。下面列举了几种方法来区分栈上的 Block 和堆上的 Block:

4.3.1. 使用 NSLog 打印 Block 的地址
  • 栈上的 Block:栈上的 Block 在作用域结束后会被销毁,所以可以通过在 Block 定义后立即打印 Block 的地址来查看 Block 的地址是否相同。
  • 堆上的 Block:堆上的 Block 在复制到堆上后会有不同的地址,可以通过在 Block 复制到堆上后打印 Block 的地址来查看是否有变化。
4.3.2. 使用 __block 变量
  • 栈上的 Block:栈上的 Block 捕获 __block 变量时,变量的值会被 Block 捕获,但是在作用域结束后,__block 变量的值会被更新为 Block 执行时的值。
  • 堆上的 Block:堆上的 Block 捕获 __block 变量时,变量的值会被复制到堆上的 Block 中,作用域结束后,__block 变量的值不会被更新。
4.3.3. 使用 Block_copy 函数
  • 栈上的 Block:对栈上的 Block 使用 Block_copy 函数会将其复制到堆上,返回一个新的堆上 Block。
  • 堆上的 Block:对堆上的 Block 使用 Block_copy 函数仍会返回一个新的堆上 Block,但是地址不同于原始的堆上 Block。
4.4.4. 使用 __weak 修饰符
  • 栈上的 Block:栈上的 Block 捕获 __weak 变量时,变量会被自动置为 nil,因为栈上的 Block不会强引用 __weak 变量。
  • 堆上的 Block:堆上的 Block 捕获 __weak 变量时,变量会正常工作,因为堆上的 Block会强引用 __weak 变量。

通过以上方法,可以辨别出 Block 是存储在栈上还是堆上的。在实际开发中,了解 Block 的存储位置有助于正确管理内存和避免潜在的问题。

五、Block的循环引用

循环引用是指两个或多个对象之间互相持有对方的强引用,导致它们无法被释放,从而造成内存泄漏。在使用 Block 时,循环引用是一个常见的问题,特别是在 Block 中捕获了外部对象并且外部对象又持有了 Block。

下面是一个典型的循环引用场景:

@interface ViewController : UIViewController@property (nonatomic, copy) void (^myBlock)(void);@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];self.myBlock = ^{// Block 中捕获了 self,导致循环引用NSLog(@"Block is executed");[self doSomething];};}- (void)doSomething {// Block 中捕获了 self,导致循环引用NSLog(@"Doing something");}@end

在上面的代码中,ViewController 的实例持有一个 Block,并且在 Block 中捕获了 self,导致了循环引用。当 ViewController 的实例被释放时,由于 Block 中持有了 selfViewController 的实例无法被释放,从而造成内存泄漏。

为了避免循环引用,可以采取以下方法:

  1. 使用 __weak 修饰符来避免循环引用,例如在 Block 中捕获 self 时使用 __weak 修饰符:

    __weak typeof(self) weakSelf = self;self.myBlock = ^{// 使用 weakSelf 避免循环引用NSLog(@"Block is executed");[weakSelf doSomething];};
  2. 使用 __block 修饰符来避免循环引用,可以让 Block 持有一个弱引用的指针,而不是强引用:

    __block typeof(self) blockSelf = self;self.myBlock = ^{// 使用 blockSelf 避免循环引用NSLog(@"Block is executed");[blockSelf doSomething];};

通过以上方法,可以有效地避免 Block 的循环引用问题,确保内存管理的正确性。在实际开发中,特别是在使用 Block 时,需要注意避免循环引用问题,以避免内存泄漏和其他潜在的问题。