iOS核心技能:Objective-C Block完全详解
从语法到原理,从内存到选型,一篇打通Block所有知识点
Block是Objective-C最核心的特性之一,也是每个iOS开发者必须掌握的知识点。它既是回调利器,也是内存陷阱的高发区。本文将系统讲解Block的一切,并在最后与Method进行全方位对比,帮助你彻底掌握Block的选型与使用。
一、什么是Block?
Block是Objective-C对C语言函数指针的扩展,它本质上是一个带有自动变量值的匿名函数对象。简单理解:Block = 函数体 + 捕获的上下文变量。
1// 一个最简单的Block 2^{ 3 NSLog(@"Hello Block"); 4}; 5
1.1 闭包的本质
Block是一种闭包(Closure)实现。闭包是指一个函数可以捕获并保存其所在作用域中的变量,即使该作用域已经销毁,这些变量依然可以被访问和修改。
1- (void (^)(void))createCounter { 2 __block int count = 0; 3 void (^counter)(void) = ^{ 4 count++; 5 NSLog(@"count = %d", count); 6 }; 7 return counter; 8} 9 10// 外部调用 11void (^counter)(void) = [self createCounter]; 12counter(); // count = 1 13counter(); // count = 2 14// count变量被Block持久化保存了 15
二、Block的语法详解
2.1 基本语法
1// 1. 最简单的Block 2^{ 3 NSLog(@"无参数无返回值"); 4}; 5 6// 2. 带参数的Block 7^(int a, int b) { 8 return a + b; 9}; 10 11// 3. 带返回值类型的完整写法 12^int (int a, int b) { 13 return a + b; 14}; 15
2.2 声明与赋值
1// 无参数无返回值 2void (^simpleBlock)(void) = ^{ 3 NSLog(@"简单Block"); 4}; 5 6// 带参数和返回值 7int (^addBlock)(int, int) = ^(int a, int b) { 8 return a + b; 9}; 10 11// 使用 12simpleBlock(); 13int result = addBlock(3, 5); // result = 8 14
2.3 使用typedef简化
1// 定义Block类型 2typedef void (^CompletionBlock)(BOOL success, NSError *error); 3typedef int (^CalculatorBlock)(int a, int b); 4 5// 使用类型 6CompletionBlock completion = ^(BOOL success, NSError *error) { 7 if (success) { 8 NSLog(@"成功"); 9 } else { 10 NSLog(@"失败: %@", error); 11 } 12}; 13 14CalculatorBlock add = ^(int a, int b) { 15 return a + b; 16}; 17 18CalculatorBlock multiply = ^(int a, int b) { 19 return a * b; 20}; 21
2.4 Block作为参数
1// 定义方法,接受Block作为参数 2- (void)fetchDataWithCompletion:(void (^)(NSData *data, NSError *error))completion { 3 // 异步操作 4 dispatch_async(dispatch_get_global_queue(0, 0), ^{ 5 NSData *data = [self loadData]; 6 dispatch_async(dispatch_get_main_queue(), ^{ 7 if (completion) { 8 completion(data, nil); 9 } 10 }); 11 }); 12} 13 14// 调用 15[self fetchDataWithCompletion:^(NSData *data, NSError *error) { 16 if (error) { 17 NSLog(@"加载失败"); 18 } else { 19 NSLog(@"加载成功,数据大小: %lu", data.length); 20 } 21}]; 22
方法结构拆解
1- (void)fetchDataWithCompletion:(void (^)(NSData *data, NSError *error))completion { 2 // 这里是方法的实现 3} 4
-:表示这是一个实例方法,只能通过类的实例来调用。(void):表示这个方法没有返回值。fetchDataWithCompletion::是方法名。通常用于发起一个异步操作(比如网络请求)。(void (^)(NSData *data, NSError *error))completion:这是方法唯一的参数,比较长,咱们拆开看:completion:是参数名。void (^)(NSData *data, NSError *error):是参数类型,它是一个Block(代码块)。
*void (^:表示这个 Block 没有返回值。
*NSData *data, NSError *error:表示这个 Block 接受两个参数(请求成功的数据和失败的错误信息)。
2.5 Block作为属性
1@interface MyClass : NSObject 2@property (nonatomic, copy) void (^dataLoadedBlock)(NSData *data); 3@property (nonatomic, copy) CompletionBlock completionHandler; 4@end 5 6// 使用 7MyClass *obj = [[MyClass alloc] init]; 8obj.dataLoadedBlock = ^(NSData *data) { 9 NSLog(@"数据已加载: %lu", data.length); 10}; 11
三、Block的底层原理
3.1 Block的数据结构
Block在底层是一个结构体对象,其简化结构如下:
1struct Block_layout { 2 void *isa; // 指向类对象(三种Block类型) 3 int flags; // 标志位 4 int reserved; // 保留字段 5 void (*invoke)(void *, ...); // 函数指针,指向Block执行代码 6 struct Block_descriptor *descriptor; // 描述信息(大小、copy/dispose辅助函数) 7 // 捕获的变量... 8}; 9
3.2 Block的三种类型
Block通过isa指针指向不同的类,分为三种类型:
| Block类型 | isa指向 | 存储位置 | 产生条件 |
|---|---|---|---|
| **NSGlobalBlock | _NSConcreteGlobalBlock | 全局数据区 | 不捕获外部变量,或捕获全局/静态变量 |
| **NSStackBlock | _NSConcreteStackBlock | 栈区 | 捕获局部变量,在栈上创建 |
| **NSMallocBlock | _NSConcreteMallocBlock | 堆区 | 从栈Block拷贝而来(ARC自动) |
1// 全局Block - 不捕获外部变量 2void (^globalBlock)(void) = ^{ 3 NSLog(@"Global Block"); 4}; 5 6// 栈Block - 捕获局部变量(ARC下可能被优化到堆上) 7int a = 10; 8void (^stackBlock)(void) = ^{ 9 NSLog(@"%d", a); 10}; 11 12// 堆Block - 显式拷贝 13void (^heapBlock)(void) = [stackBlock copy]; 14
3.3 Block捕获变量机制
1int a = 10; // 局部变量 2__block int b = 20; // __block修饰 3static int c = 30; // 静态变量 4NSString *str = @"Hello"; // 对象 5 6void (^block)(void) = ^{ 7 NSLog(@"a = %d", a); // 值捕获 → a被复制为const,不可修改 8 NSLog(@"b = %d", b); // 引用捕获 → b被包装为结构体指针 9 NSLog(@"c = %d", c); // 指针捕获 → 直接操作原变量 10 NSLog(@"str = %@", str); // 对象引用 → 强引用str 11}; 12
捕获规则总结:
| 变量类型 | 捕获方式 | 是否可修改 | 说明 |
|---|---|---|---|
| 局部变量(基本类型) | 值捕获 | ❌ | 复制为常量 |
| 局部变量(对象类型) | 强引用捕获 | ⚠️ | 可修改对象内容,不可重新赋值 |
| 静态变量 | 指针捕获 | ✅ | 直接操作原变量 |
| 全局变量 | 直接访问 | ✅ | 不捕获,直接访问 |
| __block变量 | 引用捕获(结构体指针) | ✅ | 通过结构体间接访问 |
3.4 拷贝操作详解
1// 栈Block → 堆Block(手动拷贝) 2void (^stackBlock)(void) = ^{ 3 NSLog(@"Stack Block"); 4}; 5void (^heapBlock)(void) = [stackBlock copy]; 6 7// ARC下自动拷贝的场景: 8// 1. 将Block赋值给__strong指针(大多数情况) 9// 2. Block作为返回值返回 10// 3. 将Block赋值给copy修饰的属性 11// 4. 将Block作为参数传递时(某些情况需要手动copy) 12 13// 为什么属性要用copy修饰? 14@property (nonatomic, copy) void (^myBlock)(void); 15// 确保Block被拷贝到堆上,防止栈Block提前销毁 16
四、Block的内存管理
4.1 循环引用问题
Block会强引用捕获的对象,这是循环引用的根本原因。
1// ❌ 经典循环引用 2@interface ViewController () 3@property (nonatomic, copy) void (^completionBlock)(void); 4@end 5 6@implementation ViewController 7- (void)setupBlock { 8 self.completionBlock = ^{ 9 // self → completionBlock → self(循环引用) 10 [self doSomething]; 11 }; 12} 13@end 14
4.2 打破循环引用的方法
方法一:weak-strong dance(最常用)
1__weak typeof(self) weakSelf = self; 2self.completionBlock = ^{ 3 __strong typeof(weakSelf) strongSelf = weakSelf; 4 if (strongSelf) { 5 [strongSelf doSomething]; 6 [strongSelf updateUI]; 7 } 8}; 9
方法二:使用@weakify/@strongify(ReactiveCocoa)
1@weakify(self); 2self.completionBlock = ^{ 3 @strongify(self); 4 [self doSomething]; 5}; 6
方法三:传入self作为参数
1self.completionBlock = ^(ViewController *vc) { 2 [vc doSomething]; 3}; 4// 调用时 5self.completionBlock(self); 6
方法四:使用__block并手动置nil
1__block ViewController *blockSelf = self; 2self.completionBlock = ^{ 3 [blockSelf doSomething]; 4 blockSelf = nil; // 必须置nil 5}; 6// 注意:这种方式需要保证Block一定会被执行 7
4.3 __block的内存管理
1// __block变量在Block中的内存管理 2__block int count = 0; // 在栈上 3void (^block)(void) = ^{ 4 count++; // Block拷贝时,__block变量也会被拷贝到堆上 5}; 6 7// ARC下: 8// __block变量被包装成一个结构体对象 9// 结构体中有__forwarding指针,保证栈和堆上的变量一致性 10
五、Block的常见使用场景
5.1 异步回调(最核心场景)
1// 网络请求回调 2- (void)downloadImageWithURL:(NSURL *)url 3 completion:(void (^)(UIImage *image, NSError *error))completion { 4 NSURLSessionDataTask *task = [NSURLSession.sharedSession dataTaskWithURL:url 5 completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 6 dispatch_async(dispatch_get_main_queue(), ^{ 7 if (error) { 8 completion(nil, error); 9 } else { 10 UIImage *image = [UIImage imageWithData:data]; 11 completion(image, nil); 12 } 13 }); 14 }]; 15 [task resume]; 16} 17 18// 动画回调 19[UIView animateWithDuration:0.3 animations:^{ 20 self.view.alpha = 0; 21} completion:^(BOOL finished) { 22 [self.view removeFromSuperview]; 23}]; 24
5.2 遍历操作
1// 数组遍历 2NSArray *array = @[@1, @2, @3, @4, @5]; 3[array enumerateObjectsUsingBlock:^(NSNumber *num, NSUInteger idx, BOOL *stop) { 4 if (num.integerValue > 3) { 5 *stop = YES; // 提前终止 6 } 7 NSLog(@"%@", num); 8}]; 9 10// 字典遍历 11NSDictionary *dict = @{@"key1": @"value1", @"key2": @"value2"}; 12[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 13 NSLog(@"%@ : %@", key, obj); 14}]; 15 16// 并发遍历(高性能) 17NSArray *largeArray = ...; 18[largeArray enumerateObjectsWithOptions:NSEnumerationConcurrent 19 usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 20 // 并发处理,适合大数据量 21}]; 22
5.3 排序与筛选
1// 排序 2NSArray *sorted = [array sortedArrayUsingComparator:^NSComparisonResult(NSString *s1, NSString *s2) { 3 return [s1 compare:s2]; 4}]; 5 6// 自定义排序 7NSArray *people = @[...]; 8NSArray *sortedPeople = [people sortedArrayUsingComparator:^NSComparisonResult(Person *p1, Person *p2) { 9 return [p1.age compare:p2.age]; 10}]; 11
5.4 链式调用(DSL)
1// 实现链式调用(参考Masonry) 2@interface Calculator : NSObject 3@property (nonatomic, assign) int result; 4- (Calculator *(^)(int))add; 5- (Calculator *(^)(int))subtract; 6- (Calculator *(^)(int))multiply; 7- (Calculator *(^)(int))divide; 8@end 9 10@implementation Calculator 11- (Calculator *(^)(int))add { 12 return ^Calculator *(int value) { 13 self.result += value; 14 return self; 15 }; 16} 17- (Calculator *(^)(int))subtract { 18 return ^Calculator *(int value) { 19 self.result -= value; 20 return self; 21 }; 22} 23// ... 其他方法 24@end 25 26// 使用:流畅的链式调用 27Calculator *calc = [[Calculator alloc] init]; 28int result = calc.add(5).subtract(3).multiply(4).result; // 8 29
5.5 GCD多线程
1// 异步执行 2dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 3 // 耗时操作 4 id result = [self heavyTask]; 5 dispatch_async(dispatch_get_main_queue(), ^{ 6 // 更新UI 7 [self updateUI:result]; 8 }); 9}); 10 11// 延迟执行 12dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), 13 dispatch_get_main_queue(), ^{ 14 NSLog(@"2秒后执行"); 15}); 16 17// 组队列 18dispatch_group_t group = dispatch_group_create(); 19dispatch_group_async(group, queue, ^{ /* 任务1 */ }); 20dispatch_group_async(group, queue, ^{ /* 任务2 */ }); 21dispatch_group_notify(group, mainQueue, ^{ /* 所有任务完成 */ }); 22
六、Block vs Method:全方位对比
6.1 核心区别一览
| 对比维度 | Block | Method |
|---|---|---|
| 本质 | 匿名函数对象(结构体) | 类的成员函数(SEL+IMP) |
| 有名称 | ❌ 匿名(可赋给变量) | ✅ 有方法名 |
| 所属关系 | 独立对象 | 属于类或实例 |
| 定义位置 | 任意位置(函数内、方法内、全局) | 必须在@interface/实现中 |
| 捕获局部变量 | ✅ 可以捕获 | ❌ 只能访问自身成员/全局变量 |
| 修改外部变量 | ⚠️ 需__block修饰 | ⚠️ 修改自身实例变量直接操作 |
| 作为参数传递 | ✅ 直接传递 | ❌ 需通过SEL+performSelector |
| 作为返回值 | ✅ 可以 | ❌ 不可以 |
| 存储类型 | 全局/栈/堆三种 | 随对象存储 |
| 内存管理 | 需要关注循环引用 | 较少关注(对象生命周期管理) |
| 调用方式 | block() 函数指针调用 | [obj method] 消息发送 |
| 调用效率 | 较高(直接调用) | 略低(消息转发开销) |
| 动态性 | 较低(编译时确定) | 较高(运行时决议) |
| 继承与重写 | ❌ 不支持 | ✅ 支持 |
6.2 变量访问能力对比
1// 方法中的变量访问 2- (void)methodExample { 3 int localVar = 10; // 局部变量 4 NSString *str = @"Hello"; // 局部变量 5 6 // ❌ 方法无法直接使用局部变量(除非作为参数) 7 // ✅ 可以访问实例变量 8 self.instanceVar = @"value"; 9 // ✅ 可以访问全局变量 10 globalVar = 100; 11 // ✅ 可以访问静态变量 12 staticVar = 200; 13} 14 15// Block中的变量访问 16- (void)blockExample { 17 int localVar = 10; // 局部变量 18 __block int mutableVar = 20; // 可修改的局部变量 19 NSString *str = @"Hello"; // 局部对象 20 21 void (^block)(void) = ^{ 22 // ✅ 可以捕获并访问局部变量 23 NSLog(@"%d", localVar); 24 // ✅ 可以修改__block变量 25 mutableVar++; 26 // ✅ 可以访问实例变量 27 self.instanceVar = @"value"; 28 // ✅ 可以修改捕获的对象内容 29 // str = @"World"; // ❌ 不可重新赋值,但可修改对象内容 30 }; 31} 32
6.3 内存管理对比
方法的内存管理:
1// 方法不产生独立的循环引用问题 2@interface MyClass : NSObject 3@property (nonatomic, strong) NSString *name; 4@end 5 6- (void)doSomething { 7 // ✅ 调用自身方法,不会产生循环引用 8 [self helperMethod]; 9 // ✅ 访问自身属性,不会循环引用 10 self.name = @"value"; 11} 12
Block的内存管理:
1// Block容易造成循环引用 2@interface MyClass : NSObject 3@property (nonatomic, copy) void (^block)(void); 4@end 5 6// ❌ 循环引用 7- (void)setupBlock { 8 self.block = ^{ 9 [self doSomething]; // self → block → self 10 }; 11} 12 13// ✅ 正确做法 14- (void)setupBlock { 15 __weak typeof(self) weakSelf = self; 16 self.block = ^{ 17 __strong typeof(weakSelf) strongSelf = weakSelf; 18 [strongSelf doSomething]; 19 }; 20} 21
6.4 性能对比
| 维度 | Block | Method |
|---|---|---|
| 调用方式 | 函数指针直接调用 | objc_msgSend消息发送 |
| 运行时开销 | 极小(内存间接寻址) | 较大(动态查找+转发) |
| 是否可内联 | 简单Block可内联优化 | 不支持内联 |
| 内存占用 | 额外结构体内存(捕获变量) | 无额外内存 |
| 适用场景 | 回调、遍历、函数式 | 对象行为、继承多态 |
6.5 使用场景决策树
1需要定义对象的核心行为/职责? 2 → 使用方法 3 ├── 需要被继承和重写? → 方法(优先) 4 ├── 需要作为公开API? → 方法 5 └── 需要访问私有成员? → 方法(天然支持) 6 7需要作为参数传递或返回值? 8 → 使用Block 9 ├── 异步回调、完成回调? → Block 10 ├── 遍历操作的处理逻辑? → Block 11 ├── 需要捕获上下文状态? → Block 12 ├── 一次性的临时逻辑? → Block 13 └── 链式调用/DSL构建? → Block 14
6.6 具体场景推荐
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 网络请求回调 | Block | 代码紧凑,上下文自然 |
| 按钮点击事件 | Method(Target-Action) | 传统方式,容易调试 |
| 数组遍历 | Block | API原生支持,简洁 |
| 对象核心方法 | Method | 符合OOP设计 |
| 多线程任务 | Block | GCD API原生使用Block |
| 动画完成回调 | Block | UIKit API标准 |
| 排序比较逻辑 | Block | 灵活,可内联定义 |
| 代理方法 | Method(Delegate) | 适合复杂交互 |
| 依赖注入 | Block | 轻量级解耦 |
| 算法封装 | Method | 便于测试和复用 |
七、常见陷阱与注意事项
7.1 在Block中使用self
1// ❌ 错误:可能导致循环引用 2self.block = ^{ 3 [self update]; 4}; 5 6// ✅ 正确:使用weakSelf 7__weak typeof(self) weakSelf = self; 8self.block = ^{ 9 [weakSelf update]; 10}; 11 12// ⚠️ 注意:如果Block中需要连续多次使用self 13__weak typeof(self) weakSelf = self; 14self.block = ^{ 15 // 建议在Block开始时转为strong,防止执行过程中对象被释放 16 __strong typeof(weakSelf) strongSelf = weakSelf; 17 if (strongSelf) { 18 [strongSelf step1]; 19 [strongSelf step2]; 20 [strongSelf step3]; 21 } 22}; 23
7.2 Block属性必须用copy
1// ❌ 错误:使用strong可能指向栈Block 2@property (nonatomic, strong) void (^block)(void); 3 4// ✅ 正确:使用copy确保在堆上 5@property (nonatomic, copy) void (^block)(void); 6
7.3 在Block中修改外部变量
1// ❌ 错误:无法修改 2int count = 0; 3void (^block)(void) = ^{ 4 count++; // 编译错误 5}; 6 7// ✅ 正确:使用__block 8__block int count = 0; 9void (^block)(void) = ^{ 10 count++; 11}; 12
7.4 通知中心移除问题
1// 注意:使用Block添加通知时,记得移除观察者 2__weak typeof(self) weakSelf = self; 3self.observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"Notification" 4 object:nil queue:nil usingBlock:^(NSNotification *note) { 5 [weakSelf handleNotification]; 6 }]; 7 8// 记得在dealloc中移除 9- (void)dealloc { 10 [[NSNotificationCenter defaultCenter] removeObserver:self.observer]; 11} 12
八、常见面试题
Q1: Block的底层原理是什么?
Block底层是一个结构体对象,包含isa指针、flags、invoke函数指针和descriptor。invoke指向Block的执行代码,通过直接调用函数指针执行,而不是消息发送。
Q2: Block为什么用copy修饰?
Block默认在栈上,栈Block在作用域结束后会被销毁。使用copy将Block拷贝到堆上,由ARC管理生命周期,确保在外部使用时依然有效。
Q3: __block和__weak的区别?
__block让Block可以修改外部变量,在ARC下将变量包装为结构体指针;__weak是弱引用,用于打破循环引用,但不能修改变量值。
Q4: Block的循环引用如何解决?
使用weak-strong dance:先用__weak弱引用self,Block内部再用__strong转为强引用。或者使用__block并手动置nil,或使用参数传递方式。
Q5: 为什么Block捕获的对象会被强引用?
Block在捕获对象时会调用_Block_object_assign函数,默认对对象进行强引用(retain),以确保Block执行时对象仍然存在。
Q6: Block和方法的区别是什么?
- Block是匿名函数对象,可捕获变量,可传递
- 方法是类的成员函数,不能捕获局部变量,不可传递
- Block调用是函数指针直接调用,方法是消息发送
- Block有循环引用风险,方法基本没有
九、最佳实践总结
9.1 编码规范
- 使用typedef定义Block类型,提高代码可读性
- Block属性使用copy修饰
- 调用Block前检查是否为nil
- Block内部使用weak-strong dance避免循环引用
- 合理使用__block修饰需要修改的变量
9.2 内存管理规范
| 场景 | 推荐做法 |
|---|---|
| Block作为属性 | @property (nonatomic, copy) |
| Block中访问self | __weak typeof(self) weakSelf = self |
| Block中修改外部变量 | 使用__block修饰 |
| 嵌套Block | 每层都需要处理循环引用 |
| 大对象捕获 | 考虑使用weak或手动管理 |
9.3 选型决策
1使用Method的场景: 2├── 对象的核心行为和职责 3├── 需要被继承和重写 4├── 公开API接口 5└── 复杂业务逻辑 6 7使用Block的场景: 8├── 异步回调/完成处理器 9├── 遍历操作的处理逻辑 10├── 排序/比较/筛选逻辑 11├── 延迟执行/定时任务 12├── 链式调用/DSL 13└── 一次性的临时逻辑 14
十、总结
Block是Objective-C中最具表现力的特性之一,它将闭包编程带入了iOS开发。理解Block不仅需要掌握语法,更要深入其内存管理和底层原理。
核心要点回顾:
- 本质:Block是匿名函数对象,可以捕获上下文变量
- 类型:全局Block、栈Block、堆Block,ARC下自动管理
- 内存:注意循环引用,使用weak-strong dance打破
- 应用:异步回调、遍历、排序、链式调用、GCD
- 对比方法:Block更灵活可传递,方法更符合OOP设计
Block和方法是互补关系,而非替代关系。优秀的代码往往是"方法定义结构,Block实现流程"的有机结合。
《Objective-C 之 Block 详解》 是转载文章,点击查看原文。