上篇文章我们学习了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_impl
struct __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
中包括一些属性:
isa
isa
指向确定block
类型flags
标识码reserved
保留字段invoke
函数,也就是FuncPtr
descriptor
相关附加信息
其中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
的数据结构分析等内容。