Objective-C Block 底层原理深度解析
Objective-C Block 底层原理深度解析
1. Block 是什么?
1.1 Block 的本质
- Block 是 Objective-C 中的特殊对象,实现了匿名函数的功能
- 通过 isa 指针继承自 NSObject,可以响应(如 copy、retain、release)等内存管理方法(ARC 下自动处理)。
- 遵循 Objective-C 的内存管理规则
- 可以临时保存代码并在需要时执行
- 可以访问定义时的上下文变量
- 可以像普通对象一样传递和使用
1.2 Block 的基本写法
// 最简单的 Block
void (^sayHello)(void) = ^{NSLog(@"Hello");
};// 带参数的 Block
void (^saySomething)(NSString *) = ^(NSString *words) {NSLog(@"%@", words);
};// 带返回值的 Block
int (^add)(int, int) = ^(int a, int b) {return a + b;
};
1.3 Block 的底层结构
// Block 的本结构
struct Block_layout {void *isa; // 指向类对象,证明 Block 是对象int flags; // 状态标志,包含 Block 的类型和引用计数信息int reserved; // 保留字段void (*invoke)(void *, ...); // 函数指针,指向 Block 的实现代码struct Block_descriptor *descriptor; // 描述信息,包含 Block 的大小、复制和释放函数等// 捕获的变量
};
结构解析:
isa
指针:表明 Block 是一个对象,指向其类型信息(_NSConcreteGlobalBlock
、_NSConcreteStackBlock
或_NSConcreteMallocBlock
)Flags
:记录 Block 的状态信息,如是否被复制到堆上、是否包含 C++ 对象等reserved
:保留字段,用于未来扩展invoke
:指向 Block 要执行的代码descriptor
:包含 Block 的描述信息,如大小、复制和释放函数等- 这个结构体是 Block 作为对象和函数双重身份的基础
补充说明:
- Block 是特殊的 Objective-C 对象,可以响应
copy
、release
等消息 - 内存管理遵循 Objective-C 的内存管理规则
- 在编译时会根据使用场景生成不同的类型(全局、栈、堆)
- 结构体中的
isa
指针决定了 Block 的类型和行为
1.4 Block 的类型编码
// 基本格式:@?<返回值类型参数1类型参数2类型...>
// 示例1:无参数无返回值的 Block
"@?<v>" // void (^)(void)// 示例2:带基本类型参数的 Block
"@?<vi>" // void (^)(int)
// v 表示返回值类型为 void
// i 表示参数类型为 int (注意:基本类型不需要@前缀)// 示例3:带对象参数的 Block
"@?<v@>" // void (^)(id)
// v 表示返回值类型为 void
// @ 表示参数类型为 id(对象类型)// 示例4:带 Block 参数的 Block
"@?<v@?" // void (^)(void (^)())
// v 表示返回值类型为 void
// @? 表示参数类型为 Block// 示例5:带返回值的 Block
"@?<i@>" // int (^)(id)
// i 表示返回值类型为 int (基本类型不需要@前缀)
// @ 表示参数类型为 id// 示例6:带多个参数的 Block
"@?<viB@>" // void (^)(int, BOOL, id)
// v 表示返回值类型为 void
// i 表示第一个参数为 int
// B 表示第二个参数为 BOOL
// @ 表示第三个参数为 id
类型编码规则:
基本类型(int、BOOL等)不需要加@前缀
返回值类型如果是对象,需要加@
参数列表中的类型按顺序排列,不需要分隔符
对于具体类名的对象,可以使用@"ClassName"格式
@?
:表示这是一个 Block 类型< >
:包含 Block 的签名信息- 格式:
<返回值类型参数1类型参数2类型...>
- 返回值类型在前,参数类型依次排列
- 格式:
- 类型表示
- 基本类型:
v
:voidi
:intc
:charB
:BOOLf
:floatd
:doubleq
:NSIntegerQ
:NSUInteger
- 对象类型:
- @: id/NSObject
- @“NSString”: NSString* (具体类名)
- Block类型:
- @?: 另一个Block
- 基本类型:
// 复杂示例
typedef NSString *(^ComplexBlock)(int, void (^)(BOOL), NSArray<NSNumber *> *);// 对应的类型编码
"@?<@i@?@>"
// 或更精确的(如果希望包含类名):
"@?<@"NSString"i@?@"NSArray">"
逐部分解析:
部分 | 编码 | 对应类型 |
---|---|---|
返回值 | @ 或 @“NSString” | NSString * |
参数1 | i | int(基本类型,不加 @) |
参数2 | @? | void (^)(BOOL)(Block 参数) |
参数3 | @ 或 @“NSArray” | NSArray *(对象类型) |
2. Block 的三种类型
2.1 全局 Block(Global Block)
// 定义在全局作用域
void (^globalBlock)(void) = ^{NSLog(@"我是全局 Block");
};
特点:
- 不访问任何外部变量,完全独立于上下文
- 作为单例对象存储在程序的数据区,生命周期与程序相同
- 不需要特别的内存管理,由系统自动管理
- 性能最优,因为不需要捕获变量和内存管理开销
2.2 栈 Block(Stack Block)
- (void)example {int num = 10;void (^stackBlock)(void) = ^{NSLog(@"我是栈 Block: %d", num);};
}
特点:
- 访问外部变量,需要捕获上下文中的变量
- 作为临时对象存储在栈上,生命周期与所在函数相同
- 函数执行结束后可能失效,需要复制到堆上才能长期使用
- 性能较好,但需要注意使用时机和生命周期
2.3 堆 Block(Malloc Block)
- (void)example {int num = 10;// 方式1:手动复制到堆void (^heapBlock1)(void) = [^{NSLog(@"我是堆 Block: %d", num);} copy];// 方式2:赋值给属性自动复制到堆self.block = ^{NSLog(@"我是堆 Block: %d", num);};
}
特点:
- 由栈 Block 复制而来,存储在堆上,可以长期使用
- 需要管理内存,在 ARC 环境下由系统自动管理
- 可以安全地跨函数传递和使用
- 性能相对较低,但提供了最大的灵活性和安全性
2.4 内存管理差异
类型转换规则(ARC vs MRC)
操作场景 | ARC 环境 | MRC 环境 |
---|---|---|
赋值给 strong 变量 | 自动 copy 到堆 | 需要手动调用 copy |
作为函数返回值 | 自动 copy 到堆 | 需要手动调用 copy |
传递给 GCD API | 自动 copy 到堆 | 自动 copy 到堆 |
作为属性(copy) | 自动处理 | 必须显式 copy |
作为局部变量 | 保持在栈上 | 保持在栈上 |
作为全局变量 | 保持在数据区 | 保持在数据区 |
3. Block 如何捕获变量
3.1 基本类型变量
int num = 10;
void (^block)(void) = ^{NSLog(@"%d", num); // 只能读取,不能修改
};
编译后的结构:
// 编译器生成的 Block 结构体
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;int num; // 捕获的变量
};// 使用示例
struct __main_block_impl_0 block = {.impl = {...},.Desc = {...},.num = 10 // 值复制
};
特点:
- 在编译时,Block 会创建一个结构体来存储捕获的变量
- 变量的值会被复制到 Block 的结构体中,形成独立的副本
- Block 内部使用的是这个副本,与外部变量完全独立
- 这种值复制机制保证了 Block 执行时数据的稳定性
3.2 使用 __block 修饰符
__block int num = 10;
void (^block)(void) = ^{num = 20; // 可以修改NSLog(@"%d", num);
};
编译后的结构:
// __block 变量的包装结构体
struct __Block_byref_num_0 {void *__isa;__Block_byref_num_0 *__forwarding;int __flags;int __size;int num; // 原始变量
};// Block 结构体
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;__Block_byref_num_0 *num; // 通过指针引用
};
特点:
__block
修饰符会将被修饰的变量包装成一个结构体- 这个结构体包含原始变量的指针,使得 Block 可以修改原始变量
- 在 Block 被复制到堆上时,这个结构体也会被复制
- 所有引用这个变量的 Block 共享同一个结构体,保证修改的同步性
3.3 对象类型变量
NSObject *obj = [[NSObject alloc] init];
void (^block)(void) = ^{NSLog(@"%@", obj); // 强引用
};
编译后的结构:
// Block 结构体
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;NSObject *__strong obj; // 强引用// 编译器生成的辅助函数void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*) = __main_block_copy_0;void (*dispose)(struct __main_block_impl_0*) = __main_block_dispose_0;
};
特点:
- 对象类型的变量在 Block 中默认是强引用
- Block 会通过
retain
操作增加对象的引用计数 - 当 Block 被复制到堆上时,会再次
retain
对象 - 需要注意循环引用问题,可以使用
__weak
修饰符来避免 - 在 ARC 环境下,Block 会自动管理对象的引用计数
底层实现原理:
- Block 在编译时会生成一个结构体,包含捕获的变量
- 对于基本类型,直接复制值到结构体中
- 对于
__block
变量,会生成一个包装结构体 - 对于对象类型,会生成相应的引用计数管理代码
- Block 的
copy
操作会递归复制所有捕获的变量
3.4 小结
- 编译时处理:Block 在编译时会生成包含捕获变量的结构体,并根据变量类型生成相应的内存管理代码和辅助函数。
- 变量捕获方式:基本类型通过值复制形成独立副本,
__block
变量通过包装结构体实现共享访问,对象类型通过引用计数管理实现内存管理。 - 内存管理特点:基本类型无需特殊管理,
__block
变量通过共享结构体实现同步修改,对象类型通过自动引用计数管理生命周期。 - 性能考虑:基本类型性能最优,
__block
变量有指针间接访问开销,对象类型有引用计数管理开销。
4. Block 的内存管理与循环引用
4.1 内存管理规则
- 全局 Block:作为单例对象存储在数据区,生命周期与程序相同,无需特殊管理。
- 栈 Block:作为临时对象存储在栈上,在赋值给 strong 属性、作为返回值或传递给 GCD 时会自动复制到堆上。
- 堆 Block:由栈 Block 复制而来,在 ARC 环境下自动管理内存,遵循普通对象的内存管理规则。
4.2 循环引用问题与解决方案
循环引用示例
@interface DownloadManager : NSObject
@property (nonatomic, copy) void (^progressBlock)(CGFloat progress);
@property (nonatomic, copy) void (^completionBlock)(void);
@end@interface ViewController : UIViewController
@property (nonatomic, strong) DownloadManager *downloadManager;
@end@implementation ViewController
- (void)setup {// 错误写法:形成循环引用self.downloadManager = [[DownloadManager alloc] init];// 下载进度回调self.downloadManager.progressBlock = ^(CGFloat progress) {// ViewController 持有 downloadManager// progressBlock 持有 ViewController[self updateProgress:progress]; // 直接使用 self 导致循环引用};// 下载完成回调self.downloadManager.completionBlock = ^{// ViewController 持有 downloadManager// completionBlock 持有 ViewController[self handleDownloadComplete]; // 直接使用 self 导致循环引用};
}- (void)updateProgress:(CGFloat)progress {// 更新进度条
}- (void)handleDownloadComplete {// 处理下载完成
}
@end
循环引用分析:
- ViewController 持有 downloadManager(强引用)
- downloadManager 持有 progressBlock 和 completionBlock(copy 属性)
- progressBlock 和 completionBlock 都持有 ViewController(通过 self)
- 形成循环引用链:ViewController -> downloadManager -> Block -> ViewController
- 导致内存泄漏:这些对象都无法被释放
解决方案:
- 使用
__weak
修饰符打破循环引用 - 在 Block 内部使用 weak-strong dance 确保对象不会被释放
- 注意对象之间的引用关系,合理使用 weak 属性
正确写法:
@implementation ViewController
- (void)setup {self.downloadManager = [[DownloadManager alloc] init];// 使用 weak 引用打破循环__weak typeof(self) weakSelf = self;// 下载进度回调self.downloadManager.progressBlock = ^(CGFloat progress) {__strong typeof(weakSelf) strongSelf = weakSelf;[strongSelf updateProgress:progress];};// 下载完成回调self.downloadManager.completionBlock = ^{__strong typeof(weakSelf) strongSelf = weakSelf;[strongSelf handleDownloadComplete];};
}
@end
4.3 内存管理注意事项
- 在 MRC 环境下,堆 Block 需要手动管理内存(copy/release)
- 在 ARC 环境下,编译器会自动插入适当的内存管理代码
- 使用
__block
修饰的变量在 Block 复制到堆时也会被复制 - 对象类型的变量在 Block 中默认是强引用,需要注意循环引用问题
5. Block 的使用技巧
5.1 异步编程
在 iOS 开发中,Block 最常用的场景之一就是异步编程。通过 Block 可以优雅地处理异步操作的结果,避免回调地狱。
示例:数据加载
// 定义回调 Block 类型
typedef void (^Completion)(id result, NSError *error);- (void)loadData:(Completion)completion {// 在后台线程执行耗时操作dispatch_async(dispatch_get_global_queue(0, 0), ^{id result = [self fetchData];NSError *error = nil;// 回到主线程更新 UIdispatch_async(dispatch_get_main_queue(), ^{if (completion) {completion(result, error);}});});
}
使用说明:
- 使用
typedef
定义 Block 类型,提高代码可读性 - 在后台线程执行耗时操作,避免阻塞主线程
- 在主线程回调,确保 UI 更新操作安全
- 使用
if (completion)
检查 Block 是否存在
5.2 链式调用
Block 还可以用于实现链式调用语法,使代码更加流畅和易读。这种模式在构建对象时特别有用。
示例:Person 对象构建
@interface Person : NSObject
- (Person *(^)(NSString *))name;
- (Person *(^)(NSInteger))age;
@end@implementation Person
- (Person *(^)(NSString *))name {return ^Person *(NSString *name) {_name = name;return self; // 返回 self 实现链式调用};
}
@end// 使用示例
Person *person = [[Person alloc] init];
person.name(@"张三").age(20); // 链式调用
使用说明:
- 每个方法返回一个 Block,Block 返回 self
- 通过返回 self 实现链式调用
- 代码更加简洁,可读性更好
- 适合构建复杂对象时的配置
6. Block 的实际应用
6.1 回调处理
在 iOS 开发中,Block 广泛用于各种回调场景,如网络请求、数据加载等。
示例:网络请求回调
- (void)requestData:(void (^)(NSArray *data, NSError *error))completion {[self.network requestWithCompletion:^(id response, NSError *error) {if (completion) {completion(response, error);}}];
}
使用说明:
- 使用 Block 处理异步操作的结果
- 支持错误处理和成功回调
- 代码结构清晰,易于维护
6.2 动画处理
UIKit 框架中的动画 API 大量使用 Block 来处理动画过程和完成回调。
示例:视图动画
[UIView animateWithDuration:0.3 animations:^{// 动画过程中的属性修改view.alpha = 0.5;view.frame = newFrame;} completion:^(BOOL finished) {// 动画完成后的处理if (finished) {[view removeFromSuperview];}}];
使用说明:
animations
Block 定义动画过程completion
Block 处理动画完成- 支持动画取消和完成状态判断
6.3 枚举处理
Foundation 框架中的集合类提供了基于 Block 的枚举方法,使集合操作更加灵活。
示例:数组遍历
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {// 处理每个元素if ([obj isEqual:targetObject]) {*stop = YES; // 可以提前终止遍历}
}];
使用说明:
- 支持获取元素索引
- 可以通过
stop
参数提前终止遍历 - 比传统的 for 循环更加灵活
7. Block 性能优化与调试技巧
7.1 性能优化建议
-
避免不必要的 Block 复制
全局 Block 无需 copy,栈 Block 仅在需要时复制到堆,注意 __block 变量会随 Block 一起复制。 -
减少捕获变量数量
仅捕获必要变量,避免大型对象,高频场景优先使用全局 Block。 -
强弱引用策略
外部用 __weak 防循环引用,内部转 __strong 保对象存活,避免内部频繁创建临时对象。 -
内存布局优化
高频访问变量前置,减少捕获变量总数,评估 __block 变量的替代方案。
7.2 调试技巧
-
Instruments 三剑客
Allocations 追踪内存生命周期,Leaks 检测循环引用,Time Profiler 分析执行耗时 -
使用 LLDB 调试 Block
(lldb) po block # 打印 Block 对象 (lldb) p/x (long)block->isa # 查看 Block 的类型 (lldb) p/x (int)block->flags # 查看 Block 的 flags (lldb) p *(struct __block_impl *)block # 查看 Block 捕获的变量
-
问题排查三板斧
- 类型验证:po [block class] + 检查 flags 判断堆栈类型
- 变量审计:p/x 查看地址偏移,验证复制行为
- 内存验证:Xcode Memory Graph 可视化引用,malloc_history 追溯分配堆栈
-
工具链推荐
- 基础分析:Instruments 三件套(Allocations/Leaks/Time Profiler)
- 深度调试:Memory Graph 直观测循环引用,MallocStackLogging 定位野指针
8. 总结
Block 是 Objective-C 中的特殊对象,通过 isa 指针继承自 NSObject,实现了匿名函数的功能。它有三种类型:全局 Block(不捕获变量,存储在数据区)、栈 Block(捕获变量,存储在栈上)和堆 Block(由栈 Block 复制而来,存储在堆上)。Block 可以捕获变量,对于基本类型采用值复制,使用 __block
修饰符可以修改捕获的变量,对象类型默认是强引用。在内存管理方面,Block 遵循 Objective-C 的内存管理规则,需要注意循环引用问题,可以通过 __weak
和 weak-strong dance 模式解决。Block 的类型编码使用 @?<返回值类型参数类型>
的格式,支持基本类型、对象类型和 Block 类型。在实际开发中,Block 广泛应用于异步编程、回调处理、动画和集合操作等场景,使代码更加简洁和灵活。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注我,后续会持续分享更多 iOS 底层原理与实战经验!