一、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
特点:
- 默认情况下,Block 存储在栈上。
- 栈上的 Block 在定义它的作用域内有效,当作用域结束时,Block 会被销毁。
- 栈上的 Block 不能跨作用域使用,一旦超出作用域范围,Block 就会失效。
内存管理:
- 栈上的 Block 不需要手动管理内存,由系统自动管理。
- 当 Block 被复制到堆上时,系统会自动将其从栈上复制到堆上,以延长 Block 的生命周期。
注意事项:
- 将栈上的 Block 传递给异步执行的方法时,需要注意避免在 Block 执行过程中访问已经释放的栈上变量,可以通过将 Block 复制到堆上来避免此问题。
4.2 堆上的 Block
特点:
- 通过调用
copy
方法,可以将栈上的 Block 复制到堆上。 - 堆上的 Block 的生命周期可以延长,不会受到作用域的限制。
- 堆上的 Block 可以在不同的作用域中传递和使用,不会因为作用域结束而失效。
- 通过调用
内存管理:
- 堆上的 Block 需要手动管理内存,需要在不再需要使用 Block 时手动释放。
- 在 ARC 环境下,系统会自动管理 Block 的内存,无需手动调用
copy
和release
。
注意事项:
- 当 Block 捕获外部变量时,如果 Block 存储在堆上,需要注意避免循环引用问题,可以使用
__weak
修饰符来避免循环引用。
- 当 Block 捕获外部变量时,如果 Block 存储在堆上,需要注意避免循环引用问题,可以使用
总的来说,栈上的 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 中持有了 self
,ViewController
的实例无法被释放,从而造成内存泄漏。
为了避免循环引用,可以采取以下方法:
使用
__weak
修饰符来避免循环引用,例如在 Block 中捕获self
时使用__weak
修饰符:__weak typeof(self) weakSelf = self;self.myBlock = ^{// 使用 weakSelf 避免循环引用NSLog(@"Block is executed");[weakSelf doSomething];};
使用
__block
修饰符来避免循环引用,可以让 Block 持有一个弱引用的指针,而不是强引用:__block typeof(self) blockSelf = self;self.myBlock = ^{// 使用 blockSelf 避免循环引用NSLog(@"Block is executed");[blockSelf doSomething];};
通过以上方法,可以有效地避免 Block 的循环引用问题,确保内存管理的正确性。在实际开发中,特别是在使用 Block 时,需要注意避免循环引用问题,以避免内存泄漏和其他潜在的问题。