在开发过程中,我们会经常使用到Block,今天就让我们来探究一下Block的实现。
一、NSConcreteGlobalBlock类型的block的实现
首先我们写一个最简单的Block,然后用clang -rewrite-objc
命令将其重写成C++的实现。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
void (^printBlock)(void) = ^{
printf("Hello, World!\n");
};
printBlock();
return 0;
}
上述代码通过clang -rewrite-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;
__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(int argc, const char * argv[]) {
void (*printBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)printBlock)->FuncPtr)((__block_impl *)printBlock);
return 0;
}
首先我们从两段代码相似度最高的地方入手,可以发现:
^{
printf("Hello, World!\n");
};
被转换成了:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello, World!\n");
}
我们可以看到Block花括号中的代码实际上是作为一个C语言的函数来处理的。多做几个实验可以发现,该函数名的前缀是Block所在的函数名(这里是main
),后缀是该Block在所在函数中出现的顺序值(这里是0
)。
该函数的参数__cself
是一个指向结构体__main_block_impl_0
的指针。该结构体的声明如下:
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;
}
};
这个结构体看起来有些复杂,但是当我们先除去它的构造函数并展开它嵌套的两个结构体,就会发现它跟常规的结构体是一样的:
struct __main_block_impl_0 {
void *isa; // 指向该对象所属的类
int Flags; // 用于按位表示一些block的附加信息
int Reserved; // 保留字段
void *FuncPtr; // 指向实现Block的函数的地址(这里即函数__main_block_func_0的地址)
size_t reserved; // 保留字段
size_t Block_size; // Block占用内存空间的大小
}
下面我们来看看结构体__main_block_impl_0
的构造函数:
__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;
}
它是在int main(int argc, const char * argv[])
函数中调用的:
void (*printBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
这段代码进行了很多转换,所以看起来比较复杂,下面我们来一步步地分析:
__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
首先是调用__main_block_impl_0
结构体的构造函数生成了一个__main_block_impl_0
结构体实例;
然后使用取地址符(&)获取该实例的地址;
最后将该地址赋值给__main_block_impl_0
结构体指针printBlock
。
上面的代码也可以转换成如下形式:
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
struct __main_block_impl_0 *printBlock = &tmp;
上述代码对应最初源码中的这一段:
void (^printBlock)(void) = ^{
printf("Hello, World!\n");
};
我们再来看构造函数的参数,在调用构造函数时传递了两个参数:指向__main_block_func_0
的函数指针和__main_block_desc_0
类型的结构体__main_block_desc_0_DATA
的地址。在函数的实现部分则将这两个参数分别赋值给__main_block_impl_0
类型结构体的成员变量FuncPtr
和desc
,用以进行结构体的初始化。现在就不难理解__main_block_func_0(struct __main_block_impl_0 *__cself)
函数中的参数__cself
了:它指向了将该函数指针作为成员变量的__main_block_impl_0
结构体的实例,相当于C++实例方法中指向实例自身的变量this,或者是OC实例方法中指向对象自身的变量self。
接下来在最初源码中调用了该Block:
printBlock();
对应转换后的这行代码:
((void (*)(__block_impl *))((__block_impl *)printBlock)->FuncPtr)((__block_impl *)printBlock);
去掉转换部分是这样:
(*printBlock->FuncPtr)(printBlock);
其实就是使用函数指针来调用函数,后面的括号中的printBlock
是参数,这也印证了上面对__cself
的解释。
现在可以确定,Block的实际上就是一个结构体,使用Block就是通过结构体中的成员变量,指向__main_block_func_0
函数的函数指针FuncPtr
来调用__main_block_func_0
函数。
在__main_block_impl_0
结构体中我们还有一个成员变量isa
一直没有说,它代表了Block的类型,有以下三种类型:
-
_NSConcreteGlobalBlock
,全局静态Block,它不会访问任何外部变量,我们前面研究的那个Block就是全局Block。虽然它的isa
指针指向的是_NSConcreteStackBlock
,但这是由于我们使用clang命令将OC的实现转换成C++的的实现方式和LLVM不同。实际上在代码运行过程中po
这个Block就会发现它是_NSConcreteGlobalBlock
; -
_NSConcreteStackBlock
,保存在栈中的Block,只存在于某个固定的作用域(如函数)当中,当超出这个作用域Block就会被销毁; -
_NSConcreteMallocBlock
,保存在堆中的Block,这种Block无法直接创建,是通过_NSConcreteStackBlock
拷贝到堆中而来,要在多个地方使用同一个栈上的Block时,就需要将Block从栈上拷贝到堆中,以防止Block在栈中被销毁。
另外,ARC对Block也有影响。在开启ARC的情况下,只会有_NSConcreteGlobalBlock
和_NSConcreteMallocBlock
,_NSConcreteStackBlock
将会被_NSConcreteMallocBlock
替代。
二、NSConcreteStackBlock类型的block的实现
接下来我们看看_NSConcreteStackBlock
的实现方式和_NSConcreteGlobalBlock
有什么不同。这两种Block的区别在于_NSConcreteStackBlock
将会捕捉外部变量:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {s
int a = 1024;
void (^printBlock)(void) = ^{
printf("Hello, World!\n%d\n",a);
};
printBlock();
return 0;
}
以上代码通过clang -rewrite-objc
命令转换后是这样的:
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
printf("Hello, World!\n%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 argc, const char * argv[]) {
int a = 1024;
void (*printBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)printBlock)->FuncPtr)((__block_impl *)printBlock);
return 0;
}
它与_NSConcreteGlobalBlock
源码不同的地方在于:
- 在实现Block的结构体
__main_block_impl_0
中多出了一个成员变量int a
; - 在
__main_block_func_0
函数中多了一行代码int a = __cself->a;
; - 在
main
函数调用__main_block_impl_0
的构造函数时增加了一个参数,main
函数中的局部变量a
。
现在Block捕获外部变量的过程就可以理解了:外部变量的值作为参数传递给__main_block_impl_0
结构体的构造函数,并在结构体中添加一个同名的成员变量来保存。在执行block花括号中的代码的过程其实就是调用__main_block_func_0
函数,这时__main_block_func_0
函数通过参数__cself
就可以获取外部变量的值。在这个过程中传递的是外部变量的值,这也是没有用__block
来修饰的外部变量不能在Block中修改的原因。
三、使用__block
修饰的外部变量的实现
如果使用了__block
修饰外部变量:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {s
__block int a = 1024;
void (^printBlock)(void) = ^{
printf("Hello, World!\n%d\n",a);
};
a += 1;
printBlock();
return 0;
}
那转换成C++后的源码又将大不相同:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
printf("Hello, World!\n%d\n",(a->__forwarding->a));
}
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*/);}
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, const char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1024};
void (*printBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
(a.__forwarding->a) += 1;
((void (*)(__block_impl *))((__block_impl *)printBlock)->FuncPtr)((__block_impl *)printBlock);
return 0;
}
这与没有使用__block
修饰的外部变量的源码不同的地方是__main_block_impl_0
结构体中的int a;
变成了__Block_byref_a_0 *a;
,一个指向__Block_byref_a_0
结构体的指针。在main
函数中声明了一个__Block_byref_a_0
结构体变量a并为它赋值:
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1024};
与它对应的OC代码是:
__block int a = 1024;
带有__block
修饰的局部变量a转换成了一个__Block_byref_a_0
类型的结构体变量a
,结构体中保存了该结构体变量的地址和变量a
的值。下一行代码同样是通过构造函数使__main_block_impl_0
结构体变量保存了结构体变量a
的地址。
void (*printBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
这样,在__main_block_func_0
函数中就可以获取到原来局部变量a
的值了。同时在Block外,main
函数中访问局部变量a
的值得方式也发生了改变。它同__main_block_func_0
函数中一样,同样是通过__Block_byref_a_0
结构体变量中指向自己的结构体指针来访问的:
(a.__forwarding->a) += 1;
总结成一句话:使用__block
修饰的外部变量在Block中能被修改是因为Block是通过指针访问的,而没有使用__block
修饰的外部变量,仅仅是将它的值拷贝到了Block中。
四、NSConcreteMallocBlock类型的block的实现
另外,在__main_block_desc_0
结构体中还多了两个成员变量:指向__main_block_copy_0
函数的函数指针copy
和指向__main_block_dispose_0
函数的函数指针dispose
。根据函数名和实现可以确定它们跟Block的拷贝有关,那就先来看看将Block从栈上拷贝到堆中是如何实现的。拷贝操作需要调用Block_copy()
函数,在Block.h文件中可以找到它的定义:
#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
Block_copy
是一个宏定义,它将参数进行了强制类型转换然后传给了_Block_copy
函数。在LLVM源码的runtime.c文件中可以看到它的实现。这个函数的作用是在堆中创建一个Block的拷贝,或者为一个已经在堆中的Block添加引用。需要注意的是它必须和Block_release
成对出现以恢复内存。
void *_Block_copy(const void *arg) {
return _Block_copy_internal(arg, WANTS_ONE);
}
_Block_copy
函数又将Block_copy
函数传入的Block和WANTS_ONE
作为参数调用了_Block_copy_internal
函数。在的runtime.c的286~355行可以找到_Block_copy_internal
的实现(删除了垃圾回收相关的代码并添加注释):
/* Copy, or bump refcount, of a block. If really copying, call the copy helper if present. */
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
// 判断如果参数为`NULL`则直接返回
if (!arg) return NULL;
// 将参数从指针还原成Block结构体
aBlock = (struct Block_layout *)arg;
// 如果flags中包含BLOCK_NEEDS_FREE,则说明这个Block在堆上,于是通过latching_incr_int函数将引用计数加1,这里可以判断出,Block结构体的flags中包含了Block类型和引用计数等信息
if (aBlock->flags & BLOCK_NEEDS_FREE) {
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 如果这是一个全局Block,就什么也不做
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// 代码运行到这可以确定这是一个在栈上的Block了,于是在堆上开辟一块和Block对应大小的空间,失败则返回0
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
// 将原来的的Block按位拷贝到新开辟的内存空间
memmove(result, aBlock, aBlock->descriptor->size);
// 修改堆上Block的flags,重置Block的类型信息和引用计数
result->flags &= ~(BLOCK_REFCOUNT_MASK);
result->flags |= BLOCK_NEEDS_FREE | 1;
// 将Block的isa指针设置为_NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
// 如果存在,则调用Block的辅助拷贝函数
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
在这段代码的最后判断了Block的结构体实例中是否存在一个copy
函数,如果存在,则会以指向堆上Block结构体实例的指针和指向栈上结构体实例的指针为参数调用copy
函数。这个copy
函数就是我们之前发现在__main_block_desc_0
结构体中多出来的两个成员变量中的一个。下面来看看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*/);
}
在__main_block_copy_0
函数只是简单的调用了_Block_object_assign
。根据在_Block_copy_internal
函数的源码调用copy
时传的传递的参数可知_Block_object_assign
的参数分别是堆上Block结构体实例中保存外部变量的成员变量的地址和栈上Block结构体实例中保存的外部变量和一个用来表示外部变量类型的常数。在runtime.c文件中我们同样可以找到_Block_object_assign
函数的实现(省略了无关代码):
void _Block_object_assign(void *destAddr, const void *object, const int flags) {
if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF) {
_Block_byref_assign_copy(destAddr, object, flags);
}
}
在这个函数中调用了_Block_byref_assign_copy
函数。这个函数的作用是将栈上__block
修饰的变量(也就是__Block_byref_a_0
结构体实例)拷贝到堆中。以下是它的实现(删除了垃圾回收相关代码)。
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
struct Block_byref **destp = (struct Block_byref **)dest;
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// 在堆中开辟一块与传入的Block_byref结构体相同大小的内存空间
struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
// 设置堆中Block_byref结构体的flags的值
copy->flags = src->flags | _Byref_flag_initial_value;
// 使forwarding指针指向自己
copy->forwarding = copy;
// 设置栈上的Block_byref结构体中的forwarding指针指向堆中的Block_byref结构体,这样无论通过哪个结构体的forwarding指针,访问到的都是堆上的Block_byref结构体
src->forwarding = copy;
// 设置堆中Block_byref结构体的size的值
copy->size = src->size;
}
// 已经在堆中,增加引用计数
else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
// 使通过参数传进来的结构体指针指向将堆上Block_byref结构体,即持有`__block`修饰的变量
_Block_assign(src->forwarding, (void **)destp);
}
在上文中我们提到过Block_copy
必须和Block_release
成对出现以恢复内存。那么Block_release
的作用就不言而喻,它在我们不再使用Block的时候将其释放,同样在Block_release
中也会调用__main_block_desc_0
结构体中的dispose
函数,用来释放被Block持有的__block
修饰的变量。
通过上面的分析可以知道,在Block从栈上拷贝到堆中的过程中,Block中使用的__block变量
同样会被从栈上拷贝到堆中。这点并不难理解,当需要将Block拷贝到堆上时,很多时候是因为要在其他地方使用这个Block,而此时很有可能已经超出了__block变量
的作用域。为了避免出现这样的问题,将__block变量
拷贝到堆中并由Block持有也是顺理成章的事了。
五、Block的循环引用
堆上的Block不光会持有__block变量
,同样也会持有在Block中使用的没有用__block
修饰的外部变量,这也是Block会出现循环引用问题的根源。因此在编码过程中,我们要避免出现Block和Block中使用的外部变量相互强引用的情况(这里所说的外部变量默认为是由__strong
修饰)。
__weak typeof(self) weakSelf = self;
void (^printBlock)(void) = ^{
NSLog(@"%@", weakSelf);
};
或者,在合适的时机打断它们之间的相互强引用。
__block blockSelf = self;
void (^printBlock)(void) = ^{
NSLog(@"%@", blockSelf);
blockSelf = nil;
};
但是这种方法有一个缺点,那就是:为了防止循环引用,必须执行Block。
网友评论