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 的基本写法

objective-c 复制代码
// 最简单的 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 的底层结构

c 复制代码
// 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 的类型编码

objective-c 复制代码
// 基本格式:@?<返回值类型参数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
objective-c 复制代码
// 复杂示例
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)

objective-c 复制代码
// 定义在全局作用域
void (^globalBlock)(void) = ^{
    NSLog(@"我是全局 Block");
};

特点:

  • 不访问任何外部变量,完全独立于上下文
  • 作为单例对象存储在程序的数据区,生命周期与程序相同
  • 不需要特别的内存管理,由系统自动管理
  • 性能最优,因为不需要捕获变量和内存管理开销

2.2 栈 Block(Stack Block)

objective-c 复制代码
- (void)example {
    int num = 10;
    void (^stackBlock)(void) = ^{
        NSLog(@"我是栈 Block: %d", num);
    };
}

特点:

  • 访问外部变量,需要捕获上下文中的变量
  • 作为临时对象存储在栈上,生命周期与所在函数相同
  • 函数执行结束后可能失效,需要复制到堆上才能长期使用
  • 性能较好,但需要注意使用时机和生命周期

2.3 堆 Block(Malloc Block)

objective-c 复制代码
- (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 基本类型变量

objective-c 复制代码
int num = 10;
void (^block)(void) = ^{
    NSLog(@"%d", num);  // 只能读取,不能修改
};

编译后的结构

c 复制代码
// 编译器生成的 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 修饰符

objective-c 复制代码
__block int num = 10;
void (^block)(void) = ^{
    num = 20;  // 可以修改
    NSLog(@"%d", num);
};

编译后的结构

c 复制代码
// __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 对象类型变量

objective-c 复制代码
NSObject *obj = [[NSObject alloc] init];
void (^block)(void) = ^{
    NSLog(@"%@", obj);  // 强引用
};

编译后的结构

c 复制代码
// 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 循环引用问题与解决方案

循环引用示例
objective-c 复制代码
@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 属性
正确写法:
objective-c 复制代码
@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 可以优雅地处理异步操作的结果,避免回调地狱。

示例:数据加载

objective-c 复制代码
// 定义回调 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;
        
        // 回到主线程更新 UI
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completion) {
                completion(result, error);
            }
        });
    });
}

使用说明

  1. 使用 typedef 定义 Block 类型,提高代码可读性
  2. 在后台线程执行耗时操作,避免阻塞主线程
  3. 在主线程回调,确保 UI 更新操作安全
  4. 使用 if (completion) 检查 Block 是否存在

5.2 链式调用

Block 还可以用于实现链式调用语法,使代码更加流畅和易读。这种模式在构建对象时特别有用。

示例:Person 对象构建

objective-c 复制代码
@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 广泛用于各种回调场景,如网络请求、数据加载等。

示例:网络请求回调

objective-c 复制代码
- (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 来处理动画过程和完成回调。

示例:视图动画

objective-c 复制代码
[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 的枚举方法,使集合操作更加灵活。

示例:数组遍历

objective-c 复制代码
[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 复制代码
    (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 底层原理与实战经验!

相关推荐
小羊Linux客栈2 小时前
Python小程序:上班该做点摸鱼的事情
开发语言·python·小程序·游戏程序
咛辉2 小时前
如何搭建spark yarn 模式的集群集群。
开发语言
CoderCodingNo3 小时前
【GESP】C++三级练习 luogu-B2118 验证子串
开发语言·c++
小彭努力中3 小时前
9.Three.js中 ArrayCamera 多视角相机详解+示例代码
开发语言·前端·javascript·vue.js·数码相机·ecmascript·webgl
学习中的码虫3 小时前
c#栈及其应用
开发语言·c#
bai_lan_ya3 小时前
C语言中结构体的字节对齐的应用
c语言·开发语言
小陈094 小时前
Java后端图形验证码的使用
java·开发语言·状态模式
hu_yuchen4 小时前
C++:Lambda表达式
开发语言·c++·算法
沙尘暴炒饭4 小时前
解决vue3 路由query传参刷新后数据丢失的问题
开发语言·前端·javascript