iOS Objective-C Block底层原理
在上一篇文章中我们对Block
做了简单的介绍,下面我们通过这篇文章对Block
的底层原理进行探索。
首先提出问题:
-
Block
的本质是什么? -
Block
为什么需要调用block()
? -
Block
是如何截获外界变量的? -
__block
是如何实现的?
1. 通过Clang查看Block的底层实现
1.1 编译后的代码简单分析
要想知道Block
的底层实现,我们首先想到的就是通过Clang
编译一下Block
代码,然后看看其内部的实现。我们创建一个block.c
的文件,内部代码如下:
#include "stdio.h"
int main(){
void(^block)(void) = ^{
printf("hello block");
};
block();
return 0;
}
通过xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c
命令,将.c
文件编译成.cpp
文件,我们找到main
函数进行查看,编译后的形式如下:
int main(){
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
去除掉类型强转,可以将编译后的代码简化成如下形式:
int main(){
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
函数,该函数有两个参数,其中第一个参数__main_block_func_0
就是我们在Block
代码块中写的代码。其编译后的实现如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("hello block");
}
1.2 __main_block_impl_0
我们在编译后的.cpp
文件内搜索__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;
}
};
可以看到__main_block_impl_0
是一个结构体,在该结构体中第一个参数是一个__block_impl
类型的imp
,__block_impl
源码如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
在__block_impl
内有四个变量:
-
isa
: 类似于OC对象的isa,在这个例子中指向_NSConcreteStackBlock
,实际就是指向那种类型的Block
,相当于指向类 -
Flags
:这里是0 -
Reserved
: 保留字段 -
FuncPtr
:Block
代码块的函数指针,通过该指针调用block
1.3 Block的调用
在编译后的代码中我们可以看出Block
的调用是通过block->FuncPtr(block)
来进行的。
- 可以看出
block
内部声明了一个__main_block_func_0
的函数; - 在
__main_block_impl_0
中传入的第一个参数就是__main_block_func_0
; - 在其内部用
fp
表示,然后赋值给impl
的FuncPtr
属性; - 所以我们可以可以通过
block->FuncPtr(block)
来进行调用Block
通过对Clang
编译的源码进行查看,在block
内部并不会自动调用,所以我们需要调用底层生成的函数__main_block_func_0
,才能实现block
的调用
1.4 Block捕获外界变量
1.4.1 仅使用变量
上面我们分析了一个最简单的Block
,没有任何的与外界交互,如果与外界交互时,我们的Block
又会是什么样呢?
这里我们同样使用Clang
去编译一个可以捕获外界变量的Block
,实现代码如下:
#include "stdio.h"
int main(){
int a = 123;
void(^block)(void) = ^{
printf("hello block a = %d",a);
};
block();
return 0;
}
编译后的结果:
int main(){
int a = 123;
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;
}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("hello block a = %d",a);
}
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
如果想要捕获外界变量,就会在其内部创建一个同名变量来存储从外界捕获的变量。并在__main_block_func_0
中取出捕获的变量,以供函数调用的时候使用。
1.4.2 修改变量 (__block)
如果我们使用__block
修饰外界变量,并在Block
中修改了变量是什么样子呢?
我们修改代码为如下,然后通过Clang
去编译:
#include "stdio.h"
int main(){
__block int a = 123;
void(^block)(void) = ^{
a = 10;
printf("hello block a = %d",a);
};
block();
return 0;
}
编译后的结果:
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*/);}
int main(){
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 123};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
通过以上编译后的代码我们可以看到,对于__block
修饰的变量在底层被编译成了__Block_byref_a_0
类型的结构体:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
在这个结构体中我们可以通过一个叫做__forwarding
的成员变量来间接访问我们定义的变量。
在此处生成的__main_block_impl_0
结构体中,变量a
也是取的__Block_byref_a_0
类型的结构体指针。生成代码如下:
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;
}
};
对于__main_block_func_0
中的变量a
也同样是取的a
的地址进行修改其中的值。代码如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 10;
printf("hello block a = %d",(a->__forwarding->a));
}
综上所述对于使用__block
修饰的变量,在通过Clang
编译后又如下结论:
- 对于外界的变量会编译为
__Block_byref_name_0
的结构体,其中name
是外界变量的名称 - 结构体内会保存外界变量的指针和值
- 通过结构体内的
__forwarding
的成员变量来间接访问我们定义的变量。 - 对于编译后的
block
结构体__main_block_impl_0
内部也会存储一个外界变量的__Block_byref_name_0
类型的结构体指针 - 通过
block
结构体作为参数传递给生成的__main_block_func_0
对外界变量进行访问。
所以此处并不是像2.1中的那样只是创建了一个同名的变量那样简单,在这两节中分别使用了值拷贝和指针拷贝两种方法:
- 值拷贝:也就是浅拷贝,只拷贝数值,且拷贝的值不可更改,指向不同的内存空间
- 指针拷贝:也就是深拷贝,生成的对象与原对象指向同一片内存空间,在一处修改的时候另一处也会被修改
1.5 小结
通过上面的分析我们可以得出如下结论:
-
Block
在底层是一个结构体,同样也可以使用%@
打印,所以也可以理解为对象 -
Block
需要调用是因为Block
代码块在底层是一个函数,要想让其执行,所以需要调用 -
Block
捕获外界变量时,会自动生成一个同名属性 -
Block
捕获并修改外界变量时,会生成一个__Block_byref_name_0
的结构体,并通过一个叫做__forwarding
的成员变量来间接访问我们定义的变量 - 所以
__block
的原理是生成响应的结构体,保存原始变量的指针和值,传递一个指针地址给Block
2. Block底层探索
2.1 查找Block的底层实现
通过以上对于Clang
编译后Block
的探索后我们对Block
有了初步的了解,但是我们还是想知道Block
在底层的真正的实现,以及找一份开源代码进行研究,下面我们通过汇编去寻找一下Block
的底层实现和实现库的位置。
我们创建一个iOS工程编写一段Block
代码,并添加如下断点,然后开启汇编调试Debug
->Debug Workflow
->Always Show Disassembly
运行程序后我们发现一个符号symbol
为objc_retainBlock
这里的汇编代码是call
说明调用了这个符号,我们在这行汇编代码处添加断点,如下图:
过掉原本的断点,来到上面这行处,然后按住command
鼠标点击断点处的向下的小箭头来到如下图所示的汇编代码处:
通过上面的图片我们可以知道此处又继续调用了_Block_copy
,然后我们添加_Block_copy
符号断点。过掉上面的断点来到如下图所示的汇编处:
通过上面这张图片我们可以看到_Block_copy
实现于libsystem_blocks.dylib
源码中。
我们可在Apple Opensource中下载各个版本的libclosure源码。这里推荐一下LGCooci老师的libclosure-74-KCBuild,可以编译运行的libclosure
,可以运行并断点调试Block
底层的libclosure-74
源码。
2.2 Block_layout
2.2.1 Block_layout源码及分析
首先我们全局搜索_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;
// 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;
}
}
从该函数的第一行代码中我们看到了个Block_layout
,那么我们首先来看看Block_layout
这个结构体是什么,其实这就是我们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
};
-
isa
:指向block
类型的类,就是那几种block
-
flags
:标识符,是一种位域结构,按位表示block
的一些信息 -
reserved
:保留字段 -
invoke
:函数指针,指向具体的block
实现的调用地址 -
descriptor
:block
的附加信息(其实还有Block_descriptor_2
和Block_descriptor_3
)
2.2.2 flag 分析
在_Block_copy
函数中我们可以看到aBlock->flags & BLOCK_NEEDS_FREE
,说明flag
与BLOCK_NEEDS_FREE
相关,我们跳转到BLOCK_NEEDS_FREE
找到如下枚举代码:
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
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
};
通过以上枚举代码我们可以看到这些枚举值分别带表着block
的不同信息:
- 第1位
BLOCK_DEALLOCATING
:释放标记,一般常用BLOCK_NEEDS_FREE
做按位与操作,一同传入 Flags,表示该block
是否可以释放。 - 第16位
BLOCK_REFCOUNT_MASK
:存储引用计数的值,是一个可选用的参数 - 第24位
BLOCK_NEEDS_FREE
:低16位是否有效的标志,程序根据它来决定是否增加或较少引用计数位的值 - 第25位
BLOCK_HAS_COPY_DISPOSE
:是否拥有拷贝辅助函数a copy helper function
- 第26位
BLOCK_HAS_CTOR
:是否拥有block
析构函数 - 第27位
BLOCK_IS_GC
:标志是否有垃圾回收,应用于OS X
- 第28位
BLOCK_IS_GLOBAL
:标志是否是全局Block
- 第29位
BLOCK_USE_STRET
:与30位相反,判断当前Block
是否拥有一个签名,用于runtime
时动态调用。 - 第30位
BLOCK_HAS_SIGNATURE
:与29位相反,判断当前Block
是否拥有一个签名,用于runtime
时动态调用。 - 第31位:
BLOCK_HAS_EXTENDED_LAYOUT
:标志block
是否有扩展
2.2.3 descriptor 分析
descriptor
是block
的附加信息,首先在``中看到的是Block_descriptor_1
,我们跳转过去可以看到如下代码:
#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扩展布局
};
这里的Block_descriptor_1
是必选的Block_descriptor_2
和Block_descriptor_3
不是必选的:
Block_descriptor_2
需要flags
是:BLOCK_HAS_COPY_DISPOSE
才会存在,Block_descriptor_3
需要flags
是BLOCK_HAS_SIGNATURE
和BLOCK_HAS_EXTENDED_LAYOUT
才会存在。
我们在Block_layout
中只看到Block_descriptor_1
那么是怎么访问Block_descriptor_2
和Block_descriptor_3
的呢?我们可以在其构造方法中找到答案,就是经过内存平移访问的,源码如下:
/****************************************************************************
Accessors for block descriptor fields
*****************************************************************************/
#if 0
static struct Block_descriptor_1 * _Block_descriptor_1(struct Block_layout *aBlock)
{
return aBlock->descriptor;
}
#endif
// Block 的描述 : 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);
return (struct Block_descriptor_2 *)desc;
}
// Block 的描述 : 签名相关
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;
}
2.2.4 查看Block签名
在面试中经常会提到Block
签名的问题,那么我们的Block
签名到底是什么呢?在上一节中我们可以看到我们的Block
签名存储在Block_descriptor_3
中,下面我们就定义一个block
通过读取内存的方式看一看Block
的签名长什么样。
Block 代码:
void (^block)(void) = ^{
NSLog(@"test_block");
};
block();
16061993231947.jpg
以下这段话很重要!!!!!
这里是紧密的根据上一节的内容来的,因为block
在底层本质是Block_layout
,其参数中isa
占8字节,flags
占4字节,reserved
占4字节,invoke
占8字节,descriptor
占8字节,所以我们读取的第一个4段内存中的第4个就是descriptor
的地址,根据上一节中我们的分析,知道了Block_descriptor_3
的地址是由Block_descriptor_1
偏移进行读取的,根据_Block_descriptor_3
函数中的代码,我们的Block
是否需要销毁通过BLOCK_HAS_COPY_DISPOSE
进行判断,在读取的第一个四段内存中的第二段0x50000000
与上flags
中的BLOCK_HAS_COPY_DISPOSE
也就是1 << 25
结果为0
,所以只需要偏移Block_descriptor_1
这个结构体的内存大小,Block_descriptor_1
有两个属性,分别都是uintptr_t
类型,uintptr_t
实际就是long
占8字节,两个就是16字节,所以第二个四段内存中的第三个就是Block_descriptor_3
的首地址,也就是signature
签名信息的地址,是个char *
类型,占8字节。打印结果为v8@?0
,所以这就是我们当前Block
的签名。
签名信息分析:
-
v
:返回值void
-
8
:占8位,也就是block本身占用的内存空间 -
@?
:block签名 -
0
:起始位置为0
我们再来看看有参数有返回值的Block
的签名:
代码:
NSString* (^block1)(int a, int b) = ^(int a, int b){
return [NSString stringWithFormat:@"%d---%d", a, b];
};
NSString * str = block1(1,2);
NSLog(@"字符串的值是:%@", str);
内存读取结果:
16062009165973.jpg此时的签名变成了@"NSString"16@?0i8i12
签名信息分析:
-
@"NSString":
返回值为OC
类NSString
-
16
:占用16字节 -
@?
:block的签名 -
0i8i12
:起始位置为0,block,i
为分隔符,8是第一个参数的起始位置也就是int a
,12 是第一个参数的起始位置也就是int b
打印一下签名
通过[NSMethodSignature signatureWithObjCTypes:"@?"]
通过打印我们可以看到isBlock
PS:其实我们直接po 打印也可以看到block的签名:
16062106117043.jpg 16062106674098.jpg结论:
block
的签名为@?
2.3 Block 三层拷贝 捕获外界变量并修改的底层实现(__block)
在1.4.2
中我们编译后的代码中多了两个函数__main_block_copy_0
和__main_block_dispose_0
,在那一节我们并没有详细的分析,下面我们就通过这两个函数来详细的说说在底层__block
是个啥。
首先我们在__main_block_copy_0
函数中可以看到其在内部调用了_Block_object_assign
函数,那么我们就去libclosure
中搜索一下这个函数:
2.3.1 _Block_object_assign
/*******************************************************
Entry points used by the compiler - the real API!
A Block can reference four different kinds of things that require help when the Block is copied to the heap.
1) C++ stack based objects
2) References to Objective-C objects
3) Other Blocks
4) __block variables
In these cases helper functions are synthesized by the compiler for use in Block_copy and Block_release, called the copy and dispose helpers. The copy helper emits a call to the C++ const copy constructor for C++ stack based objects and for the rest calls into the runtime support function _Block_object_assign. The dispose helper has a call to the C++ destructor for case 1 and a call into _Block_object_dispose for the rest.
The flags parameter of _Block_object_assign and _Block_object_dispose is set to
* BLOCK_FIELD_IS_OBJECT (3), for the case of an Objective-C Object,
* BLOCK_FIELD_IS_BLOCK (7), for the case of another Block, and
* BLOCK_FIELD_IS_BYREF (8), for the case of a __block variable.
If the __block variable is marked weak the compiler also or's in BLOCK_FIELD_IS_WEAK (16)
So the Block copy/dispose helpers should only ever generate the four flag values of 3, 7, 8, and 24.
When a __block variable is either a C++ object, an Objective-C object, or another Block then the compiler also generates copy/dispose helper functions. Similarly to the Block copy helper, the "__block" copy helper (formerly and still a.k.a. "byref" copy helper) will do a C++ copy constructor (not a const one though!) and the dispose helper will do the destructor. And similarly the helpers will call into the same two support functions with the same values for objects and Blocks with the additional BLOCK_BYREF_CALLER (128) bit of information supplied.
So the __block copy/dispose helpers will generate flag values of 3 or 7 for objects and Blocks respectively, with BLOCK_FIELD_IS_WEAK (16) or'ed as appropriate and always 128 or'd in, for the following set of possibilities:
__block id 128+3 (0x83)
__block (^Block) 128+7 (0x87)
__weak __block id 128+3+16 (0x93)
__weak __block (^Block) 128+7+16 (0x97)
********************************************************/
//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
//
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];
********/
_Block_retain_object(object);
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
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;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_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;
}
}
根据注释我们总结如下:
- 在将块复制到堆时,块可以引用四种不同类型的需要帮助的东西。
- 1)基于c++栈的对象
- 2)引用Objective-C对象
- 3)其他模块
- __block变量
- 当
block
或Block_byrefs
持有对象时,它们的复制例程助手就会使用这个入口点 - 所以这个函数并不仅仅用于
__block
,对于很多从栈区拷贝到堆区的操作可能都会用到此函数
这个函数有三个参数:
-
void *destArg
:捕获对象的地址、 -
const void *object
:捕获对象 -
flags
: flag标志
对于这三个参数从__block
处分析,我们可以从1.4.2
中的如下代码:
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*/);}
-
destArg
:&dst->a -
object
:src->a -
flags
:8
__main_block_impl_0和源码:
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;
}
};
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
在上一遍源码,其他的就不多说了,已经很显而易见了,下面我们在看看_Block_object_assign
函数,该函数的核心就是通过flags
中的值去找出各种外界变量种类组合,种类代码如下:
// 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
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
:对象 -
BLOCK_FIELD_IS_BLOCK
:block变量 -
BLOCK_FIELD_IS_BYREF
__block 修饰的变量 -
BLOCK_FIELD_IS_WEAK
:__weak 修饰的变量 -
BLOCK_BYREF_CALLER
:处理Block_byref内部对象内存的时候会加的一个额外标记,配合上面的枚举一起使用
此处我们看看BLOCK_FIELD_IS_BYREF
也就是对应__block
时在函数内部是怎么处理的:
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
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
函数
2.3.2 _Block_byref_copy
_Block_byref_copy源码:
// Runtime entry points for maintaining the sharing knowledge of byref data blocks.
// A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
// Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
// We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment and return it.
// Otherwise we need to copy it and update the stack forwarding pointer
static struct Block_byref *_Block_byref_copy(const void *arg) {
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;
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 {
// 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
捕获外界变量的操作内存拷贝以及一些常规处理。
- 这里首先出初始化一个局部变量
src
存储传入的外部变量 - 然后判断
block
的引用计数是否为0 - 如果不为0说明不是第一次拷贝,进入另一个分支判断是否需要释放(
free
),如果需要则调用latching_incr_int
函数增加引用计数 - 如果以上都不满足直接返回
src->forwarding
- 如果是0就说明是第一次拷贝
- 首先创建一个一样大小的
Block_byref
变量copy
- 给
copy
赋一些值,这里有一处重要的操作就是通过对copy
的forwarding
和src
的forwarding
同时指向copy
来达到变量的指针统一,以达到修改变量值时,达到同时修改的目的。- 下面判断该
block
是否需要销毁,如果需要就进行一些赋值操作 - 还会判断
block
是否有扩展信息,如果有也会进行一些赋值操作 - 最后调用
src2->byref_keep
,那么这个byref_keep
是什么呢?我们进一步分析
- 下面判断该
- 首先创建一个一样大小的
在分析byref_keep
前我们先看看latching_incr_int
函数,源码如下:
latching_incr_int源码:
static int32_t latching_incr_int(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return BLOCK_REFCOUNT_MASK;
}
if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
return old_value+2;
}
}
}
latching_incr_int
函数中的判断就不多说了,这里说一下为啥要加2
,因为记录引用计数是在flag
的第二位中,第一位是记录block
是否释放的,所以加2
想当于第二位加1.
byref_keep:
由于byref_keep
是一个BlockByrefKeepFunction
函数指针类型的属性,所以byref_keep
并不是函数名,byref_keep
所在的结构体:
//__Block 修饰的结构体
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
//__Block 修饰的结构体 byref_keep 和 byref_destroy 函数 - 来处理里面持有对象的保持和销毁
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep;
BlockByrefDestroyFunction byref_destroy;
};
struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};
那么到这里就算断了吗?那肯定不是的,我们去1.4.2
中Clang
编译后的代码中去寻找__Block_byref_a_0
这个结构体中看看,这里的第五个参数为123
,因为这个是int
类型的外部变量,我们在外部赋值的时候为123
。下面我们换个字符串试试。
OC代码:
__block NSString *block_test = [NSString stringWithFormat:@"block_test"];
void (^block)(void) = ^{
block_test = @"block_test_block";
NSLog(@"LG_Block - %@",block_test);
};
block();
编译后__Block_byref_block_test_0部分
__attribute__((__blocks__(byref))) __Block_byref_block_test_0 block_test = {
(void*)0,
(__Block_byref_block_test_0 *)&block_test,
33554432,
sizeof(__Block_byref_block_test_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"),
sel_registerName("stringWithFormat:"),
(NSString *)&__NSConstantStringImpl__var_folders_0r_7cq1c39116927bt9x0bjsbtm0000gn_T_main_0ca87c_mi_0)};
这时我们看到__Block_byref_block_test_0
第五个参数为__Block_byref_id_object_copy_131
,也就是对应byref_keep
的位置因为Block_byref
有四个属性,所以Block_byref_2
的第一属性就对应着这里面的第五个参数。
我们在Clang
编译后的代码中搜索__Block_byref_id_object_copy_131
,其实现如下:
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
我们发现__Block_byref_id_object_copy_131
内部也是调用的_Block_object_assign
函数,但是参数确是偏移了40位的,我们知道这里是传入的参数是捕获的外界变量生成的结构体,对于这次编译生成的结构体源码如下:
struct __Block_byref_block_test_0 {
void *__isa;
__Block_byref_block_test_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSString *block_test;
};
由以上代码我们可以看出前面的地址和是8+8+4+4+8+8 = 40
,所以偏移40位后就是block_test
的地址,也就是取的外界变量的值。所以这就是将外界捕获的变量在通过_Block_object_assign
进行拷贝处理一次。也验证了我们一开始时说_Block_object_assign
并不仅仅是处理__block
的。
2.3.3 _Block_copy
对于block
类型的变量会调用_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;
// 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;
}
}
此处源码我们在一开始就上过,那时只是由该函数引入了对Block
底层结构的分析,现在我们分析一下该方法:
- 首先判断是需要释放的,也就是堆区
block
则调用latching_incr_int
函数直接进行引用计数的增加,该函数在上面分析过,这里就不多说了 - 然后判断是不是全局
block
,如果是就直接返回 - 最后也就是栈区
block
- 根据
block
的大小申请一块堆区空间 - 将栈区
block
移动到堆区申请的空间 - 对
invoke
进行赋值 - 对
flags
进行赋值,是否需要释放,引用计数等 - 调用
_Block_call_copy_helper
函数处理Block_descriptor_2
的copy
动作 - 将
isa
设置为_NSConcreteMallocBlock
也就是堆block
- 根据
_Block_call_copy_helper源码:
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
}
2.3.4 小结
至此我们对Block
的拷贝就分析完了,总结如下:
- 首先会调用
_Block_copy
函数将栈区block
拷贝到堆区(一层) - 对于使用
__block
修饰的外界变量底层会生成一个__Block_byref_xxx_0
的结构体 - 对于该结构体首先会调用
_Block_object_assign
函数对齐flags判断进入不同分支处理,这里就是BLOCK_FIELD_IS_BYREF
对应__block
- 在分支中会调用
_Block_byref_copy
函数,函数内部会拷贝一个一样大小的结构体,并且将变量指针指向同一区域,已达到修改值时相同的目的(二层) - 最后会通过
Block_byref_2
中的byref_keep
属性记录的函数指针内调用_Block_object_assign
函数,传入__Block_byref_xxx_0
的结构体的外界变量的值进行又一次拷贝,这个值是通过指针偏移找到的(三层) - 如果外界变量不是对象时,例如
int
则直接记录其值,不会进行最后一次拷贝操作 - 对于
_Block_copy
函数内对block
类型的拷贝:- 全局
block
不要拷贝 - 栈区
block
需要拷贝到堆区 - 堆区
block
增加引用计数即可
- 全局
2.4 Block的释放
在上一节中我们提到,编译后的代码中会多出两个函数,其中我们分析了__main_block_copy_0
,还剩下一个__main_block_dispose_0
,下面我们就来看看__main_block_dispose_0
都做了什么?
__main_block_dispose_0源码:
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->block_test, 8/*BLOCK_FIELD_IS_BYREF*/);}
由__main_block_dispose_0
函数我们可以看出其内部调用了_Block_object_dispose
函数,所以我们就来到libclosure
源码中搜索一下这个函数,源码如下:
// When Blocks or Block_byrefs hold objects their destroy helper routines call this entry point
// to help dispose of the contents
void _Block_object_dispose(const void *object, const int flags) {
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
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
_Block_byref_release(object);
break;
case BLOCK_FIELD_IS_BLOCK:
_Block_release(object);
break;
case BLOCK_FIELD_IS_OBJECT:
_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;
}
}
通过源码我们可以看到它与_Block_object_assign
的实现方式是一致的,都是通过一个switch
函数来进行匹配不同的情况:
2.4.1 释放__block 修饰的变量(BLOCK_FIELD_IS_BYREF)
当需要释放__block
修饰的变量时会调用_Block_byref_release
函数,源码实现如下:
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->forwarding;
if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
os_assert(refcount);
if (latching_decr_int_should_deallocate(&byref->flags)) {
if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
(*byref2->byref_destroy)(byref);
}
free(byref);
}
}
}
源码分析:
- 首先获取到要释放的变量,并取消引用转发的指针,因为在赋值的时候是与外界变量指向同一空间的
- 判断是否需要释放,如果不需要就执行完毕了
- 如果需要释放
- 获取引用计数
- 调用
latching_decr_int_should_deallocate
函数判断是否应该释放- 应该释放的话就判断
flags
中是否有拷贝/释放辅助函数- 如果有的话就获取一个临时变量
byref2
调用byref_destroy
属性保存的函数
- 如果有的话就获取一个临时变量
- 最后释放
byref
- 应该释放的话就判断
关于byref_destroy
保存的函数,实现原理与byref_keep
是一致的,我们来到编译后的eC++
文件中查看,byref_destroy
属性保存的函数是__Block_byref_id_object_dispose_131
,其代码如下:
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
通过__Block_byref_id_object_dispose_131
的代码我们可以看出,再其内部调用的是_Block_object_dispose
函数,同样也是偏移了40
,如果不是对象类型,比如int
是没有保存这个函数的,原理同拷贝原理,拷贝的时候分三层拷贝,释放的时候也就要三层释放。
上面提到的latching_decr_int_should_deallocate
函数返回当前block
是否应该释放,该函数的源码如下:
latching_decr_int_should_deallocate源码:
static bool latching_decr_int_should_deallocate(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return false; // latched high
}
if ((old_value & BLOCK_REFCOUNT_MASK) == 0) {
return false; // underflow, latch low
}
int32_t new_value = old_value - 2;
bool result = false;
if ((old_value & (BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)) == 2) {
new_value = old_value - 1;
result = true;
}
if (OSAtomicCompareAndSwapInt(old_value, new_value, where)) {
return result;
}
}
}
- 这里通过一个
while循环
,不断的判断block
是否应该释放 - 首先判断
flags
与上BLOCK_REFCOUNT_MASK
等于BLOCK_REFCOUNT_MASK
,则返回false - 然后判断两个相与是否等于0,等于的话也会返回false
- 创建一个新
new_value
= old_value - 2,并定义一个bool
值result
等于false - 判断旧
flags
与上(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)
等于2,则记录result
等于true - 最后调用
OSAtomicCompareAndSwapInt
函数将新旧值比对交换,如果交换成则返回result
,否则进入新的循环。
2.4.2 释放Block变量(BLOCK_FIELD_IS_BLOCK)
当需要释放block
变量时,需要调用_Block_release
函数,其源码实现如下:
// API entry point to release a copied Block
void _Block_release(const void *arg) {
struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
_Block_call_dispose_helper(aBlock);
_Block_destructInstance(aBlock);
free(aBlock);
}
}
源码分析:
- 首先创建一个局部的的
block
变量,如果为空直接return
- 如果
block
是全局的,则直接return
- 如果
block
不需要释放也直接return
- 如果都不是则调用
latching_decr_int_should_deallocate
判断是否能够释放,函数分析在上一节已经分析过了- 如果可以释放就调用
_Block_call_dispose_helper
函数,获取descriptor_2
中dispose
存储的是否函数进行调用 - 然后还会调用
_Block_destructInstance
,这里没有相关实现,应该是没开源吧 - 最后
free
局部变量
- 如果可以释放就调用
3. 总结
-
Block
是一个匿名函数,也是一个对象,在底层是一个Block_layout
-
Block
需要调用是因为Block
代码块在底层是一个函数,要想让其执行,所以需要调用 -
Block
捕获外界变量的时候会生成一个同名的中间变量,取获取到的时候的值 -
Block
使用外界变量的时候会生成一个__Block_byref_xxx_0
的结构体 -
Block
的签名是@?
-
Block
通过__block
访问外界变量的时候会有三层拷贝- 首先是
block
从栈拷贝到堆 - 将修饰的对象转话为一个结构体,将其拷贝到堆内存
- 将修饰的对象的内存地址也进行拷贝
- 首先是
-
Block
的释放相当于拷贝的反向,拷贝的东西都需要释放的
网友评论