当前位置: 首页 > news >正文

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 对象,可以响应 copyrelease 等消息
  • 内存管理遵循 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:void
      • i:int
      • c:char
      • B:BOOL
      • f:float
      • d:double
      • q:NSInteger
      • Q:NSUInteger
    • 对象类型:
      • @: id/NSObject
      • @“NSString”: NSString* (具体类名)
    • Block类型:
      • @?: 另一个Block
// 复杂示例
typedef NSString *(^ComplexBlock)(int, void (^)(BOOL), NSArray<NSNumber *> *);// 对应的类型编码
"@?<@i@?@>"  
// 或更精确的(如果希望包含类名):
"@?<@"NSString"i@?@"NSArray">"

逐部分解析:

部分编码对应类型
返回值@ 或 @“NSString”NSString *
参数1iint(基本类型,不加 @)
参数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 内存管理规则

  1. 全局 Block:作为单例对象存储在数据区,生命周期与程序相同,无需特殊管理。
  2. 栈 Block:作为临时对象存储在栈上,在赋值给 strong 属性、作为返回值或传递给 GCD 时会自动复制到堆上。
  3. 堆 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
循环引用分析:
  1. ViewController 持有 downloadManager(强引用)
  2. downloadManager 持有 progressBlock 和 completionBlock(copy 属性)
  3. progressBlock 和 completionBlock 都持有 ViewController(通过 self)
  4. 形成循环引用链:ViewController -> downloadManager -> Block -> ViewController
  5. 导致内存泄漏:这些对象都无法被释放
解决方案:
  1. 使用 __weak 修饰符打破循环引用
  2. 在 Block 内部使用 weak-strong dance 确保对象不会被释放
  3. 注意对象之间的引用关系,合理使用 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);}});});
}

使用说明

  1. 使用 typedef 定义 Block 类型,提高代码可读性
  2. 在后台线程执行耗时操作,避免阻塞主线程
  3. 在主线程回调,确保 UI 更新操作安全
  4. 使用 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);  // 链式调用

使用说明

  1. 每个方法返回一个 Block,Block 返回 self
  2. 通过返回 self 实现链式调用
  3. 代码更加简洁,可读性更好
  4. 适合构建复杂对象时的配置

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);}}];
}

使用说明

  1. 使用 Block 处理异步操作的结果
  2. 支持错误处理和成功回调
  3. 代码结构清晰,易于维护

6.2 动画处理

UIKit 框架中的动画 API 大量使用 Block 来处理动画过程和完成回调。

示例:视图动画

[UIView animateWithDuration:0.3 animations:^{// 动画过程中的属性修改view.alpha = 0.5;view.frame = newFrame;} completion:^(BOOL finished) {// 动画完成后的处理if (finished) {[view removeFromSuperview];}}];

使用说明

  1. animations Block 定义动画过程
  2. completion Block 处理动画完成
  3. 支持动画取消和完成状态判断

6.3 枚举处理

Foundation 框架中的集合类提供了基于 Block 的枚举方法,使集合操作更加灵活。

示例:数组遍历

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {// 处理每个元素if ([obj isEqual:targetObject]) {*stop = YES;  // 可以提前终止遍历}
}];

使用说明

  1. 支持获取元素索引
  2. 可以通过 stop 参数提前终止遍历
  3. 比传统的 for 循环更加灵活

7. Block 性能优化与调试技巧

7.1 性能优化建议

  1. 避免不必要的 Block 复制
    全局 Block 无需 copy,栈 Block 仅在需要时复制到堆,注意 __block 变量会随 Block 一起复制。

  2. 减少捕获变量数量
    仅捕获必要变量,避免大型对象,高频场景优先使用全局 Block。

  3. 强弱引用策略
    外部用 __weak 防循环引用,内部转 __strong 保对象存活,避免内部频繁创建临时对象。

  4. 内存布局优化
    高频访问变量前置,减少捕获变量总数,评估 __block 变量的替代方案。

7.2 调试技巧

  1. Instruments 三剑客
    Allocations 追踪内存生命周期,Leaks 检测循环引用,Time Profiler 分析执行耗时

  2. 使用 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 捕获的变量
    
  3. 问题排查三板斧

    • 类型验证:po [block class] + 检查 flags 判断堆栈类型
    • 变量审计:p/x 查看地址偏移,验证复制行为
    • 内存验证:Xcode Memory Graph 可视化引用,malloc_history 追溯分配堆栈
  4. 工具链推荐

    • 基础分析: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 底层原理与实战经验!

相关文章:

  • WEBSTORM前端 —— 第2章:CSS —— 第4节:盒子模型
  • phpstudy修改Apache端口号
  • (开源)视频画面增强模型:Ev-DeblurVSR (可以解决视频画面不清晰的问题)
  • C++之类和对象:构造函数,析构函数,拷贝构造,赋值运算符重载
  • 从Transformer原理角度来看,prompt设置输出字数限制会生效的原因
  • 8.idea创建maven项目(使用Log4j日志记录框架+Log4j 介绍)
  • Java后端程序员学习前端之html
  • 关于浏览器对于HTML实体编码,urlencode,Unicode解析
  • gem5-gpu 安装过程碰到的问题记录 关于使用 Ruby + Garnet
  • RabbitMQ 启动报错 “crypto.app“ 的解决方法
  • 余额分账和代付有什么区别?
  • AVL树左旋右旋的实现
  • Error: error:0308010C:digital envelope routines::unsupported 高版本node启动低版本项目运行报错
  • Android启动应用时屏蔽RecyclerView滑动,延时后再允许滑动,Kotlin
  • 【免费下载】1985-2023年全国土地利用数据
  • GD32F407单片机开发入门(十七)内部RTC实时时钟及实战含源码
  • 请简述一下什么是 Kotlin?它有哪些特性?
  • React Native 太慢:kotlin-gradle-plugin-2.0.21-gradle76.jar 下载太慢
  • git学习之git常用命令
  • MATLAB函数调用全解析:从入门到精通
  • “光荣之城”2025上海红色文化季启动,红色主题市集亮相
  • 美国“杜鲁门”号航母一战机坠海
  • 王星昊再胜连笑,夺得中国围棋天元赛冠军
  • 科学时代重读“老子”的意义——对谈《老子智慧八十一讲》
  • 柴德赓、纪庸与叫歇碑
  • 解放日报头版头条:“五个中心”蹄疾步稳谱新篇