本节将深入探索block的底层原理。
- 循环引用 & 解决方案
- block结构分析
- 源码探索
- block的参数处理(多次拷贝)
1. 循环引用 & 解决方案
-
正常释放:
image.png -
循环引用:
image.png
- 解决循环引用的方法:
weakSelf弱引用self,搭配strongSelf- 使用
__block修饰对象(必须在block中置空对象,且block必须被调用)传对象self作参数,提供给代码块使用
- 测试代码:
typedef void(^HTBlock)(void);
typedef void(^HTBlock2)(ViewController *);
@interface ViewController ()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) HTBlock block;
@property (nonatomic, copy) HTBlock2 block2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.name = @"ht";
// // 循环引用 [self持有block,block持有self]
// self.block = ^(void){
// NSLog(@"%@",self.name);
// };
// self.block();
//
// // 没有循环引用 (UIView的block持有self,self与UIView的block无关)
// [UIView animateWithDuration:1 animations:^{
// NSLog(@"%@",self.name);
// }];
// 方法1: `weakSelf`弱引用`self`,搭配`strongSelf`
// weak不会让self的引用计数+1,所以不影响self的释放。 而strongSelf持有的是weakSelf。
// 当self释放时,如果block执行完了,strongSelf局部变量就会被释放,此时weakSelf也被释放。所以不会造成循环引用
// __weak typeof(self) weakSelf = self;
// self.block = ^{
// __strong typeof(self) strongSelf = weakSelf;
// NSLog(@"%@",strongSelf.name);
// };
// self.block();
// // 方法2:使用`__block`修饰对象(`必须`在block中`置空对象`,且block必须`被调用`)
// __block ViewController * vc = self;
// self.block = ^(void){
// NSLog(@"%@",vc.name);
// vc = nil; // 必须手动释放。 因为vc持有了self,vc不释放,self永远不可能释放。
// };
// self.block(); // 必须调用block。 因为vc持有了self,不过不执行block,vc永远没有nil,self永远不会释放
// 方法3:`传`对象`self`作`参数`,提供`给代码块使用`
// 最佳的使用方式,因为不会影响self的正常释放。(调用时,引用计数会+1,但是调用完后,就会-1,不影响self的生命周期)
self.block2 = ^(ViewController * vc) {
NSLog(@"%@",vc.name);
};
self.block2(self);
}
@end
2. block结构分析
2.1 block的结构
-
main.m文件中添加测试代码:
#import <stdio.h>
int main(int argc, const char * argv[]) {
void(^block)(void) = ^{
printf("HT");
};
block();
return 0;
}
-clang编译main.m文件:
clang -rewrite-objc main.m -o main.cpp
- 打开
main.cpp文件:
image.png - 可以发现
block实际上是一个结构体,所以block支持%@打印。
- 函数声明(创建):
- block是个
结构体,初始化时,存储执行代码块(匿名函数)和基础描述信息。
- 函数调用:
- 调用了
FuncPtr,实际就是__main_block_func_0函数,入参是block自己(这是为了捕获变量)
2.2 block入参分析
2.2.1 直接加入变量(值拷贝)
- 加入
变量int a,重新编译:

- 发现
编译时,就生成了对应的变量。__main_block_func_0匿名函数多了一个局部变量a,这个a是读取了cself的内存值,是值拷贝。是只读变量。
为了证明是
a是值拷贝,只读变量,我们在测试代码的block中添加a++的赋值代码,编译器立马报错提示:(造成了代码歧义)
image.png
2.2.2 __block声明变量(指针拷贝)
将int a使用__block声明:

与上面
直接使用int a不同:
上面传入的是a的值,执行block函数时,是进行值拷贝,只读。__block修饰后,会生成a的结构体对象,传入的是对象指针地址,执行block函数时,是进行指针拷贝,可读可写,通过修改指针指向的内容。完成block的内外通讯。
3. 源码探索
3.1 Block的三种类型
Block有【3种类型】:
-
__NSGlobalBlock__:无入参时,是全局Block -
__NSMallocBlock__:有外部变量时,变成堆区Block -
__NSStackBlock__:有外部变量,使用__weak修饰时,变成栈区Block
- 测试代码:
- (void)demo {
// 【3种block类型】
// 1. __NSGlobalBlock__ (无入参时,是全局Block)
void(^block1)(void) = ^{
NSLog(@"HT_Block");
};
NSLog(@"%@",block1); // 打印: <__NSGlobalBlock__: 0x102239040>
// 2. __NSMallocBlock__ (有外部变量时,变成堆区Block)
int a = 10;
void(^block2)(void) = ^{
NSLog(@"HT_Block %d",a);
};
NSLog(@"%@",block2); // 打印: <__NSMallocBlock__: 0x600000f970c0>
//3. __NSStackBlock__ (有外部变量,使用__weak修饰,变成栈区Block)
int b = 20;
void(^__weak block3)(void) = ^{
NSLog(@"HT_Block %d",b);
};
NSLog(@"%@",block3); // 打印 <__NSStackBlock__: 0x7ffeedae8240>
}

- 为弄清楚
底层原理,首先得确定block源码在哪个库中:
在
block创建前,加入断点:image.png
- 打开
汇编模式:image.png
运行
代码,加入objc_retainBlock符号断点:
image.png继续运行,加入
_Block_copy符号断点:
image.png发现
Block的相关操作,是在libsystem_blocks.dylib库中:image.png
👉 源码地址 ,搜索
libclosure-74,点击右边下载按钮。
- 查看源码,可以关注到
block的结构类型为Block_layout:(后面会明白为什么是Block_layout)
image.png
Flag标识
image.png
- 第1 位,
释放标记,一般常用BLOCK_NEEDS_FREE做& 位与操作,一同传入Flags,告知该block可释放。- 第2-16位,存储
引用计数的值;是一个可选用参数 (0xfffe二进制为1111 1111 1111 1110)- 第25位,
低16位是否有效的标志,程序根据它来决定是否增加或是减少引用计数位的值;- 第26位,是否拥有
拷贝辅助函数(是否调用_Block_call_copy_helper函数); 决定是否有block_description_2- 第27位,是否拥有
block 析构函数;- 第28位,标志是否有
垃圾回收; //OS X- 第29位,标志是否是
全局block;- 第30位,与
BLOCK_HAS_SIGNATURE 相对,判断是否当前block拥有一个签名。用于runtime时动态调用- 第31位,是否有
签名- 第32位,标志是否有
Layout,使用有拓展,决定block_description_3
- Block基础结构梳理图:

- 下面,我们通过
案例来分析和验证上面结构图
3.2 Block的类型转变
- 我们从
最简单的无入参、无返参的全局block开始分析:
- (void)demo {
void(^block)(void) = ^{
NSLog(@"HT_Block");
};
block();
NSLog(@"%@",block);
}
-
为了便于
寄存器的读取操作,我这里使用真机进行演示,在void(^block1)(void) = ^{这一行加入断点,打开汇编模式,运行代码至断点处:
image.png -
当前读取的是
全局Block
image.png 我们给
block添加入参,让block捕获外界变量:
- (void)demo {
NSArray * arr = @[@"1"];
void(^block)(void) = ^{
NSLog(@"HT_Block %@",arr); // 捕获外界变量:arr
};
block();
NSLog(@"%@",block);
}
-
断点位置不变,运行代码,进入汇编页面后,断点在objc_retainBlock这行,打印x0(当前对象):
image.png
Q: 按照
上面打印结果来说,有外界变量,没有修饰符时,应该是堆区Block,为什么这里是栈区Block?
A:Block从栈区拷贝到堆区,是_Block_copy操作的。(我们往下看)
-
往下验证,加入
objc_retainBlock符号断点,往下执行进入objc_retainBlock函数内部:
image.png -
再次读取
x0,发现此时还没变。我们继续control+ 鼠标左键,点击进入按钮。不断使用register read x0和p来读取、打印当前对象:
image.png -
发现进入
_Block_copy时是栈区Block,但是return出来时,却变成了堆区Block:
image.png 所以
block真正从栈区拷贝到堆区,是_Block_copy进行的。
回顾上面
Block基础结构梳理图,我们可以通过Block对象的首地址进行内存平移获取到invoke的。
-
通过
Block对象的首地址偏移,取到了_block_invoke:
image.png -
control + 鼠标左键,点击进入,发现_block_invoke内部就是block的函数执行内容。
image.png
【深入探究】
- 既然我们可以通过
内存平移,取到invoke函数。那顺便可以验证上面Block基础结构梳理图的完整结构:- 在
objc_retainBlock之后,加断点,此时完成了_Block_copy操作,完成了栈区到堆区的block拷贝:
image.png对照右边图,慢慢看吧 😂 整个结构非常清晰。
image.pngblock的类型是@?,签名中包含参数个数,入参和返参的具体内容和占用内存大小。
3.3 _Block_copy
- 进入
源码。搜索_Block_copy:
// Copy, or bump refcount, of a block. If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
// block都是`Block_layout`类型
struct Block_layout *aBlock;
// 没有内容,直接返回空
if (!arg) return NULL;
// The following would be better done as a switch statement
// 将内容转变为`Block_layout`结构体格式
aBlock = (struct Block_layout *)arg;
// 检查是否需要释放
if (aBlock->flags & BLOCK_NEEDS_FREE) {
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 如果是全局Block,直接返回
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
//
else {
// Its a stack block. Make a copy.
// 进入的是栈区block,拷贝一份
// 开辟一个大小空间的result对象
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
// 开辟失败,就返回
if (!result) return NULL;
// 内存拷贝:将aBlock内容拷贝到result中
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
//result的invoke指向aBlock的invoke。
result->invoke = aBlock->invoke;
#endif
// reset refcount
// BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING :前16位都为1
// ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING):前16位都为0
// 与操作,结果为前16位都为0 应用计数为0
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
// 设置为需要释放,引用计数为1
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
// 生成desc,并记录了result和aBlock
_Block_call_copy_helper(result, aBlock); //
// Set isa last so memory analysis tools see a fully-initialized object.
// 设置isa为堆区Block
result->isa = _NSConcreteMallocBlock;
return result;
}
}
- 如果block
需要释放(表示已经在堆区),就增加引用计数- 如果是
全局Block,直接返回Block_layout结构的aBlock- 其他情况,都是从
栈区拷贝到堆区:
malloc申请空间->memove内存拷贝->invoke指针拷贝->
flag引用计数设为1->生成desc-> 设置isa为堆Block-> 返回堆区Block
4. block的参数处理(多次拷贝)
Q:
block对外界捕获变量怎么管理的?有哪些操作?
- 以
__block修饰的NSArray为例,测试代码如下:
- (void)demo {
__block NSArray * arr = @[@"1"];
void(^block)(void) = ^{
NSLog(@"HT_Block %@",arr); // 捕获外界变量:arr
};
block();
NSLog(@"%@",block);
}
- 使用
clang将ViewController.m文件生成ViewController.cpp文件,分析:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

- 进入
源码,搜索_Block_object_assign:

不同
枚举类型和组合,有不同的引用方式,其中最复杂的,是_Block_copy和_Block_byref_copy。-
_Block_copy上面已分析过了,我们搜索_Block_byref_copy:
image.png
重点:
此处有
2次拷贝+更深层次拷贝:
- 【第一次拷贝】:
Block自身(拷贝一份到)->Block_byref结构体(src栈区结构体 )- 【第二次拷贝】:
src栈区结构体 (拷贝一份到)->copy堆区结构体- 【更深次拷贝】:调用
byref_keep方法,内部又执行_Block_object_assign函数,再判断是否继续往下拷贝嵌套
- 以上,就是
Block的创建、调用。Block的释放调用_Block_object_dispose函数:
image.png
关于Block的探索,都在这里了。有兴趣可以一个个类型去测试和监测






















