文章目录
前言
最近在复习OC知识,发现自己对于block的了解很少,很多东西当时都没有搞明白或者甚至不知道,然后就对block进行了重新学习。
Block的声明和创建
Block 的语法格式为:
返回值类型 (^Block名称)(参数列表) = ^返回值类型(参数列表) { 代码块 };
示例:
// 声明并创建一个无参数、无返回值的 Block
void (^myBlock)(void) = ^void(void) {
NSLog(@"Block 执行");
};
// 调用 Block
myBlock(); // 输出:"Block 执行"
问题引入
由此,我们有以下代码,后面所有的实例分析都是在此基础上:
运行结果:
我们可以看到,上述代码进行了两次block捕获,第一次捕获A和B的初始值,A为3,B为7;然后在myblock函数外对A、B变量进行自增修改处理,再次调用myblock函数进行捕获,变量A输出不变为3,变量B输出为自增后的值8,控制变量可以发现,造成这种结果,唯一不同的是我们在定义变量B时,在前面用了==__block==进行修饰。
由此我们产生了 一系列问题:block内部是什么结构?为什么在block外部修改变量不会影响到其在block内的捕获?为什么声明时用__block进行修饰后,外部修改变量block捕获也会变?可不可以直接在block内部进行变量修改?
下面,我们来对这些问题进行一一探讨。
Block的底层结构
我们用clang -rewrite-objc main.m -o main.cpp
将上述代码文件转换成C++文件:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int A = 3;
__attribute__((__blocks__(byref))) __Block_byref_B_0 B = {(void*)0,(__Block_byref_B_0 *)&B, 0, sizeof(__Block_byref_B_0), 7};
void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, A, (__Block_byref_B_0 *)&B, 570425344));
((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
A++;
(B.__forwarding->B)++;z
((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
}
return 0;
}
我们可以看到,我们定义myblock函数的代码在C++中是这样的:
上述代码Block底层实现的编译器生成代码,代码的核心是将一个 Block 的底层结构体指针转换为函数指针,并赋值给 myblock变量,具体形式:
void(*myblock)(void) = ((void (*)())&__main_block_impl_0(...));
等号左边等名义个函数指针myblock,类型为void(*)(void)
(无参数、无返回值);
等号右边通过调用__main_block_impl_0
函数生成 Block 的底层结构体,并将其转换为函数指针后赋值给 myblock。
__main_block_impl_0
是编译器自动生成的 Block 初始化函数,负责创建 Block 的底层结构体(如 struct __block_impl
)并初始化其核心字段。其参数通常包括:
参数 | 类型/含义 |
---|---|
(void *)__main_block_func_0 | 指向 Block 实际执行逻辑的函数指针(即 FuncPtr字段的内容) |
&__main_block_desc_0_DATA | 指向 Block 描述符(struct __block_descriptor)的指针,包含 Block 的元数据(如大小、复制/销毁函数) |
A | 被 Block 捕获的变量(如基本类型、不可变对象) |
(__Block_byref_B_0 *)&B | 指向 __Block_byref_B_0结构体的指针,用于捕获可变对象或需要引用捕获的变量 |
570425344 | 标志位(可能表示 Block 的行为选项,如是否复制捕获变量、是否启用优化等) |
从刚刚我们在main.m的cpp文件里看到的定义函数,我们继续探究找寻__main_block_impl_0函数的源码定义:
struct __main_block_impl_0
包含以下核心成员:
成员 | 类型/含义 |
---|---|
impl | struct __block_impl类型,Block 的核心实现结构体(封装函数指针、类型标识、标志位等)。 |
Desc | struct __main_block_desc_0*类型,指向 Block 描述符(存储元数据,如复制/销毁函数)。 |
A | int类型,捕获的整型变量(值捕获)。 |
B | __Block_byref_B_0*类型,指向引用捕获的可变对象的辅助结构体(引用捕获)。 |
构造函数 | 初始化各成员,绑定 Block 的执行逻辑、捕获变量和元数据。 |
然后我们顺藤摸瓜找到了Block 的核心实现结构体struct __block_impl函数的源码:
由上述过程,我们可以知道,Block的底层本质是一个结构体,内部封装了一些关键信息:
函数指针:指向Block对应的可执行代码
捕获的变量:Block执行时需要访问的外部变量()
执行上下文:包括 Block 的引用计数、所属的类(用于调试)等元数据
有一张图非常好地说明了block的底层调用和逻辑:
——图片来自博客【iOS】Block底层分析@zhngxvy
Block的执行流程
Block 的执行分为创建、存储、传递和调用四个阶段,核心是函数指针的调用和捕获变量的管理。
Block的创建与存储
创建:通过 ^
语法定义 Block 时,编译器会生成一个 struct Block_layout
结构体实例,并将代码块编译为对应的机器指令(存储在invoke函数指针中)。
存储位置:Block 初始存储在栈上(栈 Block),但以下场景会触发 Block 被复制到堆上(堆 Block):
- Block 被赋值给一个强引用的变量(如
__strong
修饰的属性或局部变量)。 - Block 被作为参数传递给一个异步函数(如
dispatch_async
)。 - Block 被显式复制(调用
Block_copy()
)。
Block的传递与调用
Block 可以像对象一样被传递(如作为方法参数、存储在集合中),其调用的本质是执行 invoke函数指针,并传递参数。
示例(Block 作为方法参数):
// 定义一个接受 Block 的方法
- (void)executeBlock:(void (^)(void))block {
NSLog(@"准备执行 Block...");
block(); // 调用 Block(执行 invoke 函数)
NSLog(@"Block 执行完成");
}
// 使用
[self executeBlock:^{
NSLog(@"自定义 Block 逻辑");
}];
执行流程:
- 定义 Block 时,编译器生成栈上的
struct Block_layout
。 - 将 Block 作为参数传递给
executeBlock:
方法时,Block 被复制到堆上(因方法参数需要强引用)。 - 方法内部调用
block()
时,触发invoke函数指针,执行 Block 的代码逻辑。
Block的捕获机制
Block 可以捕获外部作用域的变量(如局部变量、实例变量),捕获行为分为两种:值捕获和指针捕获。
捕获局部变量
auto:自动变量,离开作用域就自动销毁,只存在于局部变量(没有特别关键字修饰,一般默认为auto)
static:静态局部变量
我们先来看以下代码:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int A = 18;//局部变量(自动变量)
static int B = 52;//静态局部变量
void(^jubuBlock)(void) = ^{
NSLog(@"jubuBlock - A:%d - B:%d", A, B);
};
jubuBlock();
NSLog(@"输出1 - A:%d B:%d", A, B);
A = 20;
B = 100;
jubuBlock();
NSLog(@"输出2 - A:%d B:%d", A, B);
}
return 0;
}
输出结果如下:
由此,我们可以发现,当我们在block函数外部修改局部变量时,block函数内部的自动变量不会受影响,而静态局部变量会跟着被修改。这是为什么呢?
为了探寻这一原因,我们将上述代码文件转化为cpp文件,我们可以发现__main_block_impl_0
结构体定义如下:
我们可以得到以下结论:
- A:捕获的自动变量(按值复制)。
- B:捕获的静态局部变量(按指针复制,存储其内存地址)。
这就可以解释刚刚的问题:为什么在block函数外部修改自动变量,内部的变量值不会产生改变,而修改静态变量就会变?到这里,block捕获局部变量的本质就很明显了:
自动变量:按值捕获(复制其当前值到block内部)。即使外部变量后续被修改,block内部使用的仍是捕获时的副本。
静态局部变量:按指针捕获(存储其内存地址)。外部变量修改时,block通过指针访问的是最新值。
捕获全局变量
首先,有以下代码:
#import <Foundation/Foundation.h>
int A = 18;//全局变量
static int B = 52;//静态全局变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^quanjuBlock)(void) = ^{
NSLog(@"quanjuBlock - A:%d - B:%d", A, B);
};
quanjuBlock();
NSLog(@"输出1 - A:%d B:%d", A, B);
A = 20;
B = 100;
quanjuBlock();
NSLog(@"输出2 - A:%d B:%d", A, B);
}
return 0;
}
输出结果:
我们再来看这部分源码:
我们发现,这部分的源码跟上面局部变量有点出入,在全局变量一直在__main_block_impl_0
结构体定义中并没有我们声明的全局变量A和B,即全局、全局静态变量并没有出现在我们的Block实现结构体中,说明二者无法被捕获。而全局变量存储在内存中,打印的一直是最新的值。
小结
自动变量(局部变量) | 静态局部变量 | 全局变量 | |
---|---|---|---|
存储区域 | 栈(Stack) | 全局数据区(Data Segment) | 全局数据区(Data Segment) |
生命周期 | 随作用域结束(如函数返回)销毁 | 程序启动时创建,程序结束时销毁 | 程序启动时创建,程序结束时销毁 |
默认捕获机制 | 按值拷贝(复制当前值到block内部) | 按指针拷贝(存储变量内存地址) | 按指针拷贝(存储变量内存地址) |
block内访问形式 | 直接使用拷贝后的副本(独立于原变量) | 通过指针解引用访问原变量 | 通过指针解引用访问原变量 |
外部修改的影响 | 不影响block内部的副本(block捕获的是历史值) | 影响block内部(指针指向的原变量被修改) | 影响block内部(指针指向的原变量被修改) |
是否支持修改捕获值 | 默认不可直接修改(需__block 修饰) |
可直接修改(通过指针操作原变量) | 可直接修改(通过指针操作原变量) |
产生这种差异的原因:
auto和static:因为作用域的问题,自动变量的内存随时可能被销毁,所以要捕获就赶紧把它的值拿进来,防止调用的时候访问不到。
静态局部变量:存储在全局数据区,生命周期长于block,因此block捕获其指针后,即使原变量所在作用域销毁(如函数返回),仍可通过指针访问最新值。
注意:尽管静态局部变量的生命周期很长,但其**作用域(可访问范围)**仅限于声明它的函数内部:
- 函数外部无法直接访问该变量(即使通过指针或引用)。
- 不同函数中声明的同名静态局部变量相互独立(因为各自存储在全局数据区的不同位置)。
全局变量:在Block中访问局部变量相当于是跨函数访问,要先将变量存储在Block里(捕获),使用的时候再从Block中取出,而全局变量是直接访问。
tips:
自动变量按值捕获是安全的(避免悬垂指针),但可能增加内存拷贝开销(大对象需注意)。
静态/全局变量按指针捕获更高效(无拷贝),但需注意多线程并发修改时的线程安全问题。
Block的类型
上述说到Block含有isa指针,而OC对象的isa指针指向它的类型,那么Block的类型都有哪些呢?
首先有以下代码:
void (^block)(void) = ^{
NSLog(@"hello world!");
};
NSLog(@"%@ %@", block, [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
运行后结果如下:
我们可以发现这个block的类型是NSGlobalBlock,其父类是NSBlock,根父类是NSObject,这也能说明Block是一个OC对象。
除了NSGlobalBlock,Block还有哪些类型,我们先来看如下代码:
void (^block1)(void) = ^{
NSLog(@"hello world!");
};
NSLog(@"%@ %@", block1, [block1 class]);
int A = 3;
void (^block2)(void) = ^{
NSLog(@"%d", A);
};
NSLog(@"%@ %@", block2, [block2 class]);
NSString *string = @"hello world!";
void (^block3)(void) = ^{
NSString *stringCopy = [string copy];
};
NSLog(@"%@ %@", block3, [block3 class]);
当我们在ARC环境下运行代码后,有如下结果:
随后,我们在项目的Bulid Settings中关闭ARC:
随即得到如下结果:
我们可以发现,在MRC环境下,若block不捕获任何外部变量,且未被复制,则其类型为__NSGlobalBlock__
;若block捕获外部变量,但未被复制,则其类型为__NSStackBlock__
;若block被复制,则其类型为__NSMallocBlock__
。
但如果是在ARC环境下,即使不显式调用copy也会是__NSMallocBlock__
类型,这是因为在ARC下,编译器会自动插入内存管理代码(如retain
、release
、copy
),核心目的是确保block在需要跨作用域存活时保持有效,确保对象(包括block)在其生命周期内被正确管理。
所以,我们可以做出如下总结:
存储位置 | 触发条件 | 生命周期 |
---|---|---|
栈上 | block未被复制(仅在局部作用域使用,未被赋值给id类型变量或作为参数传递) | 离开作用域时自动销毁(无法跨作用域使用) |
堆上 | block被复制(显式调用copy,或隐式被id类型变量持有、作为方法参数传递等)。 | 由引用计数管理,直到release计数归零后销毁。 |
全局区 | block不捕获任何外部变量,且未被复制。 | 程序启动到结束时一直存在(全局唯一)。 |
__block修饰符
在我们前面的学习中,我们知道在block函数外部修改自动变量,函数内部捕获的值不会受到影响,那如果我们想修改在block中捕获的自动了变量的值怎么办?
首先,我们肯定想到说,想修改?那我直接在函数内部进行修改行不行!我们试一下:
我们会发现,在block函数内部直接修改自动变量会发生报错:Variable is not assignable (missing __block type specifier)
,翻译过来就是:变量未分配(丢失 __block 类型限定符)。
这是因为自动变量的默认捕获机制:在OC中,block对自动变量的默认捕获方式是按值复制Copy by Value):
- 当block定义时,会复制自动变量的当前值到block内部的副本中。
- block内部访问该变量时,实际访问的是副本,而非原变量。
- 因此,若在block内部尝试修改该变量(如赋值操作),编译器会报错:因为修改的是副本,原变量无法被block直接修改,这是不允许的。
那么我们真的没有办法修改自动变量了吗😭有的兄弟有的!
如果我们需要在block内部修改自动变量,需用__block
修饰符声明该变量。好的,我们现在进行实际操作试试😋:
OK!对自由变量A加了__block修饰后就可以在block函数内部进行修改了✅
嘶~这个__block到底有什么魔力?下面我们来探索一下。
先说结论:__block
的作用是将自动变量转换为块级变量(Block Variable),使其被block和原作用域共享同一实例。
__block变量的包装结构体
现在我们来看看__block这部分的cpp源码:
我们发现,被__block修饰的自动变量A在源码中,编译器生成了一个包装结构体__Block_byref_A_0
,用于存储被修饰变量的值及其元数据。
其成员含义:
成员 | 类型 | 说明 |
---|---|---|
__isa | void* | 指向类对象的指针(通常为_NSConcreteStackBlock或堆上的类,初始可能为NULL)。 |
__forwarding | __Block_byref_A_0* | 转发指针,指向变量的实际存储位置(栈或堆)。当block被拷贝到堆时,此指针会指向堆中的副本。 |
__flags | int | 标志位(如是否被拷贝、是否需要释放等)。 |
__size | int | 结构体的大小(用于内存管理)。 |
A | int | 被__block修饰的原始变量 |
通过此结构体,block和原作用域的变量共享同一内存空间,实现“跨作用域修改变量”的能力。
block的实例结构体
block的实例结构体( __main_block_impl_0)源码如下:
block的实例通过此结构体表示,包含block的执行逻辑、描述信息及对__block
变量的引用:
结构体成员 | 类型 | 说明 |
---|---|---|
impl | struct __block_impl | block的基类结构体,包含isa(类型标识)、Flags(标志位)、FuncPtr(执行函数指针) |
Desc | struct __main_block_desc_0* | 指向block描述符的指针,记录block的内存布局和回调函数(如拷贝、销毁) |
A | __Block_byref_A_0* | 指向__block变量包装结构体的指针(通过引用捕获,而非值拷贝) |
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_A_0 *_A, int flags=0)
: A(_A->__forwarding) { // 初始化时,A指向包装结构体的转发指针
impl.isa = &_NSConcreteStackBlock; // 标记为栈上block
impl.Flags = flags; // 设置标志位(如是否需要拷贝)
impl.FuncPtr = fp; // 绑定执行函数(如__main_block_func_0)
Desc = desc; // 绑定描述符
}
A(_A->__forwarding)表示block实例的A成员直接指向__Block_byref_A_0
结构体的__forwarding
指针。这确保了无论block是否被拷贝到堆,A始终指向变量的实际存储位置(栈或堆)。
block的执行逻辑
block的实际执行体(__main_block_func_0:),其源码如下:
通过__cself(block实例自身)访问捕获的变量。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_A_0 *A = __cself->A; // 获取包装结构体指针
(A->__forwarding->A) = 19; // 修改被包装的变量A的值(通过转发指针)
NSLog((NSString *)&__NSConstantStringImpl__... , (A->__forwarding->A)); // 打印修改后的值
}
通过A->__forwarding->A
访问被__block修饰的变量A。__forwarding
指针确保即使block被拷贝到堆,仍能正确访问变量的实际存储位置(栈或堆副本)。
Block循环引用
Block的本质是一个对象(继承自NSObject),其内存管理遵循ARC规则。当Block捕获外部对象(如self)时:
默认情况下,Block会强引用捕获的对象(对对象类型变量按引用捕获,基本类型按值捕获)。
如果外部对象(如self)本身强引用该Block(例如将Block作为self的属性),会形成强引用闭环:
self → Block(强引用) → self(强引用)
此时两者的引用计数均无法归零,导致内存泄漏。
造成的原因
Block作为对象的属性,且内部捕获self
当对象的属性是Block,且Block内部访问了self
(如调用self
的方法或属性),同时对象强引用该Block时,形成循环引用。
// ViewController.h
@interface ViewController : UIViewController
@property (nonatomic, strong) void (^myBlock)(void); // Block属性(强引用)
@end
// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 创建Block,内部捕获self(强引用)
self.myBlock = ^{
NSLog(@"Self: %@", self); // 捕获self(强引用)
};
// 2. self强引用myBlock(属性是strong)
// 此时形成循环引用:self → myBlock → self
}
@end
循环路径:
self(ViewController实例)通过strong属性强引用myBlock→ myBlock通过默认的强引用捕获self→ 两者引用计数均无法归零。
Block被嵌套对象持有,且捕获外层self
当Block被另一个对象(如manager)持有,而self又持有该manager,同时Block内部捕获self时,也可能形成循环。
// Manager.h
@interface Manager : NSObject
@property (nonatomic, copy) void (^block)(void);
@end
// ViewController.m
@implementation ViewController {
Manager *_manager;
}
- (void)viewDidLoad {
[super viewDidLoad];
_manager = [[Manager alloc] init];
// 1. Manager持有block(copy后为强引用)
_manager.block = ^{
NSLog(@"ViewController: %@", self); // Block捕获self(强引用)
};
// 2. self持有_manager(强引用)
// 循环路径:self → _manager → block → self
}
@end
解决方法
解决循环引用的核心是打破强引用闭环,通常通过==弱引用(__weak
或__unsafe_unretained
)==弱化Block对self的引用
使用__weak修饰self
在Block内部通过__weak
修饰的weakSelf引用self,避免Block强引用self。
// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 定义弱引用指向self(打破循环)
__weak typeof(self) weakSelf = self;
// 2. Block捕获weakSelf(弱引用)
self.myBlock = ^{
// 使用weakSelf替代self(可能为nil)
NSLog(@"Self: %@", weakSelf);
};
// 3. self仍强引用myBlock,但myBlock弱引用self → 无闭环
}
@end
__weak typeof(self) weakSelf = self
创建了一个弱引用weakSelf,其引用计数不增加,Block捕获weakSelf(弱引用),因此self的引用计数不会因Block的持有而增加。
循环路径被打破:self → myBlock → weakSelf(弱引用,不增加计数)。
使用__unsafe_unretained
(不推荐)
__unsafe_unretained
与__weak
类似,但不会自动将失效的指针置为nil,可能导致野指针(访问已释放的对象)。
__unsafe_unretained typeof(self) unsafeSelf = self;
self.myBlock = ^{
NSLog(@"Self: %@", unsafeSelf); // 若self已释放,unsafeSelf可能为野指针
};
__unsafe_unretained
仅在以下场景使用:目标对象生命周期明确短于Block(无需置nil)或无法使用__weak
(如兼容旧版本iOS)。
在Block执行完毕前确保self存活
若Block需要长时间执行(如异步任务),可通过临时强引用确保self在Block执行期间不被释放,执行完毕后自动释放。
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf; // 临时强引用
if (strongSelf) {
[strongSelf doSomething]; // 在Block执行期间,strongSelf保持self存活
}
}; // strongSelf在此处销毁,不影响self的引用计数
__strong typeof(weakSelf) strongSelf = weakSelf
在Block内部创建一个临时强引用strongSelf,若weakSelf未失效(self仍存活),strongSelf会强引用self,确保Block执行期间self不被释放,Block执行完毕后,strongSelf销毁,self的引用计数恢复正常。
小结
所以,我们可以总结:Block循环引用的核心是强引用闭环,解决方案的关键是弱化其中一个环节的引用。最常用的方法是使用__weak修饰self,在Block内部通过弱引用访问self,打破循环。同时需注意:
- 避免在Block内部直接强引用self(默认行为)。
- 若需在Block执行期间确保self存活,可结合临时强引用(__strong)。
__unsafe_unretained
需谨慎使用,防止野指针崩溃。
总结
Block是Objective-C中强大的闭包工具,在iOS开发中十分重要,常用于处理异步操作、回调、集合操作等,核心能力是捕获并保存环境状态,支持灵活的异步编程和回调逻辑。理解其存储位置(栈/堆/全局)、变量捕获规则(值/指针/引用)、内存管理(ARC/MRC)及循环引用解决方案是掌握Block的关键。