johnhao
johnhao
发布于 2017-07-05 / 2 阅读
0

iOS- Block

block 是 Objective-C 中对闭包(closure)的实现,它允许你将一段代码逻辑连同其上下文环境一起封装起来,并作为参数传递、存储或延迟执行。理解 block 的原理、本质以及使用注意事项对于编写高效、安全的 Objective-C 代码非常重要。


一、Block 的原理与本质

1. 原理

  • Block 在底层是一个 结构体(struct),其中包含:

    • 函数指针(指向 block 的实现代码)

    • 描述 block 类型和内存管理信息的字段(如 isa 指针,用于支持 Objective-C 对象行为)

    • 捕获的外部变量(根据变量类型,可能是值拷贝或引用)

  • 当 block 被创建时,编译器会自动将其转换为一个对象(在 ARC 下是 NSMallocBlockNSStackBlockNSGlobalBlock),并处理捕获变量的内存布局。

2. 本质

  • Block 本质上是一个 带有上下文环境的函数对象(function object / functor)

  • 它可以访问其定义作用域内的变量(即“捕获”变量),但默认情况下这些变量是 只读的副本


二、__block 的作用

1. 作用

  • __block 修饰符用于 允许 block 内部修改外部作用域中的变量

  • 默认情况下,block 捕获的是变量的 值拷贝(immutable)。加上 __block 后,变量会被包装在一个特殊的结构体中(称为“byref”结构),block 和外部作用域共享这个结构体,从而实现 可变共享

2. 示例

__block int x = 10;
void (^blk)(void) = ^{
    x = 20; // 合法:x 被 __block 修饰
};
blk();
NSLog(@"%d", x); // 输出 20

三、使用 __block 的注意点

  1. 内存管理(尤其在 MRC 下):

    • __block 变量在 block 被 copy 到堆上时,也会被移到堆上。

    • 在 ARC 下通常无需手动管理,但在 MRC 下需注意释放。

  2. 循环引用风险

    • 如果 __block 修饰的是一个对象(如 __block id obj),且该 block 被 obj 持有,则可能造成 循环引用

    • 此时应配合 __weak 使用(见下文)。

  3. 不要滥用

    • 仅在确实需要修改外部变量时才使用 __block


四、为什么 block 属性要用 copy

  • Block 默认创建在 栈上NSStackBlock),生命周期受限于当前作用域。

  • 如果要将 block 作为属性保存(如赋值给实例变量),必须将其 复制到堆上(变成 NSMallocBlock),否则离开作用域后 block 会失效,导致 crash。

  • 使用 copy 修饰符会自动调用 Block_copy(),确保 block 被正确移到堆上。

@property (nonatomic, copy) void (^completionHandler)(void);

即使在 ARC 下,也必须使用 copy,因为 strong 不会触发 block 的堆拷贝(虽然某些编译器会优化,但规范写法是 copy)。


五、Block 使用注意事项

  1. 避免循环引用(retain cycle)

    • Block 会强引用捕获的对象(包括 self)。

    • 解决方法:使用 __weak__unsafe_unretained 弱引用 self。

    __weak typeof(self) weakSelf = self;
    self.block = ^{
        [weakSelf doSomething];
    };
    
  2. 不要在 block 中直接修改非 __block 修饰的局部变量(编译报错)。

  3. 多线程环境下注意线程安全

    • 如果多个 block 并发修改同一个 __block 变量,需加锁。

  4. 避免在 block 中过早 return 导致资源未释放(尤其在异步回调中)。


六、修改 NSMutableArray 是否需要 __block

不需要。

原因:

  • NSMutableArray 是一个 对象,block 捕获的是它的 指针(引用),而不是内容的拷贝。

  • 虽然 block 默认不能修改指针本身(即不能让变量指向另一个数组),但可以 调用其方法修改内部状态(如 addObject:)。

NSMutableArray *array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
    [array addObject:@"item"]; // ✅ 合法!修改的是对象内容,不是指针
};
blk();
NSLog(@"%@", array); // 输出 ["item"]

✅ 所以:只有当你需要重新赋值变量本身(如 array = anotherArray)时,才需要 __block
❌ 如果只是调用方法修改对象内部状态,不需要 __block


总结

问题

答案

Block 本质

带上下文的函数对象(结构体 + 函数指针)

__block 作用

允许 block 修改外部变量

何时用 __block

需要修改变量本身(如 int、指针重赋值)

NSMutableArray 需要 __block 吗?

不需要,除非你要 array = ...

Block 属性为何用 copy

确保 block 从栈移到堆,避免野指针

主要注意事项

循环引用、线程安全、正确使用 __block

希望这能帮你彻底理解 block 的机制与最佳实践!