OC底层原理 学习大纲
本节将深入探索block
的底层原理
。
- 循环引用 & 解决方案
- block结构分析
- 源码探索
- block的参数处理(多次拷贝)
1. 循环引用 & 解决方案
-
正常释放:
-
循环引用:
- 解决循环引用的方法:
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
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
文件:
- 可以发现
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++
的赋值代码
,编译器
立马报错提示
:(造成了代码歧义
)
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创建前
,加入断点
:
- 打开
汇编模式
:
运行
代码
,加入objc_retainBlock
符号断点:
继续运行,加入
_Block_copy
符号断点:
发现
Block
的相关操作
,是在libsystem_blocks.dylib
库中:
源码地址 ,搜索
libclosure-74
,点击右边下载
按钮。
- 查看源码,可以关注到
block
的结构类型
为Block_layout
:(后面
会明白
为什么是Block_layout
)
Flag标识
- 第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) = ^{
这一行加入断点
,打开汇编模式
,运行
代码至断点处
:
-
当前读取的是
全局Block
我们给
block
添加入参
,让block捕获外界变量
:
- (void)demo {
NSArray * arr = @[@"1"];
void(^block)(void) = ^{
NSLog(@"HT_Block %@",arr); // 捕获外界变量:arr
};
block();
NSLog(@"%@",block);
}
-
断点
位置不变
,运行代码
,进入汇编页面后,断点在objc_retainBlock
这行,打印x0
(当前对象):
Q: 按照
上面
打印结果
来说,有外界变量
,没
有修饰符
时,应该是堆区Block
,为什么这里是栈区Block
?
A:Block
从栈区
拷贝到堆区
,是_Block_copy
操作的。(我们往下看)
-
往下验证,加入
objc_retainBlock
符号断点,往下执行
进入objc_retainBlock
函数内部:
-
再次读取
x0
,发现此时
还没变
。我们继续control+ 鼠标左键
,点击进入按钮
。不断使用register read x0
和p
来读取
、打印
当前对象:
-
发现进入
_Block_copy
时是栈区Block
,但是return
出来时,却变成了堆区Block
:
所以
block
真正从栈区
拷贝到堆区
,是_Block_copy
进行的。
回顾上面
Block基础结构梳理图
,我们可以通过Block
对象的首地址
进行内存平移
获取到invoke
的。
-
通过
Block
对象的首地址偏移
,取到了_block_invoke
:
-
control + 鼠标左键
,点击进入
,发现_block_invoke
内部就是block
的函数执行内容
。
【深入探究】
- 既然我们可以通过
内存平移
,取到invoke
函数。那顺便
可以验证
上面Block基础结构梳理图
的完整结构:- 在
objc_retainBlock
之后,加断点
,此时完成了_Block_copy
操作,完成
了栈区
到堆区
的block拷贝
:
对照右边图,慢慢看吧 整个结构非常清晰。
block
的类型
是@?
,签名中包含参数个数
,入参
和返参
的具体内容
和占用内存
大小。
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
:
重点:
此处有
2次拷贝
+更深层次拷贝
:
- 【第一次拷贝】:
Block自身
(拷贝一份到)->Block_byref
结构体(src栈区
结构体 )- 【第二次拷贝】:
src栈区
结构体 (拷贝一份到)->copy堆区
结构体- 【更深次拷贝】:调用
byref_keep
方法,内部又执行_Block_object_assign
函数,再判断
是否继续
往下拷贝嵌套
- 以上,就是
Block
的创建
、调用
。Block
的释放
调用_Block_object_dispose
函数:
关于Block
的探索
,都在这里
了。有兴趣
可以一个个类型
去测试
和监测