block的使用
基本使用
objectivec
^{
NSLog(@"This is a block");
}();// 使用括号表示调用Block的方法
void (^block1)(void) = ^ {
NSLog(@"This is another block");
};
block1();
int age = 100;
void (^block2)(void) = ^ {
NSLog(@"This is block with age %d", age);
};
block2();
struct __block_impl {
void *isa; // isa 指针,表明 Block 是一个 OC 对象
int Flags; // 标志位
int Reserved; // 保留字段
void (*FuncPtr)(void *); // 函数指针,指向 Block 内部的具体执行代码
};
// 1. 将 block2 强转为底层基础结构体指针
struct __block_impl *implPtr = (__bridge struct __block_impl *)block2;
// 2. 通过函数指针调用,并将 block2 自身作为参数传入
// 底层等价于:block2->FuncPtr(block2);
(implPtr->FuncPtr)(implPtr);
NSLog(@"%@", [block1 class]);
NSLog(@"%@", [block2 class]);
Block 可以捕获其所在作用域的外部变量。默认情况下,局部变量以值拷贝(只读) 的方式被捕获;如果需要修改外部局部变量,必须使用 __block 修饰符,此时 Block 捕获的是变量的指针(地址)
Block 会对其捕获的 OC 对象进行强引用(Strong Reference) 。如果对象本身也强持有了这个 Block(例如作为属性),就会形成 self -> block -> self 的闭环,导致内存泄漏。
标准解法(Weak-Strong Dance)
ini
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething];
}
};
底层原理
Block 并非单纯的函数指针,在底层,它是一个继承了 NSObject 的 OC 对象。编译器(Clang)会将 Block 转换为一个特殊的 C 语言结构体。
arduino
struct __block_impl {
void *isa; // isa 指针,表明 Block 是一个 OC 对象
int Flags; // 标志位
int Reserved; // 保留字段
void *FuncPtr; // 函数指针,指向 Block 内部的具体执行代码
};
// 编译器自动生成的自定义 Block 结构体
struct __main_block_impl_0 {
struct __block_impl impl; // 内嵌基础结构体
struct __main_block_desc_0* Desc; // 描述信息(包含 copy/destroy 函数指针)
int capturedVar; // 捕获的外部变量
};
Block 的三种类型
根据存储位置和 isa 指针的指向,Block 分为三种类型:
__NSGlobalBlock__(全局 Block) :没有捕获任何外部变量。它存储在程序的数据段,生命周期与程序一致,无需内存管理。__NSStackBlock__(栈 Block) :捕获了外部变量,且仅在当前作用域内使用。它存储在栈区,作用域结束后会被销毁。__NSMallocBlock__(堆 Block) :捕获了外部变量,并且被赋值给了强引用属性、作为返回值传出,或被传入 GCD 等异步队列。此时,Runtime 会将其从栈区 Copy 到堆区,交由 ARC 进行引用计数管理。
scss
^{
NSLog(@"This is a block");
}();// 使用括号表示调用Block的方法
void (^block1)(void) = ^ {
NSLog(@"This is another block");
};
block1();
int age = 100;
void (^block2)(void) = ^ {
NSLog(@"This is block with age %d", age);
};
block2();
__weak void (^block3)(void) = ^ {
NSLog(@"This is weak block with age %d", age);
};
block3();
NSLog(@"%@", [block1 class]);//__NSGlobalBlock__
NSLog(@"%@", [block2 class]);//__NSMallocBlock__
NSLog(@"%@", [block3 class]);//__NSStackBlock__
变量捕获机制
- 普通局部变量:Block 在创建时,会将变量的值复制一份到结构体内部。因此,Block 内部修改不会影响外部,外部修改也不会影响 Block 内部。
__block修饰的变量 :编译器会引入一个名为__Block_byref_xxx的新结构体,将变量包装在其中。Block 结构和外部作用域都持有这个结构体的指针,从而实现了跨作用域的共享与修改513。- OC 对象 :当栈 Block 被 Copy 到堆区时,底层会自动对捕获的 OC 对象执行
retain操作(强引用);当堆 Block 被销毁时,会执行release操作。这就是产生循环引用的根本原因。
特别强调:在clang3.9之前,捕获局部的static会引用指针,新版本会将局部的static强行提升到全局的static
objectivec
int global_a = 1000;
static int global_b = 2000;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
struct Block_descriptor {
unsigned long reserved;
unsigned long size;
// 还有可选的 copy/dispose 函数指针,ARC下会有
};
struct Block_impl {
void *isa;
int flags;
int reserved;
void *invoke;
struct Block_descriptor *descriptor; // ✅ descriptor 在结构体内部!
};
// 捕获局部变量
int age = 100;
void (^block1)(void) = ^ {
NSLog(@"This is block with age %d", age);
};
block1();
struct Block1_impl {
struct Block_impl impl; // 函数指针,指向 Block 内部的具体执行代码
//这里捕获了值
int age;
};
struct Block1_impl *block1_pointer =(__bridge struct Block1_impl *) block1;
NSLog(@"block1的值是%d",block1_pointer->age);
// 捕获局部static变量
static int age1 = 100;
void (^block2)(void) = ^ {
NSLog(@"This is block with age1 %d", age1);
};
age1 = 200;
struct Block2_impl {
struct Block_impl impl; // 函数指针,指向 Block 内部的具体执行代码
int *age1;
};
struct Block2_impl *block2_pointer =(__bridge struct Block2_impl *) block2;
NSLog(@"block1的值是%ld",block1_pointer->impl.descriptor->size); // 36
// 因为block1捕获了age1多占了4字节,这里少了4个字节,说明没有捕获任何变量
NSLog(@"block2的值是%ld",block2_pointer->impl.descriptor->size); // 32
// 全局
void (^block3)(void) = ^ {
NSLog(@"This is block with global data a %d b %d", global_a, global_b);
};
struct Block2_impl *block3_pointer =(__bridge struct Block2_impl *) block3;
NSLog(@"block3的值是%ld",block3_pointer->impl.descriptor->size); // 32
}
block与__forwarding
__block修饰的变量,在内部会有一个forwarding。当我们在代码中使用 __block 修饰一个变量时,编译器会将其包装成一个结构体对象(例如 __Block_byref_age_0)。
- 初始状态 :这个结构体默认是创建在栈区 的。此时,结构体内部的
__forwarding指针指向它自己。 - 拷贝状态 :当 Block 被 copy 到堆区 时,这个
__block结构体也会被 copy 一份到堆上。此时,
block和循环引用
1. 循环引用的本质
循环引用的本质是 "相互强引用" 。当一个对象(如 ViewController)强引用了一个 Block 属性,同时该 Block 内部又直接使用了 self(Block 会强捕获 self),就会形成 self → Block → self 的闭环。
由于 ARC(自动引用计数)机制下,只要引用计数大于 0 对象就不会被释放,这种闭环会导致两者的引用计数永远无法归零,最终造成内存泄漏(即 dealloc 方法永远不会被调用)。
2. 常见的循环引用场景
- Block 作为属性 :在 ViewController 中定义一个
strong或copy修饰的 Block 属性,且在 Block 内部直接访问self的属性或方法。 - NSTimer 与 GCD 回调 :使用
NSTimer或dispatch_after等 API 时,如果 Block 捕获了self,且定时器未被正确销毁(如未调用invalidate),也会导致循环引用14。 - Delegate 与父子对象 :虽然不全是 Block 的问题,但在对象层次结构中(如父对象强引用子对象,子对象又强引用父对象),或者 delegate 属性错误使用了
strong修饰,同样会引发循环引用3。
3. 核心解决方案:打破强引用闭环
要解决 Block 引起的循环引用,最常用的手段是使用弱引用(__weak)来打破闭环。以下是三种常见的实战方案:
方案一:使用 __weak 修饰 self(最常用)
在 Block 外部定义一个 __weak 修饰的 weakSelf,Block 内部使用它。弱引用不会增加对象的引用计数,当对象被释放时,weakSelf 会自动置为 nil。
如果 Block 内部包含异步操作(如网络请求、延迟执行),为了防止 weakSelf 在异步执行期间被意外释放,还需要在 Block 内部使用 __strong 将其临时提升为强引用(即经典的 weak-strong dance)
ini
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
// 异步操作前,强引用 weakSelf,避免 self 在 block 执行中被释放
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return; // 防止 self 已释放
[strongSelf doSomething];
};
方案二:使用 __block 修饰并手动置 nil
在 ARC 环境下,如果用 __block 修饰 self,Block 捕获该变量时会变为强引用。此时必须在 Block 执行完毕后,手动将该变量置为 nil 来打破循环。
ini
__block typeof(self) blockSelf = self;
self.myBlock = ^{
[blockSelf doSomething];
blockSelf = nil; // 手动置 nil,打破循环引用
};
self.myBlock(); // 必须确保 Block 被执行,否则依然会内存泄漏
方案三:将 self 作为 Block 的参数传递
将 self 作为参数传入 Block,Block 内部通过参数访问对象,从而避免捕获外部的 self。这种方式虽然能避免循环引用,但会破坏 Block 调用的简洁性,通常仅在简单场景下使用。
4. 最佳实践与排查工具
- 规范 Delegate 声明 :所有的 delegate 属性必须声明为
weak,避免父子对象相互强引用3。 - 善用调试工具 :在 Xcode 中可以使用 Memory Graph Debugger 可视化检查对象之间的引用关系,或者运行 Instruments 的 Leaks 工具进行内存泄漏测试。
- ARC 的局限性:虽然 ARC 极大简化了内存管理,但它无法自动处理循环引用,开发者仍需主动管理引用关系。