block
在我们的代码中经常使用,通过block
我们实现了高内聚、低耦合
,极大的方便了我们的编程,今天我们探究一下block
的底层原理。
什么是block?
block
是将函数及其执行上下文封装起来的对象。
通过
clang
编译查看底层代码
其中
ViewController
是文件名,viewDidLoad
是方法名
-
搜索
__ViewController__viewDidLoad_block_impl_0
-
搜索
__ViewController__viewDidLoad_block_func_0
-
搜索
__block_impl
block
内部有isa
指针,所以其本质也是OC
对象,他有4个属性 isa
Flags
Reserved
FuncPtr
保存了方法实现的地址
所以说block
是将函数及其执行上下文封装起来的对象。
既然block
内部封装了函数,那么它同样也有参数和返回值。
block的类型
block
有3种类型,全局block(__NSGlobalBlock__)
、栈block(__NSStackBlock__)
和堆block(__NSMallocBlock__)
- 全局
block
不使用外部变量的block
是全局block
void (^block) (void) = ^ {
NSLog(@"%d",3);
};
NSLog(@"%@",block);
打印结果:
- 栈
block
使用外部变量,并且未进行copy操作的是栈block
int a = 10;
NSLog(@"%@",[^ {
NSLog(@"%d",a);
} class]);
打印结果:
- 堆
block
使用外部变量,并且进行了copy操作的是堆block
int a = 10;
void (^block) (void) = ^ {
NSLog(@"%d",a);
};
NSLog(@"%@",[block class]);
打印结果:
访问了外部变量,并且强引用的是堆block
block的变量捕获
- 用
clang
编译
结构体的构造方法,多了一个参数a
结构体中的变量也增加了一个
a
,而且是在编译时就自动生成了。
函数方法中,用一个新的变量赋值,是值拷贝。
前面的a
并不是我们写入block
中的a
,这也就解释了局部变量可以被捕获,但是不能被修改的原因。要想修改a
的值,需要用__block
修饰。
-
__block
再次用clang
编译
在_I_ViewController_viewDidLoad
方法中,多了一行。多了一个__Block_byref_a_0
结构体。 -
查看
__Block_byref_a_0
结构体中有一个a
,__forwarding
保存的是a
的地址 -
查看
__ViewController__viewDidLoad_block_impl_0
__ViewController__viewDidLoad_block_impl_0
中多了一个__Block_byref_a_0
类型的a
-
再看
__ViewController__viewDidLoad_block_func_0
函数传入的是_a->__forwarding
,所以这里是指针拷贝,可以直接修改a
的值。
block的循环引用
block
的循环引用是经常出现的问题,请看下一段代码,dealloc
方法在ViewController
回退时是否会被调用?
@interface ViewController ()
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) dispatch_block_t block;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.name = @"Tom";
self.block = ^{
NSLog(@"%@",self.name);
};
self.block();
}
- (void)dealloc {
NSLog(@"dealloc 来了");
}
@end
当ViewController退出的时候,打印结果如下:
dealloc
方法没有调用,发生了循环引用。
self
持有block
,block
又捕获self.name
,self
和block
相互持有,发生循环引用。
解决办法
方案一:自动释放
解决办法相信大家也都知道,只需要在block
之前用__weak
修饰self
即可。
此时持有情况变为了
self -> block -> weakSelf -\-> self -> name
,而weakSelf
在一张弱引用表里,不会对self
的引用计数产生影响。
然而这样是有问题的
我们添加一段代码,延迟2秒打印,进去后直接出来。
dealloc
方法先走,才开始打印,这个时候VC
已经被释放了,所以打印为null
,self
的生命周期并没有得到保全。
- 添加一段代码,以上情况得到解决
打印结果
self
会在打印结束之后才释放。引用关系链就变成了下面这样:
self -> block -> strongSelf -> weakSelf -\-> self -> name
方案二:手动释放
使用__block
修饰符,在block
中手动释放VC
。
因为
auto
类型的局部变量可以被block
捕获,但不能修改,所以这里我们要借助__block
就可以在block
中修改了。方案三:VC作为参数传入
循环引用的原因是
self ->block -> self
,所以我们只要不直接持有VC
就可以解决问题。 把self通过传参的方式传进block
,就能解决问题。block的结构与签名
这里我们通过开启汇编,看一下block
的底层执行流程。
- 新建一个工程,写上如下代码
- (void)viewDidLoad {
[super viewDidLoad];
void (^block)(void) = ^{
NSLog(@"hello ");
};
block();
}
新建一个工程是因为Xcode
会对block
的流程进行优化缓存,暴露出来的信息会减少,不方便研究。
- 打上断点,开启
Always Show Disassembly
1. 没有引用外界变量
查看汇编
可以看到调用了一个
objc_retainBlock
方法
此时读寄存器rax
,block
是一个NSGlobalBlock
-
添加
objc_retainBlock
符号断点
可以看到objc_retainBlock
里面有一个_Block_copy
方法 -
添加
_Block_copy
符号断点
可以看到,调用了libsystem
库里的方法。 下载libclosure-74源码
-
搜索
_Block_copy
首先看到的是一个
Block_layout
的结构体,点击查看
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
这个结构与我们在clang
中看到的基本一致。
- 搜索查看
flags
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime 是否在析构
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler 是否有copy 和dispose
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
};
里面保存的是block
的一些标识位。
- 搜索查看
Block_descriptor_1
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
我们查询的时候还找到了相应的Block_descriptor_2
和Block_descriptor_3
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
这个Block_descriptor_1
是每个block
都有的,Block_descriptor_2
和Block_descriptor_3
不是每个block
都有的,要看它是否有copy
和dispose
方法,一级signature
和layout
。配合上面的标识符,判断会不会生成这两个方法。
-
查看验证
Block_descriptor_2
和Block_descriptor_3
getter
方法中,首先根据标识符判断,没有就会直接返回NULL
,有这两个结构体时,才会根据Block_descriptor_1
的地址进行内存平移得到。 -
回到汇编,执行完
objc_retainBlock
,通过LLDB
调试
详细的验证结果如下
block
的签名
v
:表示无返回值8
:参数大小为8@?
:是对象类型,是block类型0
:从0号位置开始
2. 捕获外界变量
-
查看
block
类型
此时是NSStackBlock
-
执行完
objc_retainBlock
断点
block
已经变为了NSMallocBlock
执行objc_retainBlock
中的_Block_copy
方法后,block
变为了NSMallocBlock
。
- 继续往下执行到
callq
读取SEL
失败,这里其实是一个_block_invoke
方法,control+step into
进入即可验证 - 进入
_block_invoke
从汇编可以看出,在这里执行了block
里面的NSLog
方法。
_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) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// 进来的block强转为Block_layout类型
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {//是否需要被释放
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {//为global_block时直接返回,不做处理
return aBlock;
}
else {
// 编译期不能直接生成堆block,这里只能是栈block
//栈block在这里copy
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);//在堆上开辟一块内存
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // 把原来的数据拷贝进来
#if __has_feature(ptrauth_calls)
//invoke也拷贝过来
result->invoke = aBlock->invoke;
#endif
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// 把isa的指向改为_NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
return result;
}
}
这个代码很简单。
block的三层拷贝
将以上代码通过
clang
编译,细心的同学会发现还有两段这样的代码block_copy_0
和block_dispose_0
一起组装成了block_desc_0_DATA
,保存进Block_layout
组,其实就是Block_descriptor_2
中的两个变量。- 搜索
_Block_object_assign
根据case分为几种情况,这里我们主要研究两种:
-
BLOCK_FIELD_IS_OBJECT
,普通id
对象 -
BLOCK_FIELD_IS_BYREF
,__block修饰的对象
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_FIELD_IS_OBJECT
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;
static void _Block_retain_object_default(const void *ptr __unused) { }
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object);
*dest = object;
break;
_Block_retain_object
什么都没有做,直接交给了ARC
处理
dest指针
指向了object
,对对象内存进行了持有,这也是block
强引用的原因。
BLOCK_FIELD_IS_BYREF
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/
*dest = _Block_byref_copy(object);
break;
只是调用了_Block_byref_copy
- 查看
_Block_byref_copy
static struct Block_byref *_Block_byref_copy(const void *arg) {
//Block_byref结构体,进来的变量在底层就是这个类型
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// 开辟内存
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;//一些赋值操作
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // forwarding 是外界变量的地址
src->forwarding = copy; // 这两句代码就是将外面变量a的指针,和里面copy都指向了新开辟的空间
copy->size = src->size;
//HAS_COPY_DISPOSE的时候
if (src->flags & BLOCK_BYREF_HAS_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
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;
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);
copy3->layout = src3->layout;
}
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
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) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}