iOS开发基础75-Block深度解析
iOS Block 深度解析:从底层结构到内存管理与循环引用
Block 是 iOS 和 macOS 开发中最常用、最高效的特性之一。它本质上是一个封装了代码和捕获变量的对象,为回调、异步操作、枚举遍历等场景提供了简洁优雅的解决方案。但 Block 的底层结构、三种内存类型、变量捕获机制、循环引用等问题,也是面试高频考点和开发中的常见坑。本文将从最基础的语法讲起,深入底层结构和内存管理,全面拆解 Block。
一、Block 是什么
1. 基本概念
一句话解释:Block 是一个封装了代码逻辑和其执行时所需上下文(捕获的变量)的对象,类似于其他语言中的闭包(Closure)或 lambda 表达式。
Block 的核心特点:
- 封装了一段代码,可以在任意时候调用
- 可以捕获外部变量,并在执行时使用
- 本身是一个对象(有 isa 指针),可以被持有、作为参数传递、作为返回值
- 是 C 语言的扩展,兼容 C、Objective-C、C++
2. 基本语法
Block 的语法看起来有点绕,记住一个公式:
返回值类型 (^Block名称)(参数类型) = ^(参数) { 代码体 };
// 无返回值、无参数
void (^block1)(void) = ^{
NSLog(@"Hello, Block!");
};
// 无返回值、有参数
void (^block2)(NSString *name) = ^(NSString *name) {
NSLog(@"你好,%@", name);
};
// 有返回值、有参数
int (^addBlock)(int a, int b) = ^int(int a, int b) {
return a + b;
};
// 调用
block1();
block2(@"张三");
int result = addBlock(10, 20); // 30
小技巧:返回值类型可以省略,编译器会根据 return 语句自动推断。
^int(int a, int b){ return a+b; }可以简写为^(int a, int b){ return a+b; }。
3. typedef 简化 Block 声明
Block 类型写起来很长,用 typedef 定义别名可以大幅简化:
// 定义 Block 类型
typedef void (^CompletionBlock)(BOOL success, NSString *message);
typedef int (^CalculateBlock)(int a, int b);
// 使用
@property (nonatomic, copy) CompletionBlock completion;
- (void)requestWithCompletion:(CompletionBlock)completion {
// ...
}
二、Block 的三种类型(重点)
Block 有三种类型,区别在于内存存储的位置不同。可以通过 NSLog(@"%@", [block class]) 查看 Block 的类型。
| 类型 | 类名 | 存储位置 | 生命周期 |
|---|---|---|---|
| 全局 Block | _NSConcreteGlobalBlock |
数据段(.text 区附近) | 程序运行期间一直存在 |
| 栈 Block | _NSConcreteStackBlock |
栈空间 | 作用域结束后自动销毁 |
| 堆 Block | _NSConcreteMallocBlock |
堆空间 | 引用计数为 0 时销毁 |
1. 全局 Block(_NSConcreteGlobalBlock)
判断条件:Block 不捕获任何外部自动变量(局部变量),就是全局 Block。
// 全局作用域定义,不捕获外部变量 → 全局 Block
void (^globalBlock)(void) = ^{
NSLog(@"I'm a global block");
};
int main() {
// 函数内定义,但不捕获外部变量 → 也是全局 Block
void (^globalBlock2)(void) = ^{
NSLog(@"I'm also a global block");
};
NSLog(@"%@", [globalBlock2 class]); // _NSConcreteGlobalBlock
}
全局 Block 存储在程序的数据段,生命周期和程序一致,不需要管理内存,copy 操作对它无效(还是返回自身)。
2. 栈 Block(_NSConcreteStackBlock)
判断条件:Block 捕获了外部自动变量(局部变量),且定义在函数/方法内部,在 MRC 下是栈 Block。
- (void)method {
int a = 10; // 自动变量(局部变量)
// 捕获了外部变量 a → 栈 Block(MRC 下)
void (^stackBlock)(void) = ^{
NSLog(@"a = %d", a);
};
NSLog(@"%@", [stackBlock class]);
// MRC: _NSConcreteStackBlock
// ARC: 可能是 _NSConcreteMallocBlock(ARC 会自动 copy,见后文)
}
栈 Block 存储在栈上,作用域结束后会被自动销毁。如果在作用域外调用栈 Block,会导致 EXC_BAD_ACCESS 崩溃(这是 MRC 时代的经典 bug)。
注意:在 ARC 下,大多数情况下栈 Block 会被自动 copy 到堆上,所以 ARC 下很少能直接看到
_NSConcreteStackBlock。但理解栈 Block 的概念对理解底层原理很重要。
3. 堆 Block(_NSConcreteMallocBlock)
判断条件:对栈 Block 执行 copy 操作后,就变成了堆 Block。
- (void)method {
int a = 10;
void (^stackBlock)(void) = ^{
NSLog(@"a = %d", a);
};
// 对栈 Block 调用 copy → 堆 Block
void (^heapBlock)(void) = [stackBlock copy];
NSLog(@"%@", [heapBlock class]); // _NSConcreteMallocBlock
}
堆 Block 存储在堆上,生命周期由引用计数管理(ARC 自动管理),作用域结束后不会销毁,只要有强引用就一直存在。
4. 三种类型的 copy 行为
| Block 类型 | copy 操作结果 |
|---|---|
| 全局 Block | 什么都不做,返回自身(还是全局 Block) |
| 栈 Block | 从栈 copy 到堆,返回堆 Block |
| 堆 Block | 引用计数 +1,返回自身(还是堆 Block) |
5. 如何查看 Block 类型
void (^block)(void) = ^{
NSLog(@"test");
};
NSLog(@"Block 类型: %@", [block class]);
// 输出可能是 _NSConcreteGlobalBlock / _NSConcreteStackBlock / _NSConcreteMallocBlock
三、Block 的底层结构(重点)
Block 的本质是一个结构体。通过 LLVM 编译后,一个 Block 的底层结构如下:
struct Block_layout {
void *isa; // ① isa 指针,指向 Block 的类对象
int flags; // ② 标志位,存储 Block 的特性信息
int reserved; // ③ 保留字段
void (*invoke)(void *, ...); // ④ 函数指针,指向 Block 的实现代码
struct Block_descriptor *descriptor; // ⑤ 描述符,存储 Block 大小等信息
// ⑥ 捕获的变量(紧跟在结构体后面)
};
struct Block_descriptor {
unsigned long int reserved; // 保留
unsigned long int size; // Block 结构体的大小
void (*copy_helper)(void *dst, const void *src); // copy 辅助函数(捕获了对象时才有)
void (*dispose_helper)(const void *src); // dispose 辅助函数(捕获了对象时才有)
};
下面逐个字段解释。
1. isa 指针
和所有 Objective-C 对象一样,Block 的第一个字段是 isa 指针,指向 Block 的类对象。根据 Block 类型不同,isa 分别指向:
_NSConcreteGlobalBlock(全局 Block)_NSConcreteStackBlock(栈 Block)_NSConcreteMallocBlock(堆 Block)
这就是为什么 Block 是一个对象——它有 isa 指针,可以被当作 id 类型传递。
2. flags(标志位)
flags 是一个 int,每一位代表不同的含义:
| 标志位 | 含义 |
|---|---|
BLOCK_DEALLOCATING (1<<0) |
Block 正在释放 |
BLOCK_REFCOUNT_MASK |
引用计数(低 3 位,实际引用计数存在这里) |
BLOCK_NEEDS_FREE (1<<24) |
Block 是否需要释放(堆 Block 需要) |
BLOCK_HAS_COPY_DISPOSE (1<<25) |
是否有 copy/dispose 辅助函数(捕获了 OC 对象时设置) |
BLOCK_HAS_CTOR (1<<26) |
是否有 C++ 构造函数 |
BLOCK_IS_GLOBAL (1<<28) |
是否是全局 Block |
BLOCK_USE_STRET (1<<29) |
返回值是否是结构体(需要 stret 调用) |
BLOCK_HAS_SIGNATURE (1<<30) |
是否有类型签名(包含参数和返回值类型信息) |
3. invoke(函数指针)
invoke 是一个函数指针,指向 Block 代码体编译后生成的 C 函数。调用 Block 时,本质上就是调用这个函数指针。
// 这个 Block:
int (^addBlock)(int a, int b) = ^int(int a, int b) {
return a + b;
};
// 编译后大致变成:
int addBlock_invoke(struct Block_layout *block, int a, int b) {
return a + b;
}
// block->invoke = addBlock_invoke
调用 addBlock(10, 20) 本质上就是 block->invoke(block, 10, 20),第一个参数是 Block 自身(类似 OC 方法的 self),这样函数内部才能访问捕获的变量。
4. descriptor(描述符)
描述符存储 Block 的附加信息:
size:Block 结构体的总大小(包括捕获变量的空间)copy_helper:Block 从栈 copy 到堆时调用,负责 retain 捕获的 OC 对象dispose_helper:Block 释放时调用,负责 release 捕获的 OC 对象
只有当 Block 捕获了 OC 对象(或
__block变量)时,descriptor 才会包含 copy_helper 和 dispose_helper。如果只捕获了基本数据类型(int、float 等),则不需要这两个函数。
5. 捕获的变量
捕获的变量紧跟在 Block_layout 结构体后面,作为结构体的一部分存储。每个捕获的变量在 Block 内部都有一个副本。
int a = 10;
NSString *name = @"张三";
void (^block)(void) = ^{
NSLog(@"a = %d, name = %@", a, name);
};
// 编译后 Block 的内存布局大致是:
// struct Block_layout {
// isa, flags, reserved, invoke, descriptor,
// int a; // 捕获的基本类型变量(值拷贝)
// NSString *name; // 捕获的 OC 对象(指针拷贝,ARC 下会 retain)
// };
6. Block 调用的完整底层过程
以这个 Block 为例:
- (void)test {
int multiplier = 3;
int (^multiplyBlock)(int) = ^int(int x) {
return x * multiplier;
};
int result = multiplyBlock(5); // 15
}
底层执行过程:
- 编译器在栈上创建
Block_layout结构体,设置 isa =_NSConcreteStackBlock,invoke 指向编译生成的函数,捕获的multiplier = 3存在结构体末尾。 - 调用
multiplyBlock(5)时,编译器转为block->invoke(block, 5)。 - invoke 函数内部通过 block 指针找到捕获的
multiplier = 3,计算5 * 3 = 15,返回结果。 - 方法作用域结束,栈上的 Block 被自动销毁(MRC 下;ARC 下如果被强引用会先 copy 到堆)。
四、Block 的变量捕获(重点)
Block 可以捕获外部变量,但不同类型的变量捕获方式不同。这是理解 Block 的核心,也是面试高频考点。
1. 自动变量(局部变量)—— 值捕获
自动变量就是函数/方法内定义的局部变量(非 static)。Block 对自动变量是值捕获——在 Block 定义时,把变量的值拷贝一份存到 Block 内部,之后外部变量的变化不影响 Block 内部的值。
int a = 10;
void (^block)(void) = ^{
NSLog(@"Block 内: a = %d", a); // 捕获时的值 10
};
a = 20; // 外部修改 a
block(); // 输出:Block 内: a = 10(不是 20!)
为什么是值捕获? 因为自动变量存储在栈上,作用域结束后就销毁了。如果 Block 是引用捕获,Block 执行时变量可能已经销毁,导致野指针。所以 Block 在定义时把值拷贝一份存到自己的结构体里,保证安全。
Block 内不能修改自动变量:
int a = 10;
void (^block)(void) = ^{
// a = 20; // 编译报错!Variable is not assignable (missing __block type specifier)
NSLog(@"%d", a); // 只能读,不能写
};
因为 Block 内部的 a 是值拷贝的常量,修改它没有意义(外部的 a 不会变),所以编译器直接禁止。
2. 静态变量(static)—— 引用捕获
用 static 修饰的局部变量存储在静态存储区(全局数据区),生命周期和程序一致。Block 对静态变量是引用捕获——存储变量的地址,Block 内可以读写,且和外部共享同一个变量。
static int a = 10;
void (^block)(void) = ^{
NSLog(@"Block 内: a = %d", a);
a = 30; // 可以修改
};
a = 20;
block(); // 输出:Block 内: a = 20(引用捕获,外部修改生效)
NSLog(@"Block 外: a = %d", a); // 输出:Block 外: a = 30(Block 内修改也生效)
3. 全局变量 / 静态全局变量 —— 不捕获
全局变量和静态全局变量存储在全局数据区,任何地方都可以直接访问,Block 不需要捕获,直接使用即可。
int globalVar = 10; // 全局变量
static int staticGlobalVar = 20; // 静态全局变量
void (^block)(void) = ^{
NSLog(@"%d, %d", globalVar, staticGlobalVar); // 直接访问,不捕获
globalVar = 30; // 可以修改
};
4. self 的捕获
在 OC 方法中,self 是一个隐藏的自动变量(指向当前对象)。Block 内使用 self 的属性或方法时,会捕获 self(强引用,ARC 下会 retain self)。
@interface MyClass : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) void (^block)(void);
@end
@implementation MyClass
- (void)test {
self.name = @"张三";
// Block 内使用了 self.name → 捕获了 self(强引用)
self.block = ^{
NSLog(@"%@", self.name); // 这里捕获了 self
};
}
@end
这就是循环引用的根源——self 持有 block(属性),block 又持有 self(捕获),形成循环。详见后文"循环引用"章节。
如果 Block 内只用到了某个属性,也可以只捕获属性的值(在 Block 定义时取出),避免捕获 self:
- (void)test {
NSString *name = self.name; // 提前取出,值捕获
self.block = ^{
NSLog(@"%@", name); // 只捕获了 name 变量,没有捕获 self
};
}
5. 变量捕获方式总结
| 变量类型 | 存储位置 | 捕获方式 | Block 内能否修改 | 外部变化是否影响 Block 内 |
|---|---|---|---|---|
| 自动变量(局部) | 栈 | 值捕获(拷贝值) | 不能 | 不影响 |
| 静态变量(static 局部) | 静态区 | 引用捕获(拷贝地址) | 能 | 影响 |
| 全局变量 | 全局区 | 不捕获,直接访问 | 能 | 影响 |
| 静态全局变量 | 全局区 | 不捕获,直接访问 | 能 | 影响 |
| self(方法内) | 堆 | 值捕获(拷贝指针,ARC 下 retain) | 能修改属性 | 影响 |
| __block 变量 | 栈→堆 | 引用捕获(通过 forwarding 指针) | 能 | 影响 |
五、__block 底层原理深入(重点+高频面试题)
1. __block 的作用
__block 修饰符允许 Block 内部修改外部的自动变量:
__block int a = 10;
void (^block)(void) = ^{
a = 20; // 可以修改了
NSLog(@"a = %d", a);
};
block(); // a = 20
NSLog(@"a = %d", a); // a = 20(外部也变了)
2. __block 变量的底层结构
__block 变量在编译后会被包装成一个结构体 Block_byref:
// __block int a = 10; 编译后大致变成:
struct Block_byref_int {
void *isa; // isa 指针(__block 变量也是一个对象)
struct Block_byref_int *forwarding; // 指向自身或堆上的副本
int flags; // 标志位
int size; // 结构体大小
int a; // 实际存储的变量值
};
struct Block_byref_int a = {
.isa = _NSConcreteStackBlock, // 栈上的 __block 变量
.forwarding = &a, // 指向自身
.flags = 0,
.size = sizeof(struct Block_byref_int),
.a = 10 // 初始值
};
关键点:
__block变量不再是一个简单的 int,而是一个结构体。- 结构体里有一个
forwarding指针,初始时指向栈上的自己。 - 实际的值
a存储在结构体内部。
3. forwarding 指针的作用(核心)
当 Block 从栈 copy 到堆时,Block 捕获的 __block 变量也会从栈 copy 到堆。此时:
- 栈上的
__block变量的forwarding指针被修改为指向堆上的副本。 - 堆上的
__block变量的forwarding指针指向自身。
copy 之前(都在栈上):
栈上 __block变量.forwarding → 栈上 __block变量(指向自己)
copy 之后(Block 和 __block 变量都到了堆上):
栈上 __block变量.forwarding → 堆上 __block变量(指向堆上的副本!)
堆上 __block变量.forwarding → 堆上 __block变量(指向自己)
这样,无论代码是在 Block 内部(访问堆上的变量)还是 Block 外部(访问栈上的变量),通过 forwarding 指针最终都指向堆上的同一个副本,保证了值的一致性。
__block int a = 10;
void (^block)(void) = ^{
a = 20; // 内部通过 forwarding 指针访问堆上的 a
};
a = 30; // 外部也通过 forwarding 指针访问堆上的 a(Block copy 后)
block();
// 最终 a = 20,内外一致
这就是
__block变量能在 Block 内外共享的核心原理——通过 forwarding 指针统一指向堆上的副本。
4. __block 变量的内存管理
- Block copy 到堆时,会同时 copy 它捕获的
__block变量到堆,并 retain(如果是 OC 对象)。 - 多个 Block 捕获同一个
__block变量时,堆上的__block变量有引用计数,最后一个 Block 释放时才释放__block变量。 __block修饰的 OC 对象变量,在 ARC 下会被 Block retain(和普通捕获的 OC 对象一样),所以也可能导致循环引用。
5. __block 与循环引用
在 MRC 时代,常用 __block 打破循环引用:
// MRC 下的写法
__block id weakSelf = self; // __block 修饰的对象不会被 retain
self.block = ^{
[weakSelf doSomething]; // weakSelf 是 __block,不被 retain,不循环引用
};
但在 ARC 下,__block 修饰的对象会被 Block retain,所以 ARC 下不能用 __block 打破循环引用,必须用 __weak。
// ARC 下 __block 不能打破循环引用!
__block id blockSelf = self;
self.block = ^{
[blockSelf doSomething]; // ARC 下 blockSelf 会被 retain,仍然循环引用!
};
六、Block 的内存管理
1. MRC 下的 Block 内存管理
MRC 下需要手动管理 Block 的内存:
- 栈 Block:作用域结束后自动销毁,不能在作用域外使用。
- 如果需要在作用域外使用 Block,必须调用
copy将其 copy 到堆。 - 堆 Block 使用完需要
release。 - Block 作为属性必须用
copy修饰(不能用 retain,因为 retain 栈 Block 不会把它移到堆上)。
// MRC 下
@property (nonatomic, copy) void (^block)(void); // 必须 copy
- (void)test {
int a = 10;
self.block = ^{ // 赋值给 copy 属性时会自动 copy
NSLog(@"%d", a);
};
}
2. ARC 下的 Block 自动 copy
ARC 下,编译器会在以下情况自动将栈 Block copy 到堆:
| 场景 | 是否自动 copy |
|---|---|
| Block 作为函数/方法返回值 | 自动 copy |
Block 赋值给 strong/copy 修饰的属性或变量 |
自动 copy |
Block 作为参数传递给 NSArray/NSDictionary 的方法(如 addObject) |
自动 copy |
| Block 作为参数传递给 GCD 方法(dispatch_async 等) | 自动 copy(GCD 内部会 copy) |
Block 作为参数传递给 enumerateObjectsUsingBlock: 等方法 |
自动 copy(方法内部会 copy) |
| Block 直接调用(不传递、不赋值) | 不 copy,仍在栈上 |
| Block 作为方法参数传递(普通方法,方法内部不 copy) | 不 copy,仍在栈上 |
简单来说:只要 Block 被传递到作用域之外(赋值、返回、存入集合、传入 GCD 等),ARC 就会自动 copy 到堆。 只有在当前作用域内直接调用时,才可能停留在栈上。
所以 ARC 下你几乎看不到 _NSConcreteStackBlock,大部分 Block 都被自动 copy 成了堆 Block。
3. Block 作为属性用 copy 还是 strong?
| 修饰符 | MRC | ARC |
|---|---|---|
copy |
必须用 copy | 推荐用 copy(习惯延续,效果和 strong 一样) |
strong |
不能用(retain 栈 Block 不会移到堆) | 可以用,效果和 copy 一样(ARC 会自动 copy) |
weak/assign |
不能用 | 不能用(Block 会被立即释放) |
结论:
- MRC 下必须用
copy。 - ARC 下用
copy或strong都可以,但约定俗成用copy,这样代码在 MRC/ARC 下都安全,也更清晰地表达了"Block 需要 copy 到堆"的语义。
// 推荐写法
@property (nonatomic, copy) void (^completionBlock)(void);
@property (nonatomic, copy) NSString *name; // NSString 也用 copy,同理
4. Block 作为方法参数
Block 作为方法参数时,不需要用 copy 修饰(参数是局部的,方法内部如果需要持有会自己 copy):
- (void)doSomethingWithCompletion:(void (^)(BOOL success))completion {
// 方法内部如果需要异步持有 completion,会自己 copy
self.completion = completion; // 赋值给 copy 属性时自动 copy
}
5. Block 的 copy 操作做了什么
对一个栈 Block 调用 copy 时:
- 在堆上分配一块和 Block 大小相同的内存。
- 将栈 Block 的内容(isa、flags、invoke、descriptor、捕获的变量)拷贝到堆上。
- 将堆 Block 的 isa 改为
_NSConcreteMallocBlock,flags 设置BLOCK_NEEDS_FREE。 - 如果捕获了 OC 对象或
__block变量,调用copy_helper:- retain 捕获的 OC 对象。
- 将
__block变量也 copy 到堆,并修改其 forwarding 指针。
- 返回堆 Block 的地址。
Block 释放时调用 dispose_helper,release 捕获的 OC 对象和 __block 变量。
七、Block 的循环引用(重点+高频面试题)
1. 循环引用产生的原因
循环引用(Retain Cycle)是指两个对象互相强引用,导致引用计数永远不为 0,内存无法释放。
Block 导致循环引用的经典场景:
@interface MyClass : NSObject
@property (nonatomic, copy) void (^block)(void); // self 强引用 block
@end
@implementation MyClass
- (void)test {
self.block = ^{
NSLog(@"%@", self); // block 强引用 self(捕获了 self)
};
}
// self → block → self,形成循环引用,self 无法释放
@end
关系图:
self ──strong──▶ block
▲ │
└────strong────────┘ (block 捕获了 self)
2. 哪些场景会循环引用,哪些不会?
会循环引用的场景
场景一:self 持有 Block 属性,Block 内引用 self
// 会循环引用
self.block = ^{
[self doSomething];
};
场景二:NSTimer + Block(iOS 10+ 的 block 形式 API)
// 会循环引用:timer 强引用 self(block 捕获 self),self 强引用 timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[self doSomething];
}];
场景三:__block 修饰对象(ARC 下)
// ARC 下会循环引用(__block 对象会被 retain)
__block MyClass *blockSelf = self;
self.block = ^{
[blockSelf doSomething];
};
不会循环引用的场景
场景一:Block 作为方法参数(GCD、UIView 动画等)
// 不会循环引用:GCD 执行完就释放 block,不长期持有 self
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething]; // self 被 block 强引用,但 block 是临时的,执行完就释放
});
// 不会循环引用:UIView 动画 block 执行完就释放
[UIView animateWithDuration:0.3 animations:^{
self.view.alpha = 0.5;
}];
判断标准:self 是否长期持有这个 Block? 如果 Block 是临时的(作为参数传入,执行完就释放),就不会循环引用;如果 self 把 Block 存为属性长期持有,且 Block 内引用了 self,就会循环引用。
场景二:Block 内不引用 self
// 不会循环引用:block 没有捕获 self
self.block = ^{
NSLog(@"hello"); // 没用到 self
};
场景三:提前取出属性值(值捕获)
// 不会循环引用:只捕获了 name 变量,没有捕获 self
NSString *name = self.name;
self.block = ^{
NSLog(@"%@", name);
};
3. 解决方案
方案一:__weak + __strong(weak-strong dance,推荐)
这是 ARC 下最标准、最推荐的解决方案:
__weak typeof(self) weakSelf = self; // 弱引用,不增加引用计数
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf; // 进入 block 后临时强引用,防止执行中 self 被释放
if (strongSelf) { // 判空,weakSelf 可能为 nil
[strongSelf doSomething];
NSLog(@"%@", strongSelf.name);
}
};
为什么需要 __strong?
__weak引用的对象可能在 Block 执行过程中被释放(变成 nil)。- 如果 Block 内有多个操作,第一个操作后 self 被释放,后面的操作就会访问 nil(虽然 OC 给 nil 发消息不崩溃,但逻辑可能出错)。
- 用
__strong在 Block 开始时临时持有 self,保证 Block 执行期间 self 不被释放,Block 执行完后 strongSelf 自动释放,不影响 self 的正常销毁。
简单场景可以只用 __weak:
// 如果 Block 内只有一个简单操作,可以不用 __strong
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // 即使 weakSelf 为 nil,给 nil 发消息也安全
};
方案二:使用 ReactiveObjC 的 @weakify/@strongify 宏
如果项目用了 ReactiveObjC(RAC),可以用更简洁的宏:
@weakify(self);
self.block = ^{
@strongify(self);
if (self) {
[self doSomething];
}
};
本质和 weak-strong dance 一样,只是宏帮你写了样板代码。
方案三:手动断开引用
在合适的时机把 Block 置为 nil,打破循环:
// 页面消失时断开
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
self.block = nil; // 打破循环引用
}
// 或 Block 执行完后自己置 nil
self.block = ^{
[self doSomething];
self.block = nil; // 执行完后断开(注意:block 内引用 self 仍会循环,但执行完后置 nil 就打破了)
};
这种方式不推荐作为主要方案,因为容易忘记断开。weak-strong dance 是更安全的默认选择。
方案四:NSTimer 的解决方案
NSTimer 的循环引用比较特殊,因为 timer 会被 RunLoop 持有:
// 方案一:用 __weak(iOS 10+ block API)
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer *timer) {
[weakSelf doSomething];
}];
// 方案二:在 dealloc 前 invalidate(必须在 viewWillDisappear 等时机,不能在 dealloc,因为循环引用导致 dealloc 不会调用)
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
// 方案三:用 NSProxy 中间对象打破引用(YYWeakProxy 等)
4. 循环引用检测工具
| 工具 | 说明 |
|---|---|
| Instruments → Leaks | 苹果官方,可检测循环引用和内存泄漏 |
| Instruments → Allocations | 查看对象引用计数变化 |
| FBRetainCycleDetector | Facebook 开源,代码级循环引用检测 |
| MLeaksFinder | 腾讯开源,自动检测 UIViewController 泄漏 |
| Xcode → Debug Memory Graph | Xcode 8+ 内置,可视化查看对象引用关系图 |
Xcode Memory Graph 使用方法:
- 运行 App,进入可疑页面。
- 点击 Xcode 底部调试栏的"Debug Memory Graph"按钮(三个圆圈的图标)。
- Xcode 会显示当前内存中的对象及引用关系图。
- 如果有循环引用,Xcode 会用紫色图标提示。
- 点击对象可以查看引用链,定位循环引用的原因。
八、Block 的常见使用场景
1. 回调(代替 delegate)
Block 最常用的场景就是回调,比 delegate 更简洁,代码聚合在一起:
// 网络请求回调
- (void)requestDataWithCompletion:(void (^)(NSArray *data, NSError *error))completion {
// 异步请求...
if (completion) {
completion(data, nil);
}
}
// 调用
[self requestDataWithCompletion:^(NSArray *data, NSError *error) {
if (error) {
NSLog(@"失败: %@", error);
} else {
NSLog(@"成功: %@", data);
}
}];
2. 枚举遍历
NSArray *array = @[@"张三", @"李四", @"王五"];
// 遍历数组
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"第 %lu 个: %@", idx, obj);
if ([obj isEqualToString:@"李四"]) {
*stop = YES; // 停止遍历
}
}];
// 带选项的遍历(反向、并发)
[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"反向: %@", obj);
}];
// 字典遍历
NSDictionary *dict = @{@"name": @"张三", @"age": @18};
[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
NSLog(@"%@ = %@", key, obj);
}];
3. 排序
NSArray *numbers = @[@3, @1, @4, @1, @5, @9, @2, @6];
// 升序排序
NSArray *sorted = [numbers sortedArrayUsingComparator:^NSComparisonResult(NSNumber *obj1, NSNumber *obj2) {
return [obj1 compare:obj2]; // 升序
// return [obj2 compare:obj1]; // 降序
}];
// 按对象属性排序
NSArray *people = @[person1, person2, person3];
NSArray *sortedPeople = [people sortedArrayUsingComparator:^NSComparisonResult(Person *p1, Person *p2) {
return [p1.name compare:p2.name]; // 按姓名排序
}];
4. GCD 多线程
// 异步执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 后台线程执行耗时操作
NSData *data = [NSData dataWithContentsOfURL:url];
dispatch_async(dispatch_get_main_queue(), ^{
// 回主线程更新 UI
self.imageView.image = [UIImage imageWithData:data];
});
});
// 延迟执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2 秒后执行");
});
// 只执行一次(单例)
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行一次的代码
});
5. UIView 动画
[UIView animateWithDuration:0.3 animations:^{
self.view.alpha = 0.5;
self.view.frame = CGRectMake(0, 0, 100, 100);
} completion:^(BOOL finished) {
NSLog(@"动画完成");
}];
// 弹性动画
[UIView animateWithDuration:0.5 delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0.5 options:0 animations:^{
self.view.center = CGPointMake(200, 200);
} completion:nil];
6. 通知(Block 形式)
// 注册通知(Block 形式,需要手动移除)
id observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
NSLog(@"键盘将要显示");
}];
// 移除通知
[[NSNotificationCenter defaultCenter] removeObserver:observer];
九、Block 与 delegate / 通知的对比
1. 对比表
| 对比维度 | Block | Delegate | 通知(NSNotification) |
|---|---|---|---|
| 通信方式 | 一对一 | 一对一 | 一对多(广播) |
| 代码聚合 | 高(回调代码和调用代码在一起) | 中(需要实现代理方法,分散在类中) | 低(注册和处理分离) |
| 灵活性 | 高(可以捕获上下文变量) | 中(需要定义协议) | 高(任意对象都可以监听) |
| 循环引用风险 | 高(需要 weak-strong dance) | 低(delegate 用 weak) | 低(需要移除观察者) |
| 多回调支持 | 可以(多个 block 参数) | 可以(协议多个方法) | 可以(多个通知名) |
| 适用场景 | 简单回调、异步操作、枚举遍历 | 复杂交互、多方法回调、需要返回值 | 全局事件、多对象响应、解耦 |
2. 选型建议
- 简单回调(网络请求、UI 动画完成):优先用 Block,代码聚合,可读性好。
- 复杂交互(多个回调方法、需要返回值、如 UITableViewDataSource):用 Delegate,协议定义清晰,方法分离。
- 全局事件(登录状态变化、键盘弹出、系统事件):用通知,一对多广播,解耦。
- 多个对象需要响应同一事件:用通知。
- 回调需要传递大量上下文变量:用 Block(自动捕获变量)。
十、常见问题排查
问题 1:EXC_BAD_ACCESS 崩溃(Block 调用时)
原因:MRC 下,栈 Block 超出作用域后被销毁,之后调用导致野指针崩溃。
// MRC 下的经典 bug
- (void)method {
int a = 10;
self.block = ^{ // 如果属性是 retain 而不是 copy,block 还是栈上的
NSLog(@"%d", a);
};
}
// 方法结束,栈 block 被销毁
// 之后调用 self.block() → EXC_BAD_ACCESS
// 解决:属性用 copy,或手动 copy
self.block = [^{
NSLog(@"%d", a);
} copy];
ARC 下:基本不会出现这个问题,因为 ARC 会自动 copy。但如果用 __unsafe_unretained 修饰 Block 变量,仍可能崩溃。
问题 2:循环引用导致内存泄漏
现象:页面 pop/dismiss 后,dealloc 不调用,内存不释放。
排查:
- 检查所有 Block 属性,Block 内是否引用了 self。
- 检查 NSTimer 是否 invalidate。
- 用 Xcode Memory Graph 或 Instruments Leaks 检测。
解决:用 __weak typeof(self) weakSelf = self; + __strong typeof(weakSelf) strongSelf = weakSelf;。
问题 3:Block 内修改外部变量不生效
原因:普通自动变量是值捕获,Block 内修改的是副本,外部不变。而且编译器直接禁止修改(报错)。
int a = 10;
void (^block)(void) = ^{
// a = 20; // 编译报错
};
解决:用 __block 修饰变量:
__block int a = 10;
void (^block)(void) = ^{
a = 20; // 可以修改
};
block();
NSLog(@"%d", a); // 20
问题 4:Block 内变量值不是最新的
原因:普通自动变量是值捕获,Block 定义时的值被拷贝,之后外部修改不影响 Block 内。
int a = 10;
void (^block)(void) = ^{
NSLog(@"a = %d", a); // 捕获时的值 10
};
a = 20;
block(); // 输出 a = 10(不是 20)
解决:
- 如果需要最新值,用
__block修饰变量。 - 或用静态变量/全局变量(引用捕获)。
- 或在 Block 调用时再传入参数。
问题 5:Block 作为属性用 strong 还是 copy?
答案:约定俗成用 copy。
- MRC 下必须用 copy。
- ARC 下 strong 和 copy 效果一样,但用 copy 更清晰、更安全(兼容 MRC)。
- 绝对不能用
weak/assign/retain(MRC 下 retain 栈 Block 不会移到堆)。
问题 6:Block 内引用 self 一定循环引用吗?
不一定。判断标准:self 是否长期持有这个 Block?
- self 把 Block 存为属性/成员变量 → 长期持有 → Block 内引用 self → 循环引用。
- Block 作为方法参数临时使用(GCD、UIView 动画、enumerateObjectsUsingBlock)→ 执行完就释放 → 不循环引用。
- Block 内不引用 self → 不循环引用。
十一、Swift 版本对照(闭包)
Swift 中的闭包(Closure)和 OC 的 Block 本质相同,语法更简洁:
1. 基本语法
// 无参数无返回值
let block1: () -> Void = {
print("Hello")
}
// 有参数有返回值
let add: (Int, Int) -> Int = { a, b in
return a + b
}
// 简写(类型推断)
let add2 = { (a: Int, b: Int) -> Int in
return a + b
}
// 调用
block1()
let result = add(10, 20) // 30
2. 变量捕获
var a = 10
let block = {
print(a) // Swift 闭包默认引用捕获(不是值捕获!)
}
a = 20
block() // 输出 20(Swift 是引用捕获,和 OC 的值捕获不同)
注意:Swift 闭包对
var变量是引用捕获(类似 OC 的__block),对let常量是值捕获。这和 OC Block 不同,需要注意。
3. 循环引用
class MyClass {
var block: (() -> Void)?
func test() {
// 用 weak 打破循环引用
block = { [weak self] in
guard let self = self else { return } // 类似 OC 的 strongSelf
self.doSomething()
}
}
func doSomething() {}
}
Swift 用 [weak self] 捕获列表打破循环引用,guard let self = self else { return } 类似 OC 的 weak-strong dance。
4. 闭包作为参数和尾随闭包
// 闭包作为参数
func doSomething(completion: () -> Void) {
completion()
}
// 调用
doSomething(completion: {
print("完成")
})
// 尾随闭包语法(最后一个参数是闭包时可以写在括号外面)
doSomething {
print("完成")
}
// GCD
DispatchQueue.global().async {
// 后台执行
DispatchQueue.main.async {
// 主线程更新 UI
}
}
5. @escaping 和 @nonescaping
// @escaping:闭包在函数返回后才执行(异步回调),需要标记
func request(completion: @escaping (Data) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, _ in
if let data = data {
completion(data) // 异步执行,函数已返回
}
}.resume()
}
// @nonescaping(默认):闭包在函数返回前执行完
func syncProcess(handler: (Int) -> Void) {
handler(10) // 同步执行,函数返回前完成
}
十二、总结
- Block 是什么:封装了代码逻辑和捕获变量的对象,类似闭包/lambda,有 isa 指针,是 OC 对象。
- 三种类型:全局 Block(不捕获外部变量,存在数据段)、栈 Block(捕获外部变量,存在栈上,作用域结束销毁)、堆 Block(对栈 Block copy 后存在堆上,引用计数管理)。
- 底层结构:
Block_layout结构体,包含 isa、flags、reserved、invoke(函数指针)、descriptor(描述符)、捕获变量。调用 Block 本质是调用 invoke 函数指针。 - 变量捕获:自动变量(局部)是值捕获(拷贝值,不能修改);静态变量是引用捕获(拷贝地址,可修改);全局变量不捕获直接访问;self 被捕获(强引用);
__block变量通过 forwarding 指针实现内外共享。 - __block 原理:变量被包装成
Block_byref结构体,有 forwarding 指针。Block copy 到堆时,__block 变量也 copy 到堆,栈上的 forwarding 指向堆上副本,保证内外访问同一个值。ARC 下 __block 对象会被 retain,不能打破循环引用。 - 内存管理:MRC 下属性必须用 copy,手动管理;ARC 下大多数情况自动 copy 到堆,属性用 copy(约定俗成)或 strong 均可。
- 循环引用:self 持有 Block 属性且 Block 内引用 self 时发生。解决用
__weak+__strong(weak-strong dance)。GCD/UIView 动画等临时 Block 不会循环引用。NSTimer + Block 会循环引用,需要 invalidate 或用 weak。 - 使用场景:回调、枚举遍历、排序、GCD 多线程、UIView 动画、通知。
- 与 delegate/通知对比:Block 适合简单回调(代码聚合),delegate 适合复杂多方法交互,通知适合一对多全局事件。
- 常见坑:栈 Block 超作用域崩溃(MRC)、循环引用泄漏、值捕获导致值不更新、__block 在 ARC 下不能打破循环引用。

浙公网安备 33010602011771号