OC底层 - Block的分析
前言
一、Block的类型
二、block解决循环引用的方式
三、block捕获变量
四、block修改局部变量
五、block的内存变化分析
六、block的底层结构
七、Block_copy分析
八、block捕获外界变量时的三层拷贝
九、问题补充
十、流程总结
今天来具体的总结一下Block的相关知识点。因为Block在日常的开发中会经常使用Block,所以很有必要了解它的底层实现原理。
一、Block的类型
1、NSGlobalBlock
- (void)test1 {
void(^block)(void) = ^{
};
NSLog(@"%@",block);
}
此时打印出来的block,就是__NSGlobalBlock__,此时block在内存的全局区。
2、NSMallocBlock
- (void)test2 {
int a = 1;
void(^block)(void) = ^{
NSLog(@"%i",a);
};
NSLog(@"%@",block);
}
当block捕获外界变量时,此时打印为__NSMallocBlock__,此时block在内存的堆区。
3、NSStackBlock
在早期的版本,当block捕获外界变量,且没有对block进行copy的时候,是NSStackBlock类型。但是目前的版本系统进行了一次copy,所以栈的block基本在应用层出现的很少。
- (void)test3 {
int a = 1;
void(^ __weak block)(void) = ^{
NSLog(@"%i",a);
};
NSLog(@"%@",block);
}
将block去掉底层的强持有,此时打印就为__NSStackBlock__,此时block在内存的栈区。
二、block解决循环引用的方式
说到循环引用,其实就是你中有我,我中有你。正常的释放流程如下:

循环引用的流程如下:

打破循环引用方式
1、使用weak(弱引用)
- (void)test1 {
self.name = @"1";
__weak typeof(self)weakSelf = self;
self.block = ^(void) {
NSLog(@"%@",weakSelf.name);
};
self.block();
}
- (void)dealloc {
NSLog(@"释放");
}
以上demo的引用情况如下:
self->block->weakSelf->self
self引用了block,block引用了wealSelf(被block捕获的变量是__weak),weakSelf引用了self(底层的weak表),没有对self的引用计数+1。
所以block->self这一层的持有就被打破。也就解决了循环引用。
继续看下面的demo:
- (void)test1 {
self.name = @"1";
__weak typeof(self)weakSelf = self;
self.block = ^(void) {
//模拟网络请求或其他的异步任务
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(2);
NSLog(@"%@",weakSelf.name);
});
};
self.block();
}
以上dealloc确实走了,但是打印的name为null,因为self已经被释放了。所以这里self的生命周期是不足以得到保证的,此时应该在用一个临时变量强引用一下weakself,等待异步任务执行完之后释放。
- (void)test1 {
self.name = @"1";
__weak typeof(self)weakSelf = self;
self.block = ^(void) {
__strong typeof(weakSelf)strongSelf = weakSelf;
//模拟网络请求或其他的异步任务
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(3);
NSLog(@"%@",strongSelf.name);
});
};
self.block();
}
以上strongSelf是一个临时变量,被gcd所持有,当gcd的异步任务执行完毕之后,临时变量释放,self的引用计数-1,也就释放了。
总结:使用 weak-strong 来解决循环引用。
2、利用临时变量(手动管理)
- (void)test2 {
self.name = @"test";
//self->block->vc->self
__block Test2ViewController *vc = self;
self.block = ^(void) {
NSLog(@"%@",vc.name);
vc = nil;
};
self.block();
}
以上,因为加了__block之后,block内对vc进行了持有,所以临时变量vc不会因为出作用域而释放。另外因为block内部持有的是地址指针(下面会具体分析),并没有对self本身引用计数+1,所以当调用self.block()时,将vc=nil,也就使本身self引用计数-1,从而消除了循环引用。
3、将self做为block的参数
- (void)test3 {
self.name = @"test";
self.block2 = ^(Test2ViewController *vc) {
NSLog(@"%@",vc.name);
};
self.block2(self);
}
以上将self作为block的参数传入,block内会当作临时变量处理,不会对它持有。从而解决循环引用。
总结,以上是3中解决block循环引用的方法,第一种用的最多。
三、block捕获变量
1、通过xcrun编译以下代码:
//xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main.cpp
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
void(^block)(void) = ^{
NSLog(@"test");
};
block();
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
编译之后的cpp整理如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
//函数式
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_12__nkjmvg1741fv4ynqnz517hr0000gn_T_main_145185_mi_0);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
void(*block)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA);
block->FuncPtr(block);
return 0;
}
block实际上就是一个结构体(__main_block_impl_0),其结构内部也有isa,所以block也是对象,也就能够被%@打印。
__main_block_impl_0这个结构体,其中一个参数就是__main_block_func_0,是一个执行函数,所以block在外层就是一个匿名函数,也叫代码块。
再看block的调用:block->FuncPtr(block); 这里之所以把block传进去,是因为这样可以在函数中获取block的数据。相当于oc的方法都有2个隐藏参数:self和cmd。
2、block捕获局部auto变量
通过xcrun编译下面代码:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
int a = 10;
void(^block)(void) = ^{
NSLog(@"%i",a);
};
block();
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
整理如下:
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy 只读
NSLog((NSString *)&__NSConstantStringImpl__var_folders_12__nkjmvg1741fv4ynqnz517hr0000gn_T_main_c25834_mi_0,a);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
int a = 10;
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);
block->FuncPtr(block);
return 0;
}
以上在block结构体中增加了变量a。在调用的时候,直接在函数内通过__cself获取a,然后进行值拷贝,从而打印。
如果此时对a进行修改:
int a = 10;
void(^block)(void) = ^{
a++;
NSLog(@"%i",a);
};
block();
因为block内部是值拷贝,对a进行++操作之后,并不会对原本的a进行++。所以此时会造成编译器的代码歧义,编译是不通过的。
3、block捕获局部静态变量
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
static int a = 10;
void(^block)(void) = ^(){
NSLog(@"%i",a);
};
block();
NSLog(@"%i",a);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
通过xcrun编译如下:
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_12__nkjmvg1741fv4ynqnz517hr0000gn_T_main_4824c8_mi_0,(*a));
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
static int a = 10;
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a));
block->FuncPtr(block);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_12__nkjmvg1741fv4ynqnz517hr0000gn_T_main_4824c8_mi_1,a);
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
可以看到block内部捕获的是a的地址指针。所以局部静态变量可以在block内进行修改。
4、block捕获全局变量
static int a = 10;
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
void(^block)(void) = ^(){
NSLog(@"%i",a);
};
block();
NSLog(@"%i",a);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
编译如下:
static int a = 10;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_12__nkjmvg1741fv4ynqnz517hr0000gn_T_main_ae4e59_mi_0,a);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
block->FuncPtr(block);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_12__nkjmvg1741fv4ynqnz517hr0000gn_T_main_ae4e59_mi_1,a);
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}
可以看到,block内部并没有捕获全局静态变量,而是直接使用,所以同样可以在block内部进行修改。把a写成全局变量(非静态)也是如此。
总结:
- (1)当block捕获局部auto变量时,会进行值拷贝。
- (2)当block捕获局部静态变量时,会进行指针拷贝。
- (3)当block不会捕获全局变量,而是直接使用。
四、block修改局部变量
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
__block int a = 10;
void(^block)(void) = ^{
a++;
NSLog(@"%i",a);
};
block();
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
我们都知道,想要在block内部修改局部变量,必须进行__block修饰,那么为什么经过__block修饰之后,就可以修改局部变量呢?
继续通过xcrun编译以上代码:
//byref的结构体
struct __Block_byref_a_0 {
void *__isa; //8
__Block_byref_a_0 *__forwarding; //8
int __flags; //4
int __size;//4
int a;
};
//block的结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//copy函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
//释放函数
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
//desc
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, char * argv[]) {
//将int a 封装成结构体
__Block_byref_a_0 a = {(void*)0,
(__Block_byref_a_0 *)&a, //指向结构体本身
0,
sizeof(__Block_byref_a_0),
10};
void(*block)(void) = &__main_block_impl_0(__main_block_func_0,
&__main_block_desc_0_DATA,
(__Block_byref_a_0 *)&a,
570425344));
block->FuncPtr(block);
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}
//函数体
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref 指针拷贝
//a->__forwarding 是内部copy出来的那个block_byref
(a->__forwarding->a)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_12__nkjmvg1741fv4ynqnz517hr0000gn_T_main_c6ba7d_mi_0,(a->__forwarding->a));
}
以上是整理之后的完整的cpp代码。可以看到变量a,被封装成了__Block_byref_a_0结构体。而被block捕获的就是这个__Block_byref_a_0结构体指针。
在回调函数中,先获取block捕获到的__Block_byref_a_0指针,然后通过a(结构体指针)访问__forwarding,在通过__forwarding去修改a(值)。
由上可知,修改和读取的变量都是a->__forwarding->a,所以达到了在block内部修改局部变量的目的。
总结:
(1)block修改局部变量时,会把局部变量封装成相应的结构体。
(2)生成的结构体保存了原来变量的指针和值。
(3)block对其结构体指针进行捕获。
五、block的内存变化分析

首先在这个位置打一个断点,然后看汇编:
0x1064d13d0 <+16>: leaq 0x3d79(%rip), %rdi ; __block_literal_global
此时读寄存器register read rdi,然后打印:
(lldb) register read rdi
rdi = 0x00000001064d5150 BlockTest`__block_literal_global
(lldb) po 0x00000001064d5150
<__NSGlobalBlock__: 0x1064d5150>
signature: "v8@?0"
invoke : 0x1064d1400
//....
(lldb)
可以看到当block没有捕获外界局部非静态变量的时候,就是__NSGlobalBlock__。
接着让block捕获一个局部变量:
int a = 0;
void (^block)(void) =^{
NSLog(@"%i",a);
};
block();
在这里打一个断点,然后register read rdi

<__NSStackBlock__: 0x7ff7b9b84828>
signature: "v8@?0"
invoke : 0x1063793e0
//...
此时block的类型就变成了__NSStackBlock__。
继续跳过objc_retainBlock断点之后,register read rax,此时block的类型变成了:
<__NSMallocBlock__: 0x600002564de0>
signature: "v8@?0"
invoke : 0x1063793e0
//...
通过分析,objc_retainBlock内调用了_Block_copy函数之后,block的类型才变成__NSMallocBlock__。
以上就是block在内存中的变化。
六、block的底层结构
首先block的结构如下:
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor; //
// imported variables
};
block的底层结构就是Block_layout:
(1)它有一个isa指针,指向不同的block类型。
(2)flags是一个标识符
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime 是否正在析构
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime 引用计数的mask
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
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler //是否有签名
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
(3)reserved 保留字段
(4)invoke 函数指针,就是block的调用
(5)descriptor block的描述(重点)
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;//block的大小
};
// 可选
#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_descriptor_2是通过flags标记中的BLOCK_HAS_COPY_DISPOSE来判断是否存在。
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
//通过内存平移得到Block_descriptor_2
return (struct Block_descriptor_2 *)desc;
}
Block_descriptor_3是通过flags标记中的BLOCK_HAS_SIGNATURE来判断是否存在。
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
return (struct Block_descriptor_3 *)desc;
}
block的结构分析完之后,尝试打印出来block的方法签名:
省略打断等操作,直接来到读取寄存器操作,拿到block的内存地址:
(lldb) register read rax
rax = 0x0000600000071b00
po 0x0000600000071b00 打印出block
<__NSMallocBlock__: 0x600000071b00>
signature: "v8@?0"
invoke : 0x10b1073e0//...
然后x/8gx读出block的内存情况:
(lldb) x/8gx 0x0000600000071b00
0x600000071b00: 0x00007ff866687ed8 0x00000000c1000002
0x600000071b10: 0x000000010b1073e0 0x000000010b10b130
0x600000071b20: 0x00007ff800000000 0x0000600000001c20
0x600000071b30: 0x0000000114b24688 0x0000000000000000
由block的内存结构可知,isa占8字节、flags和reserved共占8字节、invoke占8字节,所以descriptor1的内存地址为:0x000000010b10b130。
因为descriptor2和descriptor3都是通过descriptor1平移得到,所以打印0x000000010b10b130的内存情况:
(lldb) x/8gx 0x000000010b10b130
0x10b10b130: 0x0000000000000000 0x0000000000000024
0x10b10b140: 0x000000010b10a2f5 0x000000010b10a35d
0x10b10b150: 0x0000000000000000 0x0000000000000028
0x10b10b160: 0x000000010b107ca0 0x000000010b107ce0
因为descriptor1中reserved和size都是指针类型,所以各占8字节,那么0x000000010b10a2f5要么是BlockCopyFunction,要么是signature。
- 通过 BLOCK_HAS_COPY_DISPOSE = (1 << 25) 判断是否有dispose函数。
用 (1 << 25) & 0x00000000c1000002(block的flags,reserved为0),得到结果为0,所以它没有descriptor2。 - 通过BLOCK_HAS_SIGNATURE = (1 << 30) 判断是否有签名。
用 (1 << 30) & 0x00000000c1000002 得到 (unsigned int) $5 = 0x40000000,证明它有descriptor3。
所以0x000000010b10a2f5一定是signature。
(lldb) p (char *)0x000000010b10a2f5
(char *) $0 = 0x000000010b10a2f5 "v8@?0"
此时完美验证。注意@?是blockobjc类型(block类型)
七、Block_copy分析
接着来分析,block是如何从栈区拷贝到堆区的。直接看源码:
// 栈 -> 堆
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock; // 不需要直接返回
}
else { // 栈
// Its a stack block. Make a copy.
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
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;
#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);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}
可以看到,传进来的参数arg被强转为Block_layout。然后判断是否需要释放,是否是全局block。
重点是当为栈block的时候:
(1)根据block的小大(aBlock->descriptor->size),在堆中申请内存,将栈block深拷贝到堆中。
(2)将invoke指向原来栈中的invoke,为了能调用原来的函数。
(3)设置flags标记。
(3)调用_Block_call_copy_helper函数,执行Block_descriptor_2中的copy函数。
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
if (!desc) return;
(*desc->copy)(result, aBlock); // do fixup
}
(4)将isa设置为_NSConcreteMallocBlock
八、block捕获外界变量时的三层拷贝
通过上面的Block_copy的分析可知,在_Block_call_copy_helper函数中,调用了Block_descriptor_2中的copy函数。
再通过上面分析的cpp代码可知:
//copy函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
//这里的magicGeng是block_byref
_Block_object_assign((void*)&dst->magicGeng, (void*)src->magicGeng, 8/*BLOCK_FIELD_IS_BYREF*/);}
copy函数会调用_Block_object_assign,接下来就分析Block_object_assign函数。
1、Block_object_assign
//destArg 是空指针 或者 是已经copy的
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:
/*******
id object = ...;
[^{ object; } copy];
********/
// objc 指针地址 weakSelf (self)
// 交给arc (这里其实是交给了系统级别的ARC进行相关的处理。)
_Block_retain_object(object);
// 持有地址指针
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
// block 被一个 block 捕获
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
//__block修饰的变量,开始走到了这里
/*******
// 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;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
//__block修饰的变量,最终走到了这里
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
*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:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/
*dest = object;
break;
default:
break;
}
}
首先传进来的2个参数:destArg是待拷贝(或者是已经拷贝过block_byref)的指针,一个是通过编译器生成的原block_byref的指针。
先看一下Block_byref的结构:
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
isa不用管,因为在上面编译后的cpp代码中被复制为0。
forwarding,开始指向的是自身,这个下面具体分析。
flags,标记位:
enum {
// Byref refcount must use the same bits as Block_layout's refcount.
BLOCK_BYREF_LAYOUT_MASK = (0xf << 28), // compiler //掩码
BLOCK_BYREF_LAYOUT_EXTENDED = ( 1 << 28), // compiler
BLOCK_BYREF_LAYOUT_NON_OBJECT = ( 2 << 28), // compiler
BLOCK_BYREF_LAYOUT_STRONG = ( 3 << 28), // compiler
BLOCK_BYREF_LAYOUT_WEAK = ( 4 << 28), // compiler
BLOCK_BYREF_LAYOUT_UNRETAINED = ( 5 << 28), // compiler
BLOCK_BYREF_IS_GC = ( 1 << 27), // runtime
BLOCK_BYREF_HAS_COPY_DISPOSE = ( 1 << 25), // compiler //是否有COPY、DISPOSE函数
BLOCK_BYREF_NEEDS_FREE = ( 1 << 24), // runtime //是否需要释放
};
size 就是Block_byref的大小。
继续看_Block_object_assign函数,会枚举一下标记,也就是数据类型:
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.
};
分以下几种情况处理:
- (1)当是BLOCK_FIELD_IS_OBJECT(对象类型)时,交给arc处理,然后进行地址指针的捕获。
比如block捕获了一个对象,那么就会被block持有,对象的引用计数就会+1,当block释放之后,才会取消对这个对象的引用。
补充:当用__weak修饰时,编译cpp代码如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *__weak weakself;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__weak _weakself, int flags=0) : weakself(_weakself) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{ _Block_object_assign((void*)&dst->weakself, (void*)src->weakself, 3/*BLOCK_FIELD_IS_OBJECT*/);}
可以看到,NSObject *__weak weakself;,然后交给_Block_object_assign函数,并且flag = BLOCK_FIELD_IS_OBJECT。此时因为是弱引用,不会对引用计数+1,所以__weak可以解决循环引用。(BLOCK_FIELD_IS_WEAK 这个标记注释写的很清楚,是仅在byref内使用)。
- (2)当是BLOCK_FIELD_IS_BLOCK(也就是block嵌套)时,继续走_Block_copy函数。
- (3)(重点)当时BLOCK_FIELD_IS_BYREF或者BLOCK_FIELD_IS_WEAK时,就走_Block_byref_copy
- (4)当时BLOCK_BYREF_CALLER|BLOCK_FIELD_IS_OBJECT 以及BLOCK_BYREF_CALLER|BLOCK_FIELD_IS_BLOCK 进行地址指针的捕获。
- (5)当时BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK 以及 BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK 时,同样的进行地址指针的捕获。
2、重点分析_Block_byref_copy
//arg是传给block的原数据(src->Block_byref)
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) {
// src points to stack
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
// 问题 - block 内部 持有的 Block_byref 所持有的对象 是不是同一个
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
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 {
//如果是捕获的基础数据类型走这里(比如捕获的是int a)
// Bitwise copy.
// This copy includes Block_byref_3, if any.
//把值拷贝到copy中 (src->size - sizeof(*src) 就是要拷贝数据的长度)
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;
}
核心流程如下:
(1)在堆中申请一片内存空间
(2)将copy的byref中的forwarding指向本身。
(3)将原来的byref中的forwarding指向copy。这样就可以达到在外层操作的forwarding是同一个对象。
(4)判断是否有copy和dispose,如果有则进行byref_keep和byref_destroy的赋值。如果没有证明是基础数据类型,直接进行内存拷贝。
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep; // 结构体 __block 对象
BlockByrefDestroyFunction byref_destroy;
};
struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};
(5)判断是否有BLOCK_BYREF_LAYOUT_EXTENDED(这个做为扩展)。
最后执行byref_keep函数。
那么byref_keep是执行的什么操作呢?
由上面分析的cpp代码可知:
//byref的结构体
struct __Block_byref_magicGeng_0 {
void *__isa;
__Block_byref_magicGeng_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *magicGeng;
};
__Block_byref_magicGeng_0 magicGeng = {(void*)0,(__Block_byref_magicGeng_0 *)&magicGeng,
33554432,
sizeof(__Block_byref_magicGeng_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
objc_msgSend((id)objc_getClass("NSObject"), sel_registerName("new"))};
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);
}
此时执行byref_keep,就是又一次执行了_Block_object_assign,此时传进去的参数是:
(1)已经拷贝的byref内存地址偏移40之后的对象
(2)原始的byref内存地址偏移40之后的对象。
另外重点是flags=131。
回顾上面的_Block_object_assign的枚举类型:
BLOCK_FIELD_IS_OBJECT | BLOCK_BYREF_CALLER = 131,也就是下面的判断:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
//__block修饰的变量,最终走到了这里
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
*dest = object;
break;
所以此时,会对外界被__block 修饰的那个对象进行一次指针持有,注意这里并没有增加其引用计数。所以这也就是为什么__block可以捕获并修改外界变量的原因。
当我们手动解决循环引用的时候,在block回调函数内进行置空,因为此时没有arc的参与,所以当取消指针引用的时候,对象的引用计数为0,即可释放。
总结:
(1)第一层copy是block自身的拷贝(从栈拷贝到堆)。(深拷贝)
(2)第二层copy是对编译器生成的block_byref的拷贝。(深拷贝)
(3)第三层是对block_byref内变量的拷贝。(浅拷贝)
九、问题补充
MRC下 __block 为什么可以解决循环引用?ARC下 __block为什么不能解决循环引用?
通过上面的底层分析,__blcok的原理已经非常的清晰了。关于这个问题,其实就是ARC和MRC机制的问题。
1、先来看MRC
demo如下:
- (void)testMRC {
MRCObject *objc = [MRCObject new];
[objc test];
}
//....
@interface MRCObject ()
@property (nonatomic, copy) void(^block)(void);
@end
@implementation MRCObject
- (void)test {
__block typeof(self)blockSelf = self;
self.block = ^() {
NSLog(@"%@",blockSelf);
};
self.block();
}
- (void)dealloc {
[super dealloc];
NSLog(@"释放了");
}
@end
MRCObject 是一个MRC的类,当我们加入了__block之后,block内部会对当前的blockSelf进行指针持有,并没有对引用计数+1(上面分析的__Block_byref_id_object_copy_131)。所以__block在MRC下是可以解决循环引用的。
但是这里有个坑点,当我把代码改成:
- (void)test {
__block typeof(self)blockSelf = self;
self.block = ^() {
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"%@",blockSelf);
});
};
self.block();
}
此时发生了崩溃,因为block内部对当前blockSelf进行的引用相当于是unsafe_unretained的。当延时函数执行时,当前的blockSelf已经被释放了,所以造成了野指针。
改为一下代码:
- (void)test {
__block typeof(self)blockSelf = [self retain];
self.block = ^() {
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"%@",blockSelf);
[blockSelf release];
});
};
self.block();
}
此时就解决了野指针问题,这种解决方案和上面分析的ARC下手动解决循环引用是一样的。
2、来看ARC
@interface ARCObject ()
@property (nonatomic, copy) void(^block)(void);
@end
@implementation ARCObject
- (void)test {
__block typeof(self)blockSelf = self;
self.block = ^() {
NSLog(@"%@",blockSelf);
blockSelf = nil;
};
self.block();
}
- (void)dealloc {
NSLog(@"释放了");
}
这种情况上面就分析过,必须在block内部手动调用blockSelf = nil,才能解决循环引用。
(1)blockSelf在进行赋值的时候,ARC机制会进行一次retain,没有问题。
(2)block内部会对blockSelf进行指针持有,不会对引用计数+1,这个也没有问题。
(3)问题是blockSelf出了作用域,blockSelf不会进行release吗?
假如进行了release,那么我们根本不用进行手动调用blockSelf = nil。调试结果证明,blockSelf出来作用域并没有release,猜想应该是ARC机制的判断。
总结:
(1)MRC下, __block可以解决循环引用,因为block内部只进行指针持有,但是可能会出现野指针问题。
(2)ARC下,__block不能彻底解决循环引用,因为ARC机制会对__block的对象进行一次retain操作,并且出了作用域不会release,所以需要手动在block回调中进行置空的操作才能解决循环引用。
十、流程总结





