block

对应结构体的定义如下:
objective-c
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
图中我们可以看到,isa其实有六个部分
-
isa指针,所以对象都有isa指针。这就证明了block其实本质上就是一个Objective-C对象 ,他的值通常是这三种
-
_NSConcreteGlobalBlock(全局区:没捕获任何外部变量) -
_NSConcreteStackBlock(栈区:捕获了变量,但还没被强引用) -
_NSConcreteMallocBlock(堆区:被拷贝到了堆上,生命周期由你掌控)
-
-
- block如果不访问自由变量的话,都是存储在全局区的,如果访问全局变量的话,也是存储在全局区的Block
- block如果访问自由变量的话
- 如果没有创建block变量,才会创建一个栈区Block变量
- 创建了一个Block变量,且访问自由变量,才会创建出一个堆区的Block,这里创建出堆区Block的原因是 栈区的Block执行了拷贝操作
-
flags,他是有个int整型数字,按bit来存储Block的各种附加状态。Runtime需要靠它来判断这个Block的特性
-
Reserved,一个保留字段 基本没啥用 主要是为了内存对齐
-
invoke,这是一个函数指针 当你在大括号里写下逻辑时,编译器会把括号里的代码抽离出来变为一个普通的C语言函数。invoke指向这个函数的内存地址。当你调用
myBlock()时,底层实际上执行的是myBlock->invoke(myBlock, ...)。它把 Block 自己作为第一个参数传了进去 因为只有把 Block 自己传进去,里面的代码才能拿到储存在 Block 尾部的那些"捕获变量"。 -
descriptor,他是只想另一个结构体的指针,记录这个Block的辅助信息:size,copy函数指针(当 Block 从栈拷贝到堆(Malloc)时,调用它来保住捕获的对象(比如执行
retain)),dispose函数指针(当 Block 从堆上销毁时,调用它来释放捕获的对象(比如执行release)。) -
variables,capture过来的变量,block能够访问它外部的局部变量,就是因为这些变量(或其地址)复制到了结构体中。他是6个部分中唯一动态变化 的部分,前面的 1~5 是所有 Block 都有的"固定头部",而第 6 部分是直接拼贴在描述信息之后的内存块。block的灵魂就在这里,如果block用到了外部的int a和NSString *b,编译器会在这个位置塞入a的值和b的指针。
_NSConcreteGlobalBlock
全局的静态block,不会访问任何外部变量,就是NSConcreteGlobalBlock。如下所示:他是一个全局的block,在编译期间就已经决定了。
objective-c
#include <stdio.h>
int main()
{
^{ printf("Hello, World!\n"); } ();
return 0;
}
经过clang命令后 其中关键代码如下:
objc
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 构造函数,用来初始化block
__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) {
printf("Hello, World!\n");
}
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()
{
(void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
return 0;
}
__main_block_impl_0就是该block的实现
- 一个block实际就是一个对象,他主要由一个isa和一个impl和一个descriptor组成。
- 由于 clang 改写的具体实现方式和 LLVM 不太一样,并且这里没有开启 ARC。所以这里我们看到 isa 指向的还是
_NSConcreteStackBlock。但在 LLVM 的实现中,开启 ARC 时,block 应该是 _NSConcreteGlobalBlock 类型,具体可以看 《objective-c-blocks-quiz》 第二题的解释。 - impl 是实际的函数指针,本例中,它指向 __main_block_func_0。这里的 impl 相当于之前提到的 invoke 变量,只是 clang 编译器对变量的命名不一样而已。
- descriptor 是用于描述当前这个 block 的附加信息的,包括结构体的大小,需要 capture 和 dispose 的变量列表等。结构体大小需要保存是因为,每个 block 因为会 capture 一些变量,这些变量会加到 __main_block_impl_0 这个结构体中,使其体积变大。在该例子中我们还看不到相关 capture 的代码,后面将会看到。
_NSConcreteStackBlock
保存在栈的block,当函数返回时被销毁。可以这么理解,他就是引用了外部变量的block
objective-c
#include <stdio.h>
int main() {
int a = 100;
void (^block2)(void) = ^{
printf("%d\n", a);
};
block2();
return 0;
}
转化后代码如下,NSConcreteStackBlock内部会有一个结构体__main_block_impl_0,这个结构体会保存外部变量,使其体积变大。而这就导致了NSConcreteStackBlock并不像宏一样,而是一个动态的对象。而它由于没有被持有,所以在它的内部,它也不会持有其外部引用的对象。
objective-c
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a; //多了一个被捕获的变量a
// 构造函数,cpp的初始化列表中,它把外面传进来的100,复制了一份
__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(复制过来的)
printf("%d\n", 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 a = 100;
void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
return 0;
}
- __main_block_impl_0 中增加了一个变量 a,在 block 中引用的变量 a 实际是在申明 block 时,被复制到
__main_block_impl_0结构体中的那个变量 a。因为这样,我们就能理解,在 block 内部修改变量 a 的内容,不会影响外部的实际变量 a。 - __main_block_impl_0 中由于增加了一个变量 a,所以结构体的大小变大了,该结构体大小被写在了
__main_block_desc_0中。 __main_block_func_0中的a是值拷贝,如果此时在block内部实现中作 a++操作,是有问题的,会造成编译器的代码歧义,即此时的a是只读的.
总结: block捕获外部变量时,在内部自动生成同一个属性来保存

我们在变量前面加一个__block关键字后,重新观察生成代码:
objective-c
struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding; //新增结构体,用于保存我们要capture并且修改变量的i
int __flags;
int __size;
int i;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // by ref 达到修改外部变量的作用
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_i_0 *i = __cself->i; // bound by ref
printf("%d\n", (i->__forwarding->i));
(i->__forwarding->i) = 1023;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
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*); //Block被拷贝到堆上时,把内部的i也copy进去
void (*dispose)(struct __main_block_impl_0*);// Block从堆上销毁时,把i一同销毁
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main()
{
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};
void (*block1)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344);
((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);
return 0;
}
- 我们观察到新增了一个名为 __Block_byref_i_0 的结构体,用来保存我们要 capture 并且修改的变量 i的指针和值。他的里面有一个isa指针,也就意味着他变成了一个对象。这样在Block从栈被拷贝到堆的时候,底层才能通过引用计数来管理他的生命周期。
- __main_block_impl_0 中引用的是 __Block_byref_i_0 的结构体指针,这样就可以达到修改外部变量的作用。
- 我们需要负责 __Block_byref_i_0 结构体相关的内存管理,所以 __main_block_desc_0 中增加了 copy 和 dispose 函数指针,对于在调用前后修改相应变量的引用计数。
- (i->__forwarding->i) = 1023; 为什么要这么绕一圈? 刚执行在栈上时,这个指针指向他自己,forwarding相对于在改变自己的内存。当Block被拷贝到堆上(比如使用GCD异步)时,系统会把这个
__Block_byref_i_0结构体原封不动复制在堆上一份,此时有两个i结构体,一个在栈一个在堆,系统会把那个旧结构体的forwarding指针强行掰弯,让他指向堆上的那个新结构体,同时,堆上新结构体的__forwarding指针指向他自己。这样无论从外面栈上的作用域去访问 还是从Block内部(堆上的作用域)去访问i,所有人都要经过i->__forwarding->i,所以对最终访问到的,永远是堆上的同一块内存。

_NSConcreteMallocBlock
保存在堆上的block,当引用计数为0时会被销毁。NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,因为默认它是当一个 block 被 copy 的时候,才会将这个 block 复制到堆中。以下是一个 block 被 copy 时的示例代码 (来自 这里),可以看到,在第 8 步,目标的 block 类型被修改为 _NSConcreteMallocBlock。
objective-c
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
// 1
if (!arg) return NULL;
// 2
aBlock = (struct Block_layout *)arg;
// 3
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 4
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// 5
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
// 6
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// 7
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;
// 8
result->isa = _NSConcreteMallocBlock;
// 9
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
变量的复制
对应block外的变量引用,block默认是将其复制到其数据结构中实现访问,

对于用__block修饰的外部变量引用,block通过复制其地址来实现

Block的三层拷贝
想要分析block的三层copy,首先需要知道外部变量的种类有哪些,其中用的最多的是BLOCK_FIELD_IS_OBJECT和BLOCK_FIELD_IS_BYREF
objc
// Runtime support functions used by compiler when generating copy/dispose helpers
// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
// see function implementation for a more complete description of these fields and combinations
//普通对象,即没有其他的引用类型,也就是我们任何的一个Object都是这个逻辑
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
//block类型作为变量,block套block
BLOCK_FIELD_IS_BLOCK = 7, // a block variable
//经过__block修饰的变量和对象
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
//weak 弱引用变量
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers
//返回的调用对象 - 处理block_byref内部对象内存会加的一个额外标记,配合flags一起使用
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};
__Block_copy_internal
objc
static void *_Block_copy_internal(const void *arg, const int flags)
{
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
if (!arg) return NULL;
aBlock = (struct Block_layout *)arg; // 强制转化成Block_layout对象,防止对外界造成影响
if (aBlock->flags & BLOCK_NEEDS_FREE) // NSConcreteMallocBlock
//是否需要释放
{
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) // NSConcreteGlobalBlock
//如果是全局block,直接返回
{
return aBlock;
}
// 为栈block或者是堆区block,由于堆区需要申请内存,所以是栈区的block的操作
// Its a stack block. Make a copy.
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
//通过memmove内存拷贝,将aBlock按位拷贝到result
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;
result->isa = _NSConcreteMallocBlock; //isa指针被重写 变身堆block
if (result->flags & BLOCK_HAS_COPY_DISPOSE) //如果 Block 捕获了对象(比如强引用了 self),或者使用了 __block 变量,它的 flags 里就会带有 BLOCK_HAS_COPY_DISPOSE 标记。
{
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
// 对__block对象 把它也拷贝到堆上,并把那个forwarding指针指向堆区新位置
}
return result;
}
这个copy就是栈block到堆block到核心,无论你在上层写 [block copy] 还是 ARC 帮你自动 copy,全天下的 Block 最终都要流经这个。
__Block_object_assign
只有当你的 __block 修饰的不仅是个基本类型,而是一个真正的 Objective-C 对象 时(比如 __block NSObject *obj),才会触发这最深的一层。现在Block_byref 结构体已经搬到堆上了,它里面还装着一个 NSObject *obj 的指针!这个 NSObject 可是由 ARC(引用计数)管理的。既然堆上的 Block_byref 现在持有了它,就必须对它负责!堆上的 Block_byref 结构体会调用 _Block_object_assign,并传入代号 BLOCK_FIELD_IS_OBJECT (值为 3)。系统一看,懂了,对肚子里这个真正的 OC 对象执行一次 retain 操作(引用计数 +1),确保它不会被提前释放。
- 如果是普通对象(3 - IS_OBJECT),则交给
系统arc处理,并拷贝对象指针,即引用计数+1,所以外界变量不能释放 - 如果是
block类型的变量(7 - IS_BLOCK),则通过_Block_copy操作,将block从栈区拷贝到堆区 - 如果是
__block修饰的变量(8 - IS_BYREF),调用_Block_byref_copy函数 进行内存拷贝以及常规处理 - 如果是弱引用(16 - IS_WEAK),他只做弱引用绑定,不增加引用计数,防止循环引用
__Block_byref_copy
如果block的标识位里有_Block_byref_copy有 BLOCK_HAS_COPY_DISPOSE,并且捕获的变量带有 __block 前缀。发生第二层拷贝
objective-c
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 和 外界的Block_byref 所持有的对象是同一个,这也是为什么__block修饰的变量具有修改能力
//copy 和 scr 的地址指针达到了完美的同一份拷贝,目前只有持有能力
copy->forwarding = copy; // patch heap copy to point to itself // 把堆区的指针指向自己
src->forwarding = copy; // patch stack to point to heap copy // 把栈区的指针指向堆区,保证数值一致
copy->size = src->size;
//如果有copy能力
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) { // 有自己的一个copy的逻辑
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
//Block_byref_2是结构体,__block修饰的可能是对象,对象通过byref_keep保存,在合适的时机进行调用
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;
}
//等价于 __Block_byref_id_object_copy
(*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;
}
咱们之前说过,加了 __block 的变量会被编译器变成一个庞大的结构体(Block_byref)。这个结构体最开始也是在栈上的,也需要被搬到堆上面。这一层解决了__block变量在堆区的存活和数据同步问题,如果是类似__block int a = 10,拷贝到这里也结束了。
总结:
-
第一层通过_Block_copy实现对象的自身拷贝,从栈区拷贝到堆区
-
第二层通过调用_Block_byref_copy这个来实现对于对象拷贝成Block_byref类型
-
第三次调用_Block_object_assign对于__block修饰的当前变量内部对象的 内存 管理
当且仅当用__block变量的时候才会有三次拷贝.
循环引用
-
Weak wrong dance在这里不过多赘述,给出几行总结
-
如果block内部没有直接嵌套block,直接使用
__weak修饰self -
如果block内部嵌套block,需要同时使用
__weak和__strong
-
-
__block修饰符。我们可以采用
__block修饰符,在主动调用完后手动释放selfobjective-c__block PersonViewController* vc = self; self.testBlock = ^(void){ NSLog(@"%@", vc.name); vc = nil; }; self.testBlock(); -
对象self作为参数。主要是把对象self作为参数,提供给block内部使用,不会有引用计数问题
objective-c
self.testBlock = ^(PersonViewController* vc){
NSLog(@"%@", vc.name);
};
ACR与block类型的影响
在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
在上面介绍NSConcreteStackBlock的时候,是在ARC环境下跑的,而打印出来的日志明确的显示出,当时的block类型为NSConcreteStackBlock。
而实际上,为什么大家普遍会认为ARC下不存在NSConcreteStackBlock呢?
这是因为本身我们常常将block赋值给变量,而ARC下默认的赋值操作是strong的,到了block身上自然就成了copy,所以常常打印出来的block就是NSConcreteMallocBlock了。
原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。证明方式是以下代码在 XCode 中,会输出 <__NSMallocBlock__: 0x100109960>。在苹果的 官方文档 中也提到,当把栈中的 block 返回时,不需要调用 copy 方法了。
我个人认为这么做的原因是,由于 ARC 已经能很好地处理对象的生命周期的管理,这样所有对象都放到堆上管理,对于编译器实现来说,会比较方便。