一、block 简介
1.1 block 分类
-
NSGlobalBlock
:- 位于全局区。
- 在
Block
内部不使用外部变量,或者只使用静态变量和全局变量。
-
NSMallocBlock
:- 位于堆区。
- 在
Block
内部使用局部变量或者OC
属性,并且赋值给强引用或者copy
修饰的变量。
-
NSStackBlock
:- 位于栈区。
- 与
NSMallocBlock
一样,可以在内部使用局部变量或者OC
属性。但是不能赋值给强引用或者copy
修饰的变量。
验证:
void (^globalBlock)(void) = ^{
NSLog(@"globalBlock");
};
int a = 10;
void (^mallocBlock)(void) = ^{
NSLog(@"mallocBlock - %d",a);
};
void (^__weak stackBlock)(void) = ^{
NSLog(@"stackBlock - %d",a);
};
NSLog(@"globalBlock: %@\nmallocBlock: %@\nstackBlock: %@",globalBlock, mallocBlock, stackBlock);
输出:
globalBlock: <__NSGlobalBlock__: 0x10982a050>
mallocBlock: <__NSMallocBlock__: 0x6000011c1950>
stackBlock: <__NSStackBlock__: 0x7ffee63d60c0>
ARC
下需要使用__weak
修饰才是stackBlock
。block
持有的是^{}
的指针,指向它的内存空间。
1.2 block 拷⻉到堆 block
⚠️ 前提是捕获了局部变量或者OC属性。
- 手动
copy
。
NSObject *objc = [NSObject alloc];
void(^ __weak weakBlock)(void) = ^{
NSLog(@"%@",objc);
};
//copy
NSLog(@"weakBlock: %@,weakBlock copy: %@",weakBlock,[weakBlock copy]);
//weakBlock: <__NSStackBlock__: 0x7ffee3a8e5c8>,weakBlock copy: <__NSMallocBlock__: 0x60000316d860>
-
block
作为返回值。
- (void(^)(void))returnBlock {
int a = 0;
return ^{
NSLog(@"%d",a);
};
}
//调用
NSLog(@"return block: %@",[self returnBlock]);
//输出
return block: <__NSMallocBlock__: 0x600002ad0f60>
目前在ARC
下在运行时默认会自动copy
成堆block
,在MRC
下返回栈block
会直接报错,需要copy
到堆区再返回:
- 被强引用或者
copy
修饰(捕获了局部变量/oc
属性)。默认情况下就是__strong
。
void(^strongBlock)(void) = ^{
NSLog(@"%@",objc);
};
- 系统
API
包含usingBlock
。
- (void)block_copy_usingBlock {
NSArray *arrar = @[@1,@2,@3];
void (^usingBlock)(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) = ^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
};
[arrar enumerateObjectsUsingBlock:usingBlock];
NSLog(@"%@",usingBlock);
}
这个时候定义的usingBlock
是全局block
,输出也是。不过打断点后底层调用了_Block_copy
(最终不会真的copy
,全局block
就直接返回了):
当自己定义的函数有block
参数时:
int a = 10;
[self block:^{
NSLog(@"%d",a);
}];
NSLog(@"end");
- (void)block:(void (^)(void))block {
block();
NSLog(@"%@",block);
}
block
本身是栈block
,不会调用_Block_copy
。当内部使用NSLog
打印block
时打印的是堆block
,这个时候调用了_Block_copy
:
NSLog
打印时block
转成OC
对象进行了Copy
。
1.3 block 案例分析
1.3.1 block 捕获变量的引用计数
NSObject *objc = [NSObject new];
//1
NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
//1 + 2 (栈1 + 堆 1)
void(^strongBlock)(void) = ^{
NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
strongBlock();
//栈1
void(^__weak weakBlock)(void) = ^{
NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
weakBlock();
//堆1
void(^mallocBlock)(void) = [weakBlock copy];
mallocBlock();
输出:
retain count: 1
retain count: 3
retain count: 4
retain count: 5
-
objc
创建后引用计数1
。输出1
。后续的block
都对objc
进行了持有。 -
strongBlock
是一个堆block
,从栈拷贝到堆(栈+1
,堆+1
)引用计数+2
。输出3
。 -
weakBlock
是一个栈block
,引用计数+1
。输出4
。 -
mallocBlock
是从weakBlock
拷贝的,是一个堆block
引用计数+1
。输出5
。 - 这里
weakBlock
与mallocBlock
是对strongBlock
的拆分。
当将objc
使用__weak
修饰时:
NSObject *objc = [NSObject new];
//1
NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
__weak NSObject *weakObjc = objc;
//放入弱引用表中,+1 = 2
NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(weakObjc)));
void(^strongBlock)(void) = ^{
//弱引用表中已经存在,仍然是2
NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(weakObjc)));
};
strongBlock();
void(^__weak weakBlock)(void) = ^{
//弱引用表中已经存在,仍然是2
NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(weakObjc)));
};
weakBlock();
void(^mallocBlock)(void) = [weakBlock copy];
mallocBlock();
输出:
retain count: 1
retain count: 2
retain count: 2
retain count: 2
retain count: 2
weakObjc
是一个__weak
修饰的变量被放入了弱引用表,本身不会对引用计数产生变化,在调用CFGetRetainCount
的时候内部会调用objc_loadWeakRetained
对原始对象引用计数+1
,然后调用retainCount
,最后调用objc_release
对引用计数-1
,所以输出的引用计数会多1
。
1.3.2 block 的内存拷贝
int a = 0;
void(^ __weak weakBlock)(void) = ^{
NSLog(@"%d", a);
};
struct _HPBlock *blc = (__bridge struct _HPBlock *)weakBlock;
//stack block,只是赋值
id __strong strongBlock = weakBlock;
blc->invoke = nil;
//类型强转
void(^strongBlock1)(void) = strongBlock;
strongBlock1();
_HPBlock
是模仿block
的底层结构写了一个结构体,block
底层就是结构体。主要是为了修改invoke
,invoke
在底层是一个函数指针指向代码块地址。这样其实能达到hook block
的目的。
上面的代码运行会报错:
因为将
blc
指向了weakBlock
设置invoke
为nil
从而导致callout
的时候找不到invoke
。
修改
strongBlock
如下:
//id __strong strongBlock = weakBlock;
//栈 -> 堆
id __strong strongBlock = [weakBlock copy];
这个时候运行就没有问题了,strongBlock
是对weakBlock
的一份copy
。在拷贝之后对blc
的invoke
置为nil
相当于是对栈中block
的invoke
进行的修改,对堆中的strongBlock
没有影响。
如果block
本身已经在堆中了再对其copy
只是引用计数+1
,这个时候修改invoke
也会影响到堆中copy
的block
。
block
从栈中copy
到堆中是深拷贝,堆中block
拷贝是引用计数增加。
1.3.3 block 堆栈释放差异
- (void)testBlock3 {
int a = 10;
void(^__weak weakBlock)(void) = nil;
{
//代码块中 strongBlock 捕获 a
void(^__weak strongBlock)(void) = ^{
NSLog(@"a:%d", a);
};
//栈block给到weakBlock,weakBlock是栈block
weakBlock = strongBlock;
}
weakBlock();
}
上面的代码运行输出:
a:10
strongBlock
是一个栈block
捕获了a
,然后将strongBlock
赋值给weakBlock
此时weakBlock
也是栈block
。出了代码块作用域后调用栈block
仍然有用。因为栈block
的作用域是函数的栈中由系统自动回收,与代码块无关。
在代码块中代码执行完毕后此时栈中数据为:self - testBlock3 - a - weakBlock
。可以通过读取栈中数据进行验证,修改代码如下:
- (void)testBlock3 {
int a = 10;
void(^__weak weakBlock)(void) = nil;
{
//代码块中 strongBlock 捕获 a
void(^__weak strongBlock)(void) = ^{
NSLog(@"a:%d", a);
};
weakBlock = strongBlock;
NSLog(@"weakBlock:%@ strongBlock:%@",weakBlock,strongBlock);
}
//验证栈中数据
void *sp = (void *)&self;
void *end = (void *)&weakBlock;
long count = (sp - end) / 0x8;
for (long i = 0; i <= count; i++) {
void *address = sp - 0x8 * i;
if (i == 1) {//SEL
NSLog(@"%p : %s",address, *(void **)address);
} else if (i == 2) {//int
//由于int存储在高32位中,所以右移还原
NSLog(@"%p : %d",address, *(uint64_t*)(address) >> 32);
} else {//object
NSLog(@"%p : %@",address, *(void **)address);
}
}
//end
weakBlock();
}
int
数据只占用4
字节,所以右移32
位还原。输出:
0x7ffee8852118 :
0x7ffee8852110 : testBlock3
0x7ffee8852108 : 10
0x7ffee8852100 : <__NSStackBlock__: 0x7ffee88520d0>
由于栈中内存分配从高到低,所以这里输出顺序与上面分析完全相反。当然也可以直接lldb
调试查看:
将strongBlock
的__weak
修饰去掉:
- (void)testBlock3 {
int a = 10;
void(^__weak weakBlock)(void) = nil;
{
//代码块中 strongBlock 捕获 a
void(^strongBlock)(void) = ^{
NSLog(@"a:%d", a);
};
//堆block给到 weakBlock 两个都变为堆block
weakBlock = strongBlock;
NSLog(@"weakBlock:%@ strongBlock:%@",weakBlock,strongBlock);
}
weakBlock();
}
输出:
weakBlock:<__NSMallocBlock__: 0x600000309050> strongBlock:<__NSMallocBlock__: 0x600000309050>
这个时候运行输出block
类型后就崩溃了,因为strongBlock
是堆block
作用域是代码块中(出了作用域会调用_Block_release
)。而weakBlock
是弱引用(也是堆block
,与strongBlock
指向同一块内存空间)指向strongBlock
没有强持有strongBlock
出了作用域后strongBlock
就释放了,weakBlock
就为nil
了。
此时修改a
为NSObject
类型的变量objc
:
- (void)testBlock3 {
NSObject *objc = [NSObject alloc];
void(^__weak weakBlock)(void) = nil;
{
//代码块中 strongBlock 捕获 objc
void(^__weak strongBlock)(void) = ^{
NSLog(@"objc:%@", objc);
};
weakBlock = strongBlock;
NSLog(@"weakBlock:%@ strongBlock:%@",weakBlock,strongBlock);
}
//验证栈中数据
void *sp = (void *)&self;
void *end = (void *)&weakBlock;
long count = (sp - end) / 0x8;
for (long i = 0; i <= count; i++) {
void *address = sp - 0x8 * i;
if (i == 1) {//SEL
NSLog(@"%p : %s",address, *(void **)address);
}
else {//object
NSLog(@"%p : %@",address, *(void **)address);
}
}
//end
weakBlock();
}
输出:
weakBlock:<__NSStackBlock__: 0x7ffee5b1d0d0> strongBlock:<__NSStackBlock__: 0x7ffee5b1d0d0>
0x7ffee5b1d118 :
0x7ffee5b1d110 : testBlock3
0x7ffee5b1d108 :
0x7ffee5b1d100 : <__NSStackBlock__: 0x7ffee5b1d0d0>
objc:(null)
可以看到此时block
中objc
输出null
,而objc
在栈中是存在的。a
是值拷贝,objc
是引用计数增加。
如果weakBlock
与strongBlock
都是强引用那么此时block
中objc
是可以输出的。那么说明栈block
在捕获的对象在出了作用域后释放了。也就是说如果block
是一个assign
或者weak
修饰的属性,那么捕获的对象也是被释放的。
- 栈的作用域在函数栈区,堆的作用域在代码块中。栈
block
作用域同理。 block
捕获对象是指针引用,捕获基本数据类型是值拷贝。栈block
捕获的对象类型在出了作用域后对象就被释放了。weak
或者assign
修饰的block
在出了函数栈后block
被释放,为空;在出了作用域后捕获的对象被释放,为空。
1.3.4 栈block
- (void)blockHeap {
int a = 0;
void(^ __weak block)(void) = ^{
NSLog(@"---%d", a);
};
dispatch_block_t dispatch_block = ^{
block();
};
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
}
以上代码在执行后会crash
,由于延迟调用的原因出了函数后栈block
被释放了。
这个时候可以加个阻塞就不会崩溃了:
- (void)blockHeap {
int a = 0;
void(^ __weak block)(void) = ^{
NSLog(@"---%d", a);
};
dispatch_block_t dispatch_block = ^{
block();
};
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
// sleep(3);
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}
那么如果block
是一个堆block
呢?
- (void)blockHeap {
int a = 0;
void(^block)(void) = ^{
NSLog(@"---%d", a);
};
dispatch_block_t dispatch_block = ^{
block();
};
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
}
运行不会有问题。两个的区别是__weak
与__strong
,dispatch_block
对于这两个的捕获是与修饰符对应的。这也就是block
的 遇弱捕弱,遇强补强。
二、block 的循环引用
正常对象的释放流程如下:
当block
和对象互相持有时:
2.1 案例1
typedef void(^HPBlock)(void);
@property (nonatomic, copy) HPBlock block;
@property (nonatomic, copy) NSString *name;
self.name = @"HotpotCat";
self.block = ^(){
NSLog(@"%@",self.name);
};
self.block();
以上案例会造成循环引用self -> block -> self
。这个时候使用__weak
修饰self
就可以解决循环引用了:
self.name = @"HotpotCat";
__weak typeof(self) weakSelf = self;
self.block = ^(){
NSLog(@"%@",weakSelf.name);
};
self.block();
这个时候持有链变成了self -> block -> weakSelf(nil) -> block
所以不会造成循环引用。在block
内部执行一个延时操作:
self.name = @"HotpotCat";
__weak typeof(self) weakSelf = self;
self.block = ^(){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",weakSelf.name);
});
};
self.block();
在快速退出页面后,打印的时候self
已经释放了。这个时候需要在block
内部对weakSelf
进行强引用:
self.name = @"HotpotCat";
__weak typeof(self) weakSelf = self;
self.block = ^(){
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf.name);
});
};
self.block();
这个时候持有链变成了self -> block -> strongSelf(临时变量) —> weakSelf(nil) > block
。这个时候strongSelf
只是一个临时变量,出了作用域空间strongSelf
就释放了。当延迟函数没有执行完毕退出页面的时候并不马上释放,等到执行完毕后才进行释放。weakSelf
会自动置空,在block
内部strongSelf
对weakSelf
进行了短暂的持有。这种解决循环引用的方式是 weak-strong-dance
(强弱共舞)。
那么修改为网络请求呢?
__weak typeof(self) weakSelf = self;
[[[NSURLSession sharedSession] dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.raywenderlich.com"]] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf);
});
}] resume];
在进入页面后立马退出,输出:
(null)
与上面的区别是在执行完NSURLSession
的时候weakSelf
已经是nil
了,内部strongSelf
指向的就是nil
。(这里没有互相持有)。
__weak
是将修饰的变量放入弱引用表,用的时候从弱引用表取出。取的时候如果为nil
了就赋值nil
,否则就赋值它所指向的对象给临时变量,临时变量出了作用域后自动释放。这也就是weak
能解决循环引用的根本原因。
2.2 五种解决循环引用的方式
除了上面说大家比较熟悉的 weak-strong-dance
(强弱共舞) 解决循环引用的方式,还有4
种方式。
2.2.1 临时变量置nil
__block ViewController *vc = self;
self.block = ^(){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
vc = nil;
});
};
self.block();
使用本身的类型接收self
然后用__block
修饰,这个时候的持有关系是vc -> self -> block -> vc
,但是在使用完毕后vc = nil
切断了循环引用的链条也就断开了循环引用。
⚠️ 当然也可以在调用后或者在
block
内部调用完成时直接设置self.block = nil
。一般这么做没什么意义,block
设置为属性的目的就是为了多次使用,如果只使用一次就释放直接局部block
就可以了。
2.2.2 self 作为 block 参数
typedef void(^HPBlock)(ViewController *);
self.block = ^(ViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
});
};
self.block(self);
将self
作为了参数传递给block
,block
内部是对参数的引用,不会造成循环引用。
⚠️另外两种待补充
2.3 案例2
static ViewController *staticSelf;
- (void)weak_static {
__weak typeof(self) weakSelf = self;
staticSelf = weakSelf;
}
以上案例在页面退出的时候dealloc
不会执行造成了内存泄露。此时持有关系是staticSelf -> weakSelf -> self
。
那么weakSelf
为什么没有释放呢?
本质上staticSelf 、weakSelf 、self
都指向同一块内存空间:
而
staticSelf
是全局变量并不会释放,所以即使退出页面页面也不会释放。
2.4 案例3
typedef void(^HPBlock)(void);
@property (nonatomic, copy) HPBlock block1;
@property (nonatomic, copy) HPBlock block2;
- (void)block_weak_strong {
__weak typeof(self) weakSelf = self;
self.block1 = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
weakSelf.block2 = ^{
NSLog(@"%@",strongSelf);
};
weakSelf.block2();
};
self.block1();
}
以上代码会造成循环引用。虽然strongSelf
是一个临时变量,但是strongSelf
被block2
持有了(出了block1
代码块仍然不会释放),block2
又被self
持有就有了self -> block2 -> strongSelf(与self指向同一块内存)
。这个时候由于strongSelf
不会自动置为nil
所以会造成循环引用。
即使不调用
block2
也会造成循环引用,因为赋值的时候已经持有了。
那么如果修改如下呢?
- (void)block_weak_strong {
self.block = ^{
void (^block1)() = ^ {
NSLog(@"%@",self);
};
block1();
};
self.block();
}
这个时候仍然会造成循环引用,由于 block
的捕获是逐层捕获的:
2.4.1 强弱共舞解决循环引用
这个时候如果block2
中直接使用weakSelf
或者再对strongSelf
转一次weak
就可以解决问题了:
__weak typeof(self) weakSelf = self;
self.block1 = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
__weak typeof(strongSelf) weakSelfAgain = strongSelf;
weakSelf.block2 = ^{
//NSLog(@"%@",weakSelf);
NSLog(@"%@",weakSelfAgain);
};
weakSelf.block2();
};
self.block1();
此时如果block2
中还有延迟函数就又要进行一次临时变量强持有了:
__weak typeof(self) weakSelf = self;
self.block1 = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
__weak typeof(strongSelf) weakSelfAgain = strongSelf;
weakSelf.block2 = ^{
__strong typeof(weakSelfAgain) strongSelfAgain = weakSelfAgain;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelfAgain.name);
});
};
weakSelf.block2();
};
self.block1();
就无限套娃就完事了。
2.4.2 临时变量置为nil
__weak typeof(self) weakSelf = self;
self.block1 = ^{
__block ViewController* vc = weakSelf;
weakSelf.block2 = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
vc = nil;
});
};
weakSelf.block2();
};
self.block1();
对于block2
中有延时任务的时候也可以通过临时变量使用完置为nil
解决循环引用,当然没有延时任务直接使用weakSelf
就完事了。
前面分析了block
的分类以及循环引用,那么有以下疑问:
-
block copy
后就从栈区到堆区了,究竟是怎么处理的呢? -
block
是如何捕获变量的呢? -
block
捕获的对象是怎么保存和释放的呢?
三、block 的 Clang 底层分析
一个最简单的block
:
#include
int main(){
int a = 18;
void(^block)(void) = ^{
printf("%d",a);
};
block();
return 0;
}
通过clang
编译成.cpp
文件,此时main
函数变成了:
int main(){
int a = 18;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
为了方便忽略类型强转还原后如下:
int main(){
int a = 18;
//(__main_block_impl_0)函数调用的结果取地址给到 block,参数是 __main_block_func_0,&__main_block_desc_0_DATA 以及 a
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);
//调用 block->FuncPtr,参数是 block 自己
block->FuncPtr(block);
return 0;
}
-
void(^block)(void)
相当于__main_block_impl_0
的调用结果赋值给它,参数是__main_block_func_0
、&__main_block_desc_0_DATA
以及a
。 -
block()
执行相当于block
调用FuncPtr
参数是自己。
3.1 __main_block_impl_0
__main_block_impl_0
是一个结构体:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
通过结构体的构造函数生成block
,3
个参数与结构体的成员变量对应。当block
不持有临时变量时:
此时如果捕获的是对象类型呢?
可以看到仍然是对
objc
的赋值,这里变成了引用类型也就是持有。
block
捕获值类型/引用类型的时候内部结构体生成了对应的成员变量,通过C++
语法在构造函数中直接赋值。- 在编译阶段所有类型的
block
的isa
都指向_NSConcreteStackBlock
,也就是说block
类型是在运行时确定的。
既然block
在底层是一个结构体,而oc
对象在底层也是结构体。那么block
是否是oc
的对象呢?
void (^block)(void) = ^ {
NSLog(@"test");
};
NSLog(@"%d",[block isKindOfClass:NSClassFromString(@"__NSGlobalBlock__")]);
输出:1
。那么可以确定其实block
也是oc
对象。在源码中有如下定义:
block
也是oc
对象,继承自NSObject
。
3.2 __main_block_func_0
第一个参数fp(__main_block_func_0)
赋值给了FuncPtr
,FuncPtr
是在block
调用的时候进行调用的。fp
是函数式保存,这样就说明如果block
不执行它的功能就不会被调用。
__main_block_func_0
内部获取了结构体存储的值,对于值类型/引用类型以及__block
修饰的类型的存储与调用如下:
- 基本类型存储的是值,调用的是存储的这个值。
- 引用类型存储的是指针,调用的是对应的指针。
-
__block
修饰的类型存储的是__Block_byref_
,调用的是ref->__forwarding->b
。
那么__block
究竟干了什么呢?
3.2.1 基本类型的__block
在main
中b
变成了__Block_byref_b_0
(精简后的去掉了类型强转):
__Block_byref_b_0 b = {
(void*)0,
(__Block_byref_b_0 *)&b,//取b本来的的地址
0,
sizeof(__Block_byref_b_0),
20};
__Block_byref_b_0
结构如下:
struct __Block_byref_b_0 {
void *__isa;
__Block_byref_b_0 *__forwarding;
int __flags;
int __size;
int b;
};
这里看__forwarding
就是指向b
。在block
的构造函数中赋值给b
的是__forwarding
也就是b
的指针。所以加了__block
后基本数据类型就指向了外部的变量,这个时候修改ref
就是修改外部的基本类型了。
3.2.2 引用类型的 __block
objc2
变成了__Block_byref_objc2_1
类型:
__Block_byref_objc2_1 objc2 = {
(void*)0,
(__Block_byref_objc2_1 *)&objc2,//取objc2本来的地址,指向的内存空间
33554432,
sizeof(__Block_byref_objc2_1),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"))};
__Block_byref_objc2_1
的结构如下:
struct __Block_byref_objc2_1 {
void *__isa;
__Block_byref_objc2_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *objc2;
};
__forwarding
指向objc2
指向的内存空间。也就是__forwarding
与objc2
两个指针指向了同一片内存空间,不是之前的指向objc2
了。所以就能在block
内部修改objc2
本身了。
__block
生成了__Block_byref_
结构体,传递给结构体的是对应指向的内存空间。
在__Block_byref_objc2_1
中还发现了__Block_byref_id_object_copy
以及__Block_byref_id_object_dispose
,对应的传递的函数为__Block_byref_id_object_copy_131
与__Block_byref_id_object_dispose_131
(只有定义没有调用):
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
内部调用了_Block_object_assign
与_Block_object_dispose
。
3.3 __main_block_desc_0
block
构造函数的第二个参数是__main_block_desc_0_DATA
:
-
reserved
是保留字段。 -
Block_size
block(__main_block_impl_0)
的大小,实际上就是成员变量占用的内存空间。 -
copy
拷贝函数,传递的是__main_block_copy_0
。 -
dispose
释放函数,传递的是__main_block_dispose_0
。
__main_block_copy_0
以及__main_block_dispose_
内部对block
持有的引用类型以及__block
修饰的类型(ref
)进行了_Block_object_assign
与_Block_object_dispose
的调用,他们的实现并没有在.cpp
文件中。
3.2 __weak
__weak
为什么能解决循环引用呢?
NSObject *objc = [NSObject alloc];
__weak NSObject *objc2 = [NSObject alloc];
void(^block)(void) = ^{
NSLog(@"%@,%@",objc,objc2);
};
block();
可以看到
block
对weak
变量的持有也是__weak
类型,调用依然是。也就是对应的 遇弱捕弱,遇强补强。
四、汇编分析block底层
void(^block)(void) = ^{
NSLog(@"HotpotCat");
};
block();
在有缓存的情况下可能会省略掉,最好使用一个新的工程测试。
4.1 定位到 _Block_copy
对block
的赋值下断点查看汇编调用:
下符号断点
objc_retainBlock
或者直接跟踪进去:
定位到了
libobjc objc_retainBlock
,在源码中它的实现如下:
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
内部调用了_Block_copy
,它的实现并不在objc
中:
继续对
_Block_copy
下符号断点:
定位到了
libsystem_blocks.dylib
,那也就是说block
的实现是在libsystem_blocks
中的。libsystem_blocks
本身没有开源,可以通过libclosure工程替代。
可以将
libclosure
编译成可运行的工程调试。
4.2 汇编分析 _Block_copy
断点进入_Block_copy
后读取寄存器查看参数:
这个时候是一个全局
block
,修改代码让block
持有变量再次查看:
变成了栈
block
,也就是说在_Block_copy
之前仍然是栈block
。在汇编ret
继续断点查看x0
:
block
在编译阶段isa
指针指向_NSConcreteStackBlock
,在运行阶段调用_Block_copy
之后从栈block
变成了堆block
。
4.3 汇编分析 签名函数
在打印block
的时候有一个signature
,它是否v8@?0
就是上面block
的签名呢?
-
v
返回值void
。 -
8
代表占用的空间。 -
@?
代表一个类型,从0
号位置开始占用8
字节。
block
的签名类型就是@?
。
五、libclosure 分析 block 底层实现
5.1 _Block_copy 分析
/*
拷贝 block,参数是 block (Block_layout 对象)本身。
1.堆上 block 引用计数 + 1。
2.栈上 block 先拷贝到堆上,引用计数初始化为1。然后调用 copy helper 方法(如果存在)。
3.全局 block 直接返回block本身。
返回 拷贝后的 block 地址。
*/
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
//block 不存在,直接返回 NULL。
if (!arg) return NULL;
// The following would be better done as a switch statement
//block 强转为 Block_layout 对象
aBlock = (struct Block_layout *)arg;
//已经在堆上
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
//引用计数 + 1
latching_incr_int(&aBlock->flags);
return aBlock;
}
//全局 block 不进行操作直接返回。
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {//栈 block,需要拷贝到 堆上
// Its a stack block. Make a copy.
//获取原始 栈block 大小
size_t size = Block_size(aBlock);
//在堆上重新开辟一块 与 aBlock 相同大小的内存空间
struct Block_layout *result = (struct Block_layout *)malloc(size);
// 开辟失败,返回 NULL
if (!result) return NULL;
//将 aBlock 内存上的数据全部复制到新开辟的 result 上
memmove(result, aBlock, size); // bitcopy first
......
// reset refcount
//重新设置引用计数以及释放标志位(清除置为0)
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
// 将 result 标记位为堆上,需要手动释放;并且引用计数初始化为 1
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
//拷贝成员变量的工作 调用 Block_descriptor_2 以及 Block_descriptor_3
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
//isa标记为栈block
result->isa = _NSConcreteMallocBlock;
//返回新的block
return result;
}
}
- 参数是
block
(Block_layout
对象)本身。 -
BLOCK_NEEDS_FREE
代表已经有引用计数了(也就是堆block
),则引用计数+1
。 - 全局
block
直接返回。 - 栈
block
创建空间拷贝block
。- 重置引用计数标志以及释放标识位。
- 设置引用计数为
1
。 - 调用
Block
的copy
函数,内部调用的是_Block_object_assign
(也就是拷贝拷贝成员变量)。 - 设置
isa
标志为_NSConcreteMallocBlock
。 - 返回拷贝后的
block
地址。
5.1.1 latching_incr_int
/*
引用计数加 1,最多不超过 BLOCK_REFCOUNT_MASK。
volatile的作用是:作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。简单地说就是防止编译器对代码进行优化
*/
static int32_t latching_incr_int(volatile int32_t *where) {
while (1) {
// 如果 old_value 在第 1~15 位都已经变为 1 了,即引用计数已经满了,就返回 BLOCK_REFCOUNT_MASK
int32_t old_value = *where;
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return BLOCK_REFCOUNT_MASK;
}
// 比较 where的值 是否等于 old_value,如果等于,就将新值 oldValue + 2 放入 where
// 否则继续下一轮循环
// 这里加 2,是因为 flag 的第 0 位已经被占了,引用计数是第 1~15 位,所以加上 0b10,引用计数只是加 1
if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
return old_value+2;
}
}
}
引用计数加 1
,最多不超过 BLOCK_REFCOUNT_MASK
。
5.1.2 _Block_call_copy_helper
/*
调用 block 的 copy helper 方法,即 Block_descriptor_2 中的 copy 方法
*/
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
//获取 copy 函数
if (auto *pFn = _Block_get_copy_function(aBlock))
//调用函数,最终会调用到 _Block_object_assign
pFn(result, aBlock);
}
获取Block_descriptor_2
的copy
进行调用。
5.2 Block_layout 结构
在_Block_copy
中block
是以Block_layout
接收的,也就是说block
在底层是Block_layout
类型:
struct Block_layout {
void * __ptrauth_objc_isa_pointer isa;
volatile int32_t flags; // contains ref count
int32_t reserved;//保留字段
BlockInvokeFunction invoke;//最终回调
struct Block_descriptor_1 *descriptor;//描述信息
// imported variables
};
-
isa
指针指向block
对应类型。
void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };
-
flags
标志位记录是否正在析构、引用计数掩码、是否有签名等。 -
invoke
是block
最终的回调函数。 -
descriptor
记录描述信息。
// imported variables
注释说明了应该还有其它字段。
5.2.1 flags 标志位说明
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime 引用计数掩码
BLOCK_INLINE_LAYOUT_STRING = (1 << 21), // compiler
#if BLOCK_SMALL_DESCRIPTOR_SUPPORTED
BLOCK_SMALL_DESCRIPTOR = (1 << 22), // compiler
#endif
BLOCK_IS_NOESCAPE = (1 << 23), // compiler
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
- 第
1
位:释放标记,一般常用BLOCK_NEEDS_FREE
做位与操作,一同传入Flags
,告知该block
可释放。 - 第
16
位:存储引用计数的值,是一个可选用参数。 - 第
24
位:第16
是否有效的标志,程序根据它来决定是否增加或是减少引用计数位的值;。 - 第
25
位:是否拥有拷贝辅助函数(a copy helper function
)。 - 第
26
位:是否拥有block
析构函数。 - 第
27
位:标志是否有垃圾回收(macOS
使用)。 - 第
28
位:标志是否是全局block
。 - 第
30
位:与BLOCK_USE_STRET
相对,判断是否当前block
拥有一个签名。用于runtime
运行时动态调用。
5.2.2 Block_descriptor_1
struct Block_descriptor_1 {
uintptr_t reserved;//保留字段
uintptr_t size;//大小
};
但是在上面clang
分析的时候__main_block_desc_0
不仅仅是这两个字段,并且汇编打印的时候block
中也有其它字段,比如copy
与dispose
就没有出现在Block_layout
与Block_descriptor_1
中。但是在Block_descriptor_1
的周围发现了Block_descriptor_2
与Block_descriptor_3
:
也就是通过
flags
标志位BLOCK_HAS_COPY_DISPOSE
和BLOCK_HAS_SIGNATURE
确定是否有Block_descriptor_2
与Block_descriptor_3
的。
那么是在什么时机进行拼接生成的呢?
既然有设置就有获取,搜索_Block_descriptor_2
的获取:
可以看到在
desc1
后面拼接了desc2
以及desc3
,当然这个过程中会根据标志位判断他们是否存在。
对应结构如下:
读取block
内存结构如下:
block
偏移8 + 4 + 4
字节得到invoke
(与打印出来的invoke
地址相同),偏移8 + 4 + 4 + 8
得到desc
指针:
desc
指针指向desc1
、desc2
以及desc3
中保存的值。与copy
、dispose
、signature
是完全对应的。
desc
根据标记位以及内存平移进行访问。
5.3 block捕获变量的生命周期
上面分析了Block_layout
的内存结构,那么变量是如何捕获和释放的呢?
在clang
分析的时候__main_block_copy_0
以及__main_block_dispose_
赋值给了copy
以及dispose
。
其实现调用了
_Block_object_assign
与_Block_object_dispose
。这里的copy
与dispose
是desc2
中的,与block
本身的copy
没有关系。copy
的是捕获的引用类型以及__block
修饰的成员变量,
搜索_Block_object_assign
有注释(翻译过后)如下:
这是给编译器提供的 API
block 可以引用 4 种不同的类型的对象,当 block 被拷贝到堆上时,需要 help,即帮助拷贝一些东西。
1)基于 C++ 栈的对象
2)Objective-C 对象
3)其他 Block
4)被 __block 修饰的变量
block 的 helper 函数是编译器合成的(比如编译器写的 __main_block_copy_1() 函数),它们被用在 _Block_copy() 函数和 _Block_release() 函数中。copy helper 对基于 C++ 栈的对象调用调用 C++ 拷贝构造函数,对其他三种对象调用 _Block_object_assign 函数。 dispose helper 对基于 C++ 栈的对象调用析构函数,对其他的三种调用 _Block_object_dispose 函数。
_Block_object_assign 和 _Block_object_dispose 函数的第三个参数 flags 有可能是:
1)BLOCK_FIELD_IS_OBJECT(3) 表示是一个对象
2)BLOCK_FIELD_IS_BLOCK(7) 表示是一个 block
3)BLOCK_FIELD_IS_BYREF(8) 表示是一个 byref,一个被 __block 修饰的变量;如果 __block 变量还被 __weak 修饰,则还会加上 BLOCK_FIELD_IS_WEAK(16)
所以 block 的 copy/dispose helper 只会传入四种值:3,7,8,24
上述的4种类型的对象都会由编译器合成 copy/dispose helper 函数,和 block 的 helper 函数类似,byref 的 copy helper 将会调用 C++ 的拷贝构造函数(不是常拷贝构造),dispose helper 则会调用析构函数。还一样的是,helpers 将会一样调用进两个支持函数中,对于对象和 block,参数值是一样的,都另外附带上 BLOCK_BYREF_CALLER (128) bit 的信息。#疑问:调用的这两个函数是啥?BLOCK_BYREF_CALLER 里究竟存的是什么??
所以 __block copy/dispose helper 函数生成 flag 的值为:对象是 3,block 是 7,带 __weak 的是 16,并且一直有 128,有下面这么几种组合:
__block id 128+3 (0x83)
__block (^Block) 128+7 (0x87)
__weak __block id 128+3+16 (0x93)
__weak __block (^Block) 128+7+16 (0x97)
对应的有如下枚举定义:
// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
// see function implementation for a more complete description of these fields and combinations
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // a block variable
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};
所以block
对捕获变量的类型有如下处理:
-
BLOCK_FIELD_IS_OBJECT
:对象类型变量。 -
BLOCK_FIELD_IS_BLOCK
:block
类型变量。 -
BLOCK_FIELD_IS_BYREF
:__block
类型变量。 -
BLOCK_FIELD_IS_WEAK
:__weak
修饰的变量。 -
BLOCK_BYREF_CALLER
:ref
类型的变量。
使用以下代码进行验证:
NSObject *objc = [NSObject alloc];
void(^block1)(void) = ^{
NSLog(@"%@",objc);
};
__block NSObject *objc2 = [NSObject alloc];
__weak NSObject *objc3 = [NSObject alloc];
__weak __block NSObject *objc4 = [NSObject alloc];
void(^block)(void) = ^{
NSLog(@"%@,%@,%@,%@,%@",objc,block1,objc2,objc3,objc4);
};
block();
__weak
底层是与ref
相同的处理。
对于错误1.
cannot create _weak reference because the current deployment target does not support weak references
需要指定-fobjc-runtime
版本。
2.cannot create __weak reference in file using manual reference
需要指定-fobjc-arc
。
完整命令:
xcrun clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 block.m -o block.cpp
5.3.1 _Block_object_assign
//参数 destArg 是一个二级指针,指向真正的目标指针。
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
//普通对象类型
case BLOCK_FIELD_IS_OBJECT:
// _Block_retain_object_default = fn (arc),交给了系统arc处理。
_Block_retain_object(object);
//src 给到 dest 指向同一块内存。地址指向相同,地址不同。
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
//block会被传过来的block进行拷贝
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
//__block 对象,也就是 ref 拷贝,使 dest 指向拷贝到堆上的 byref
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
*dest = object;
break;
default:
break;
}
}
根据传递的类型的不同进行不同的处理:
- 普通对象类型(3):
retain
操作交给arc
自动处理,然后指针赋值。 -
block
类型(7):调用_Block_copy
进行拷贝。 -
__block
类型(8):调用_Block_byref_copy
对ref
进行拷贝,如果捕获的对象是引用类型则继续调用_Block_object_assign
进行普通对象类型的拷贝。
5.3.2 _Block_byref_copy
/*
1.如果 byref 原来在栈上,就将其拷贝到堆上,拷贝的包括 Block_byref、Block_byref_2、Block_byref_3,
被 __weak 修饰的 byref 会被修改 isa 为 _NSConcreteWeakBlockVariable,
原来 byref 的 forwarding 也会指向堆上的 byref;
2.如果 byref 已经在堆上,就只增加一个引用计数。
参数 dest是一个二级指针,指向了目标指针,最终,目标指针会指向堆上的 byref
*/
static struct Block_byref *_Block_byref_copy(const void *arg) {
// arg 强转为 Block_byref* 类型
struct Block_byref *src = (struct Block_byref *)arg;
// 引用计数等于 0
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
//为新的 byref 在堆中分配内存
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
//isa指向置为空
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
// 新 byref 的 flags 中标记了它是在堆上,且引用计数为 2。
// 为什么是 2 呢?注释说的是 non-GC one for caller(调用者), one for stack(src 的 forwarding 也指向了 copy,相当于引用了 copy)
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
//堆空间 forwarding 指向自己
copy->forwarding = copy; // patch heap copy to point to itself
//栈空间原先的对象 forwarding 指向 拷贝的堆空间。原始的 forwarding 与 拷贝的 forwarding 都指向 堆空间的。
src->forwarding = copy; // patch stack to point to heap copy
//size大小赋值
copy->size = src->size;
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {//有copy与dispose
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
// 取得 src 和 copy 的 Block_byref_2
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
//函数赋值
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
// 如果 src 有扩展布局,也拷贝扩展布局
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
// 没有将 layout 字符串拷贝到堆上,是因为它是 const 常量,不在栈上
copy3->layout = src3->layout;
}
//byref_keep的调用(ref copy 的调用,最终也是调用了_Block_object_assign,走的是普通对象的逻辑)。
(*src2->byref_keep)(copy, src);
}
else { //如果 src 没有 copy/dispose helper
// Bitwise copy.
// This copy includes Block_byref_3, if any.
// 将 Block_byref 后面的数据都拷贝到 copy 中,一定包括 Block_byref_3
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap 有引用计数,已经拷贝到堆区了
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
// src 已经在堆上,就只将引用计数加 1
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
- 如果
ref
已经拷贝到堆区了则引用计数+1
,否则进行拷贝。 - 创建
Block_byref
的空间。 -
isa
指向null
。 -
copy
的ref
的forwarding
指向自己,原先栈空间的ref
的forwarding
指向copy
的堆空间的ref
。 - 有
copy
与dispose
则创建ref_2
(引用类型),赋值原先的copy
和dispose
函数(这里的copy
和dispose
是针对ref
本身的)。- 有
layout
则创建ref_3
并赋值。 - 调用
byref_keep
也就是copy
函数,最终会调用到_Block_object_assign
,参数layout
是objc
。
- 有
- 没有
copy
与dispose
直接将ref_3
拷贝(值类型)。
5.3.3 Block_byref
__block
修饰的变量会被转换成Block_byref
结构体:
与
Block
结构体本身一样,它的一些字段也是可选的。
对应
clang
的赋值:
Block_ref
内部的copy
传递的参数是objc
本身。
- 调用
_Block_copy
拷贝自身,从栈
->堆
。 -
_Block_copy
中调用_Block_object_assign
进行拷贝捕获的变量(引用类型&__block
才有)。- 如果是普通引用类型走的是指针赋值的逻辑。
- 如果是
block
走_Block_copy
逻辑。 -
__block
走_Block_byref_copy
逻辑拷贝Block_byref
。- 引用类型会再走
Block_byref
的copy
逻辑。再次执行_Block_object_assign
拷贝引用类型本身。
- 引用类型会再走
__block
修饰的引用类型的三层copy
:
copy block
本身,栈 -> 堆
block copy
过程中copy Block_byref
Block_byref copy
过程中copy objc
5.4 block 释放
既然有copy
那么就有对应的释放,__block
修饰的引用类型的变量被block
捕获的时候有3
层copy
,那么释放应该也是3
层。
5.4.1 _Block_release
/*
1.block 在堆上,才需要 release,在全局区和栈区都不需要 release.
2.先将引用计数减 1,如果引用计数减到了 0,就将 block 销毁
*/
void _Block_release(const void *arg) {
//强转
struct Block_layout *aBlock = (struct Block_layout *)arg;
//block不存在直接返回。
if (!aBlock) return;
//全局 block 直接返回
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
//栈区 block 直接返回
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
// 引用计数减 1,如果引用计数减到了 0,会返回 true,表示 block 需要被销毁
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
//调用block dispose
_Block_call_dispose_helper(aBlock);
//销毁实例
_Block_destructInstance(aBlock);
free(aBlock);
}
}
-
block
在堆上,才需要release
,在全局区和栈区都不需要release
。 - 先将引用计数减
1
,如果引用计数减到了0
,就将block
销毁。- 先调用
block dispose
。 - 销毁
block
实例,释放空间。
- 先调用
5.4.2 latching_decr_int_should_deallocate
/*
1.引用计数减 1,如果引用计数减到了 0,就将 block 置为 deallocating 状态
2.返回值是 block 是否需要被 dealloc
*/
static bool latching_decr_int_should_deallocate(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
// 如果引用计数还是满的,就不能 dealloc,#疑问:引用计数满了以后就不能减了么?满了后永远不释放了?
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return false; // latched high
}
// 如果引用计数为 0,按照正常的逻辑,它应该已经被置为 deallocating 状态,不需要再被 dealloc,所以返回 false
if ((old_value & BLOCK_REFCOUNT_MASK) == 0) {
return false; // underflow, latch low
}
//引用计数减 1
int32_t new_value = old_value - 2;
bool result = false;
// 如果 old_value 在 0~15 位的值是 0b10,即引用计数是 1,且不是 deallocating 状态
if ((old_value & (BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)) == 2) {
// old_value 减 1 后,新值 new_value 在 0~15 位值是 0b01,即引用计数变为0,且 BLOCK_DEALLOCATING 位变为 1
new_value = old_value - 1;
// 需要被 dealloc
result = true;
}
// 将新值 new_value 放入 where 中
if (OSAtomicCompareAndSwapInt(old_value, new_value, where)) {
return result;
}
}
}
- 引用计数减
1
,如果引用计数减到了0
,就将block
置为deallocating
状态。 - 返回值是
BOOL
值,确定block
是否需要被dealloc
。
5.4.3 _Block_object_dispose
/*
调用 block 的 dispose helper 方法,即 Block_descriptor_2 中的 dispose 方法
*/
static void _Block_call_dispose_helper(struct Block_layout *aBlock)
{
//找到dispose
if (auto *pFn = _Block_get_dispose_function(aBlock))
//调用dispose,最终会调用到 _Block_object_dispose
pFn(aBlock);
}
_Block_call_dispose_helper
最终会调用到_Block_object_dispose
。
block
的dispose
也就是调用_Block_object_dispose
释放持有的对象以及ref
。
_Block_object_dispose
//当 block 和 byref 要 dispose 对象时,它们的 dispose helper 会调用这个函数
void _Block_object_dispose(const void *object, const int flags) {
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
// byref
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
// get rid of the __block data structure held in a Block
//ref 调用 _Block_byref_release,对 byref 对象做 release 操作
_Block_byref_release(object);
break;
case BLOCK_FIELD_IS_BLOCK:
// block 继续调用 _Block_release,block 做 release 操作
_Block_release(object);
break;
case BLOCK_FIELD_IS_OBJECT:
//对象交给ARC
_Block_release_object(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
break;
default:
break;
}
}
- 普通对象类型(3):
release
操作交给arc
自动处理。 -
block
类型(7):调用_Block_release
进行释放。 -
__block
类型(8):调用_Block_byref_release
对ref
进行释放,如果捕获的对象是引用类型则继续调用_Block_object_dispose
进行普通对象类型的释放。
5.4.4 _Block_byref_release
/*
对 byref 对象做 release 操作。
1.堆上的 byref 需要 release,栈上的不需要 release。
2.release 就是引用计数减 1,如果引用计数减到了 0,就将 byref 对象销毁。
*/
static void _Block_byref_release(const void *arg) {
struct Block_byref *byref = (struct Block_byref *)arg;
// dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
// 取得真正指向的 byref,如果 byref 已经被堆拷贝,则取得是堆上的 byref,否则是栈上的,栈上的不需要 release,也没有引用计数
byref = byref->forwarding;//获取 forwarding
//byref 被拷贝到堆上,需要 release
if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
// 取得引用计数
int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
os_assert(refcount);
// 引用计数减 1,如果引用计数减到了 0,会返回 true,表示 byref 需要被销毁
if (latching_decr_int_should_deallocate(&byref->flags)) {
// 如果 byref 有 dispose helper,就先调用它的 dispose helper
if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
//dispose函数获取
struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
//调用 _Block_object_dispose 释放objc
(*byref2->byref_destroy)(byref);
}
//释放ref
free(byref);
}
}
}
- 堆上的
byref
需要release
,栈上的不需要release
。 - 堆上
byref
释放操作:- 取得引用计数。
- 引用计数减
1
,如果引用计数减到了0
,会返回true
,表示byref
需要被销毁。 - 如果是引用类型则调用
ref
的dispose
函数,内部其实调用了_Block_object_dispose
参数是objc
。-
_Block_object_dispose
这次走普通对象逻辑。
-
- 调用
free
释放空间。
__block
修饰的引用类型的三层dispose
与copy
完全相反:
Block_byref dispose
过程中先dispose objc
block dispose
过程中先dispose Block_byref
dispose block
本身,释放空间
六、总结
-
block
分类:- 全局 block 位于全局区,在
Block
内部不使用外部变量,或者只使用静态变量和全局变量。 - 堆 block 位于堆区,在
block
内部使用局部变量或者OC
属性,并且赋值给强引用或者copy
修饰的变量。 - 栈 block 位于栈区,与
堆block
一样,可以在内部使用局部变量或者OC
属性,但是不能赋值给强引用或者copy
修饰的变量。
- 全局 block 位于全局区,在
-
block
拷贝到堆 block
的情况(捕获了局部变量或者OC
属性):- 手动
copy
、block
作为返回值、被强引用或者copy
修饰、系统API
包含usingBlock
。
- 手动
-
block
捕获变量(遇强捕强、遇弱捕弱):- 栈
block
捕获变量引用计数+1
,堆block
捕获变量引用计数+2
(堆栈各+1
)。 -
__weak
修饰的比变量会被加入弱引用表,在调用CFGetRetainCount
的时候会临时对引用计数+1
,调用完后会释放-1
。 -
block
捕获对象是指针引用,捕获基本数据类型是值拷贝。栈block
捕获的对象类型在出了block
作用域后对象就被释放了。 -
weak
或者assign
修饰的block
在出了函数栈后block
被释放,为空;在出了作用域后捕获的对象被释放,为空。 -
block
的捕获是 逐层捕获。
- 栈
-
block
从栈中copy
到堆中是 深拷贝,堆中block
拷贝是引用计数增加。 - 栈的作用域在函数栈区,堆的作用域在代码块中。栈
block
作用域同理。 -
__weak
是将修饰的变量放入弱引用表,用的时候从弱引用表取出。取的时候如果为nil
了就赋值nil
,否则就赋值它所指向的对象给临时变量,临时变量出了作用域后自动释放。这也就是weak
能解决循环引用的根本原因(block
捕获的变量也是weak
类型)。 - 循环引用的解决方案:
-
weak-strong-dance
(强弱共舞)。 - 临时变量置
nil
。 -
self
作为block
参数。
-
-
block
在底层是结构体,有一个isa
指针以及funcptr
,funcptr
指向一个函数(代码块函数)。block()
执行相当于block
调用funcPtr
参数是block
自己。 -
block
捕获值类型/引用类型的时候内部结构体生成了对应的成员变量。基本类型存储的是值,调用的是存储的这个值。引用类型存储的是指针,调用的是对应的指针。 -
block
也是oc
对象,继承自NSObject
。 -
__block
修饰的类型存储的是__Block_byref_
结构体,传递给结构体的是对应指向的内存空间。 -
block
在编译阶段isa
指针指向_NSConcreteStackBlock
,在运行阶段调用_Block_copy
之后从栈block
变成了堆block
。 -
block
的签名类型是@?
。 -
block
的拷贝(_Block_copy
):- 1.堆上
block
引用计数 + 1。 - 2.栈上
block
先拷贝到堆上,引用计数初始化为 1。然后调用copy helper
方法(如果存在,最终会调用到_Block_object_assign
拷贝成员变量)。- 2.1 普通对象类型(
3
),retain
操作交给arc
自动处理,然后指针赋值。 - 2.2
block
类型(7
),调用_Block_copy
进行拷贝。 - 2.3
__block
类型(8
),调用_Block_byref_copy
对ref
进行拷贝。- 2.3.1
ref
已经在堆区则引用计数+1,否则进行ref
拷贝。 - 2.3.2
ref
是引用类型(有copy
以及dispose
)则继续调用_Block_object_assign
,参数是objc
。
- 2.3.1
- 2.1 普通对象类型(
- 3.全局
block
直接返回block
本身。 - 4.
__block
修饰的引用类型的三层 copy:-
copy block
本身,栈 -> 堆。 -
block copy
过程中copy Block_byref
。 -
Block_byref copy
过程中copy objc
。
-
- 1.堆上
-
block
的释放(_Block_release
):- 1.
block
在堆上,才需要release
,在全局区和栈区都不需要release
。 - 2.先将引用计数减
1
,如果引用计数减到了0
,就将block
销毁。- 2.1 先调用
block dispose
(_Block_object_dispose
)。- 2.1.1 普通对象类型(
3
),release
操作交给arc
自动处理。 - 2.1.2
block
类型(7
),调用_Block_release
进行释放。 - 2.1.3
__block
类型(8),调用_Block_byref_release
对ref
进行释放,如果捕获的对象是引用类型则继续调用_Block_object_dispose
进行普通对象类型的释放。- 堆上的
byref
需要release
,栈上的不需要release
。 -
ref
引用计数-1
,如果引用计数减到了0
,会返回true
,表示ref
需要被销毁。 - 如果
ref
持有的是引用类型则调用ref
的dispose
函数,内部其实调用了_Block_object_dispose
参数是objc
。 - 释放空间。
- 堆上的
- 2.1.1 普通对象类型(
- 2.2 销毁
block
实例,释放空间。
- 2.1 先调用
-
__block
修饰的引用类型的三层dispose
与copy
完全相反:- 先释放
ref
持有的objc
。 - 释放
Block_byref
。 - 释放
block
本身,释放内存空间。
- 先释放
- 1.
-
strong
与copy
修饰block
最终都会调用_Block_copy
,只不过strong
修饰的会先调用objc_retainBlock
。