上篇文章我们学习了block的分类,三种block分别为:__NSGlobalBlock__、__NSStackBlock__、__NSMallocBlock__;分析了block捕获外界变量以及block可能会引起的循环引用问题。
本篇将继续分析block的底层实现原理,栈区block是如何拷贝的堆区的,block捕获外部变量的本质,block的数据结构等内容。
1.block原理
老方法,通过clang将.m文件编译成.cpp文件,查看block的本质。`
1.未捕获外部变量
案例代码:
int main(int argc, char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"hello block");
};
block();
NSLog(@"%@", block);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
clang之后生成.cpp文件。查看编译之后的main函数:
-
block是一个结构体,该结构体定义为:__main_block_impl_0,该结构体继承自__block_implstruct __block_impl { void *isa; // isa指针 int Flags; int Reserved; void *FuncPtr; // func }; -
结构体中提供了一个构造函数
__main_block_impl_0,该构造函数对block结构体中相关属性进行设置__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; } -
构造函数
__main_block_impl_0的第一个参数为__main_block_func_0方法实现地址,在声明定义block时,将block的任务函数封装到FuncPtr属性中 -
调用
block执行时,实际调用的是block->FuncPtr,并将block结构体作为参数传入到方法实现中((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); -
总结
通过上面的分析,我们可以了解到,
block是一个结构体,也可以认为是一个函数或者参数;通过函数式编程,将block任务,保存到结构体的FuncPtr属性中;函数调用block->FuncPtr(),block作为隐藏参数,函数执行过程中,会持有block中的全部数据。
2.捕获未使用__block修饰的外部变量
修改一下上面的案例,让其捕获外部变量。见下面代码:
int main(int argc, char * argv[]) {
@autoreleasepool {
int a = 10;
void (^block)(void) = ^{
NSLog(@"hello block %d", a);
};
block();
NSLog(@"%@", block);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
我们都知道block具有外部变量捕获功能,那么这种捕获是怎么实现的呢?查看编译后的实现:
通过上面的代码发现:
- 当捕获外部变量时,
block结构体中会多一个成员变量a,并且构造函数也会多一个参数a - 如果没有
__block修饰,则通过值拷贝的方式,对其成员变量a进行赋值 - 在执行
block任务时,从结构体中获取对应的成员变量__cself->a,进行处理
捕获变量,在编译阶段就自动生成了相应的属性变量,来存储外界捕获的值,变量值拷贝。不能对常量进行变更,因为是值拷贝,在内部和外部会有相同的变量值,所以会导致代码歧义!编译不过!
3.捕获使用__block修饰的外部变量
再对上面的案例进行修改,外部变量添加__block。见下面代码:
int main(int argc, char * argv[]) {
@autoreleasepool {
__block int a = 10;
void (^block)(void) = ^{
NSLog(@"hello block %d", a);
};
block();
NSLog(@"%@", block);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
__block修饰的外部变量又有什么区别呢,见下面编译结果:
-
当外部变量使用
__block修饰时,会封装成一个结构体__Block_byref_a_0 -
block结构体中,多出一个属性a,属性a的类型为__Block_byref_a_0 -
a的地址会赋值到__Block_byref_a_0结构体的__forwarding属性中 -
总结:
使用
__block修饰符修饰的变量在编译时,block会为其创建一个结构体,结构体中保留了该变量的地址,地址拷贝。在使用该变量时,实际使用的是指针的方式访问,因此不管是在函数中或者是在block中改变该变量都不会互相影响。通过下图可以验证:
4.捕获变量总结
上面分析了block捕获外部变量的逻辑,下面我们在通过两个案例来验证一下。
-
未使用
__block修饰的外部变量在案例中,跟踪
objc的引用计数,会发现其在捕获到block内部后由1变成了3。其实在上一篇文章# block以及循环引用问题中也有类似的案例,并且对引用计数的变化进行了分析。在本例中,在外层时
objc被创建好以后,引用计数为1;因为objc没有使用__block进行修饰,所以是通过值拷贝的方式进行处理;于此同时,因为block捕获了外部变量,所以在运行时会从栈区拷贝到堆区,这样objc的引用计数会再次加1。最终objc的引用计数为3。 -
使用
__block修饰的外部变量此时的
objc使用__block就行修饰,运行结果发现objc对象的引用计数一直是1。因为__block修饰的外部变量是通过指针拷贝的方式捕获到block结构体中的,所以内部和外部操作的是同一个对象。所以引用计数一直是1。
2.block出处探索
在block定义处设置断点:
运行程序,查看汇编,见下图:
通过汇编代码,可以发现,底层调用了objc_retainBlock方法。下面设置objc_retainBlock的符号断点,继续运行程序:
发现objc_retainBlock方法来自libobjc.A.dylib,也就是我们最熟悉的objc库。在libobjc.A.dylib库中也找到了对应的方法实现,该方法会调用_Block_copy,但是_Block_copy的实现并不在libobjc.A.dylib库中。见下图:
继续跟踪汇编,设置_Block_copy符合断点,运行程序,见下图:
可以发现_Block_copy在libsystem_blocks.dylib库中。
同时,我们在clang获取的cpp文件中也可以看到block源码的出处,来自Block_private.h。见下图:
并且通过在cpp文件中block定义的结构体__block_impl和源码中Block_layout的结构体是一致的,见下图:
3.block内存变化
在上一篇文章# block以及循环引用问题中已经了解了block的三种类型。特别是捕获了外部变量的block,编译时是栈block,在运行时会copy到堆区,变成堆block。我们这里就来分析,block的这种内存变化是何时发生的,如何发生的。
引入下面的案例,该block捕获了外部变量,并设置断点进行跟踪,见下图:
通过跟踪汇编的方式,跟踪其内存变化过程。当程序运行到objc_retainBlock时,通过读取寄存器,分析block的数据状态变化。见下图:
调用objc_retainBlock方法时,此时依然是一个栈block。继续跟踪汇编,运行至_Block_copy,很显然block通过该方法完成了内存的变化,如何验证呢?汇编流程很长,在retq的地方设置断点,也就是在方法return的地方设置断点,查看其最终的处理结果,见下图:
同样通过读取寄存器x0,获取block此时为__NSMallocBlock__,并且地址发生了改变,从栈区拷贝到了堆区。
至此,可以得出结论,捕获了外部变量的block,编译时是栈block,在运行时通过_Block_copy方法会copy到堆区,变成堆block。
4.Block_layout结构分析
Block_layout的源码定义如下:
struct Block_layout {
void * __ptrauth_objc_isa_pointer isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
#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_layout中包括一些属性:
isaisa指向确定block类型flags标识码reserved保留字段invoke函数,也就是FuncPtrdescriptor相关附加信息
其中flag的值来描述块对象。先看一下flag的定义:
block的Block_descriptor_1相关属性是必然存在,其中reserved为保留字段,size为block的大小;但是Block_descriptor_3是可选的参数。而这里就通过flag字段来判断block是否存在Block_descriptor_3的相关属性。Block_descriptor的get方法可以发现,通过地址平移的方式获取对应的值,并且在获取Block_descriptor_3时会判断Block_descriptor_2是否存在,如果不存在,就不需要添加Block_descriptor_2的地址空间。见下图:
我们可以通过lldb进行相关的验证:
获取block的内存空间,平移3*8个字节后就是Block_descriptor_1的地址。我们可以查看Block_descriptor_1之后的内存空间,进而分析Block_descriptor_2和Block_descriptor_3的相关信息。
比如我们可以打印block的签名信息,见下图:
签名在Block_descriptor_3的signature属性中。
5._Block_copy流程分析
_Block_copy实现源码:
// 栈 -> 堆 研究拷贝
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// Block_layout - block结构体类型
// 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;
}
// 判断是是否为全局block
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;
// 进行block拷贝
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; // 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; // isa修改
return result;
}
}
- 首先进行
block类型强转,转成Block_layout类型 - 判断是否需要释放,如果需要则不处理
- 判断是否为
GLOBAL,如果是,也不需要进行copy操作 - 如果不是
GLOBAL,那要么是栈、要么是堆,而编译器不可能编译出一个堆区的block,所以源码中else的分支是对栈block的处理 - 调用
malloc,初始化一个内存空间,通过memmove进行相关数据的拷贝,并进行invoke和flag的设置 - 最终将
block的isa设置为_NSConcreteMallocBlock
6.block捕获外部变量
block是如何捕获外部变量的呢,block三重拷贝过程是怎样的?回到cpp文件!查看__main_block_desc_0结构体的定义。见下图:
该结构体即对应源码中的Block_descriptor信息。其中reserved和size对应Block_descriptor_1的两个属性;另外,void (*copy)和void (*dispose)对应Block_descriptor_2的两个方法;在copy方法的实现中,会调用_Block_object_assign,此过程即为外部变量的捕获和释放过程。
在源码中全局搜索_Block_object_assign,得到以下注释信息:
由编译器提供了辅助函数,用于Block_copy和Block_release,称为复制和处置辅助函数。 复制助手为基于C++堆栈的对象发出对C++ const的构造函数的调用,并为其调用运行时支持函数_Block_object_assign。dispose helper对C++析构函数,调用_Block_object_dispose。
_Block_object_assign和_Block_object_dispose的flags参数设置为:
BLOCK_FIELD_IS_OBJECT (3),捕获Objective-C Object的情况BLOCK_FIELD_IS_BLOCK (7),捕获另一个block的情况BLOCK_FIELD_IS_BYREF (8),捕获__block变量的情况
其枚举定义,见下图:
我们用的最多的就是BLOCK_FIELD_IS_OBJECT和BLOCK_FIELD_IS_BYREF。
-
_Block_object_assign源码分析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: _Block_retain_object(object); *dest = object; break; case BLOCK_FIELD_IS_BLOCK: *dest = _Block_copy(object); break; case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK: case BLOCK_FIELD_IS_BYREF: *dest = _Block_byref_copy(object); break; case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT: case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK: *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: *dest = object; break; default: break; } }- 如果持有变量是
BLOCK_FIELD_IS_OBJECT类型,即没有__block修饰,指针指向该对象,将对该对象进行持有,引用计数加1*dest = object; - 如果是
BLOCK_FIELD_IS_BLOCK类型,捕获一个block,则进行_Block_copy操作*dest = _Block_copy(object); - 如果是BLOCK_FIELD_IS_BYREF,即有
__block修饰,则会调用_Block_byref_copy*dest = _Block_byref_copy(object);
- 如果持有变量是
-
_Block_byref_copy实现源码static struct Block_byref *_Block_byref_copy(const void *arg) { struct Block_byref *src = (struct Block_byref *)arg; // __block 内存是一样 同一个家伙 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_byref *src -
如果是
BLOCK_FIELD_IS_BYREF,则会调用malloc,生成一个Block_byref *copy -
设置
forwarding,保证block内部和外部都指向同一个对象copy->forwarding = copy; src->forwarding = copy; -
Block_byref中keep函数和destroy处理,并进行byref_keep函数的调用Block_byref的设计思路和Block_layout中descriptor流程类似,通过byref->flag标识码判断对应的属性,以此来判断Block_byref_2是否存在,Block_byref定义见下图:如果用
__block修饰了外部变量,编译生成的cpp文件中,Block_byref结构体中就会默认生成两个方法,即对应Block_byref_2的keep方法和destory方法,见下图:在
cpp文件中搜索这两个函数的实现,见下图:此过程会再次调用
_Block_object_assign函数,对Block_byref结构体中的对象进行BLOCK_FIELD_IS_OBJECT流程处理。
-
至此block的三重拷贝已经摸清:
block的拷贝,即将栈区block,拷贝至堆区__block修饰的对象,对应的Block_byref结构体的拷贝- 对
Block_byref修饰的对象,调用_Block_object_assign函数进行修饰处理
至此,完成block捕获外部变量的本质分析,block的底层实现原理分析,block的数据结构分析等内容。