主要内容:
- 捕获基本类型或对象
__block
修饰符_NSConcreteGlobalBlock
、_NSConcreteStackBlock
以及_NSConcreteMallocBlock
- Block 的结构
- Block 的复制
- Block 的循环引用
先放上几篇我看的文章:
- Language Specification for Blocks
- Block Implementation Specification
- A look inside blocks: Episode 1
- A look inside blocks: Episode 2
- A look inside blocks: Episode 3 (Block_copy)
捕获变量
主要是关于捕获自动变量的值,如果是全局变量,因为作用域的原因也就没有必要去捕获它。
测试一下以下两个局部变量是如何被捕获的:
int main() {
static int value1 = 10;
int value2 = 20;
void (^blockDemo1)(void) = ^{
NSLog(@"%d %d", value1, value2);
};
blockDemo1();
}
借助 clang
的 -rewrite-objc
命令可以得到 C++ 的代码(部分省略):
struct __main_block_impl_0 {
...
int *value1;
int value2;
__main_block_impl_0(..., int *_value1, int _value2, ...) : value1(_value1),
value2(_value2)
{...}
};
从构造方法中可以看到,对于普通的自动变量,block 捕获了它的值(值传递),对于静态自动变量 block 捕获了对它的引用(引用传递)。也正因为这样,不能在 block 中对 value2
这种普通的自动变量的值做出改变。
无论是基本的数据类型的变量还是后文的对象,都成为了 block 结构体的成员变量。
捕获对象
之前的例子中 block 都是捕获基本类型的变量,下面是关于捕获对象的一些例子。
虽然在 block 中不能对捕获的自动变量做赋值、运算等等操作,但是如果该变量是一个对象,则可以调用该对象的实例方法:
NSMutableArray *array = [NSMutableArray array];
void (^blockDemo2)(id) = ^(id obj){
[array addObject:obj]; // 可以
// array = [NSMutableArray array]; 不可以
};
blockDemo2(obj);
使用 clang -rewrite-objc -fobjc-arc xxxx.m
命令之后得到部分代码如下:
struct __main_block_impl_0 {
...
NSMutableArray *__strong array;
...
};
如果将 NSMutableArray *array = [NSMutableArray array]
改为 __weak
或者 __unsafe_unretained
,该 block 结构体中也会有一样的修改。
也正是因为 block 对 array
有强引用,array
可以超出作用域存在,以下代码可以正常运行。
block blockDemo9;
{
NSMutableArray *array = [NSMutableArray new];
blockDemo9 = ^(id obj){
[array addObject:obj];
NSLog(@"%lu", [array count]);
};
}
blockDemo9([NSObject new]);
blockDemo9([NSObject new]);
blockDemo9([NSObject new]);
修改一下 blockDemo9
:
block blockDemo9;
{
NSMutableArray *array = [NSMutableArray new];
NSMutableArray __weak *array2 = array;
blockDemo9 = ^(id obj){
[array2 addObject:obj];
NSLog(@"%lu", [array2 count]);
};
}
blockDemo9([NSObject new]);
blockDemo9([NSObject new]);
blockDemo9([NSObject new]);
因为 blockDemo9
以 __weak
的形式持有对 array2
的弱引用,所以当 array
的作用域结束,强引用失效,该数组被销毁后 array2
被赋值为 nil
,block 中打印的 count
都是0。
__block
修饰符
带有 __block
修饰符的自动变量可以在 block 中被修改:
__block int value = 10;
__block NSMutableArray *array = [NSMutableArray array];
void (^blockDemo3)(void) = ^{
array = [NSMutableArray array];
value++;
};
blockDemo3();
使用 clang -rewrite-objc -fobjc-arc xxxx.m
命令之后的代码如下:
struct __Block_byref_value_0 {
...
int value;
};
struct __Block_byref_array_1 {
...
NSMutableArray *__strong array;
};
struct __main_block_impl_0 {
...
__Block_byref_array_1 *array;
__Block_byref_value_0 *value;
...
};
被 __block
修饰的变量会产生一个该变量的结构体,在这里分别是 __Block_byref_value_0
和 __Block_byref_array_1
,各自有成员变量 value
和 array
。这两个结构体又作为 block 的成员变量。
Block 的类
block 有以下三种类型:
_NSConcreteStackBlock
、_NSConcreteMallocBlock
以及 _NSConcreteGlobalBlock
,分别存储在栈、堆以及静态存储区。
1. _NSConcreteGlobalBlock
:
第一种情况:全局范围内的 block 是 _NSConcreteGlobalBlock
第二种情况:如果没有捕获自动变量或者 __block
修饰的自动变量,那就是 _NSConcreteGlobalBlock
,前面那个 blockDemo1
因为捕获了自动变量 value2
,所以就不是 _NSConcreteGlobalBlock
。
static int value1 = 10; // Global
int value2 = 20; // Global
int main() {
static int value3 = 30; // Global
int value4 = 40; // 非 Global
__block int value5 = 50; // 非 Global
}
2. _NSConcreteStackBlock
:
在 ARC 的情况下,应该基本不会有该类型的 block 出现,而是会自动被拷贝到堆成为 _NSConcreteMallocBlock
的类型。
上文提到的 blockDemo1
在 MRC 下就是 _NSConcreteStackBlock
类型。
3. _NSConcreteMallocBlock
:
block invokeBlock() {
int value = 10;
block blk = ^{
NSLog(@"%d", value);
};
return blk;
}
int main() {
block blockDemo4 = invokeBlock();
blockDemo4();
}
在 MRC 模式下,变量 blk
是一个存储在栈上的 block,也就是说,invokeBlock
的作用域结束之后,该变量随时会被销毁,这时候如果在 main
方法中调用 blockDemo4
可能会出现错误。
在这种情况下,需要让 block 超出作用域而继续存在,就需要将 block 拷贝到堆。在 MRC 模式下需要调用 copy
方法,在 ARC 模式下,blk
会自动被拷贝到堆。
int main() {
int value = 10;
block blockDemo5 = ^{
NSLog(@"%d", value);
};
blockDemo5();
}
blockDemo5
默认是被 __strong
修饰的,所以会被自动拷贝到堆,即是 _NSConcreteMallocBlock
类型,如果是被 __weak
或者 __unsafe_unretained
修饰,则依然是 _NSConcreteStackBlock
类型。
Block 的结构
在整篇文章中,用 clang -rewrite-objc
得到的 C++ 代码只能作为一个参考,而不能当作 block 真正的实现,block 真正的实现可以在这里找到。
目前在网上别的博客看到的 block 结构体是这样的:
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. */
};
但是翻了一下源码,现在 block 的结构体应该是发生了改变,上述代码中的 Block_descriptor
结构体被分成了3个结构体,部分代码像下面这样:
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
};
先给一个简单的 block:
int main() {
block blk = ^{
NSLog(@"block");
};
blk();
}
使用 clang -S -x objective-c -arch armv7 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.0.sdk xxxx.m
命令将代码编译成汇编代码(个人不太懂汇编)。
首先看一下 ___block_literal_global
,这个其实就是 blk
的实现:
___block_literal_global:
.long __NSConcreteGlobalBlock
.long 1342177280 ## 0x50000000
.long 0 ## 0x0
.long ___main_block_invoke
.long ___block_descriptor_tmp
-
_NSConcreteGlobalBlock
其实就是Block_layout
中isa
的值,该值也可以是_NSConcreteMallocBlock
以及_NSConcreteStackBlock
。 -
___main_block_invoke
就是调用 block 的函数指针。
Block 的复制
基本类型的变量
首先,以 blockDemo6
为例,当这个 block 从栈复制到堆的时候会调用 objc_retainBlock
方法,随后调用 _Block_copy
方法。
int main() {
int value = 10;
block blockDemo6 = ^{
NSLog(@"%d", value);
};
blockDemo6();
}
value
仅仅是作为 block 的结构体变量从栈被拷贝到了堆。如果 block 捕获的是一个对象呢。
对象
int main() {
NSObject *obj = [NSObject new];
block blockDemo10 = ^{
NSLog(@"%@", obj);
};
blockDemo10();
}
C++ 代码如下:
struct __main_block_impl_0 {
...
NSObject *__strong obj;
...
};
static void __main_block_copy_0(struct __main_block_impl_0*dst,
struct __main_block_impl_0*src)
{
_Block_object_assign((void*)&dst->obj,
(void*)src->obj,
3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
使用 clang -rewrite-objc
命令之后多了两个方法,分别是 __main_block_copy_0
和 __main_block_dispose_0
。
不过,我用 clang -S -fobjc-arc xxxx.m
命令看了下,好像并没有调用这两个方法。obj
这个变量本身就在堆内存中,当 block 从栈拷贝到堆的时候应该也就没必要调用这两个方法了。
__block
变量
如果存在一个 __block
变量,又会是不一样的情况:
int main() {
__block int value = 10;
__block NSObject *obj = [NSObject new];
block blockDemo7 = ^{
NSLog(@"%d %@", value, obj);
};
blockDemo7();
}
value
变量会生成一个在栈上的结构体,该结构体中包含 value
这个成员变量,当 block 被拷贝到堆的时候,value
这个结构体也会被拷贝到堆。同理,obj
这个变量也是一样。
以下是比较完整的 clang -rewrite-objc
之后的代码:
// value 结构体
struct __Block_byref_value_0 {
...
__Block_byref_value_0 *__forwarding;
...
int value;
};
// obj 结构体
struct __Block_byref_obj_1 {
...
__Block_byref_obj_1 *__forwarding;
...
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong obj;
};
// block
struct __main_block_impl_0 {
...
__Block_byref_value_0 *value;
__Block_byref_obj_1 *obj;
...
};
static void __main_block_copy_0(struct __main_block_impl_0*dst,
struct __main_block_impl_0*src)
{
_Block_object_assign((void*)&dst->value,
(void*)src->value,
8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->obj,
(void*)src->obj,
8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
}
如果 block 捕获了 __block
变量或者对象,就会产生 __main_blcok_copy_0
和 __main_block_dispose_0
两个方法。当 __block
变量被拷贝到堆或者被销毁的时候就会调用这两个方法。
关于 __forwarding
变量
继续 blockDemo7
的例子,如果在 block 中需要访问变量,实际上会以 value->__forwarding->value
的形式访问。
将 blockDemo7
修改一下:
int main() {
__block int value = 10;
block blockDemo8 = ^{
value++;
};
value++;
blockDemo8();
}
blockDemo8
的 C++ 代码如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_value_0 *value = __cself->value;
(value->__forwarding->value)++;
}
int main() {
...
(value.__forwarding->value)++;
...
}
无论在 block 内部还是外部,都是通过先访问 __forwarding
,再访问成员变量 value
的方式,而 __forwarding
仅仅是一个指向自身的指针。但是为什么需要 __forwarding
呢?
因为通过 block 从栈复制到堆,被 block 持有的 __block
变量也会从栈复制到堆,也就是说这个时候在栈和堆上会同时存在 __block
变量的结构体。
如果不存在 __forwarding
这个变量,在 block 内部访问的是堆上的 __block
变量的结构体,而在 block 外部访问的则是在栈上的,而这两个结构体的值可能是不一样的。所以需要一个 __forwarding
变量,当 __block
变量从栈拷贝到堆的时候,栈上的 __forwarding
会指向堆上的结构体,保证 block 内外访问同一个变量。
Block 的循环引用
typedef void (^block)(void);
@interface Person : NSObject
@property (nonatomic, copy) block personBlock;
@end
@implementation Person
- (void)dealloc {
NSLog(@"dealloc");
}
- (void)invokeBlock {
self.personBlock = ^{
NSLog(@"%@", self);
};
self.personBlock();
}
@end
int main() {
Person *p = [Person new];
[p invokeBlock];
}
在上面的代码中,肯定不会调用 dealloc
方法,因为很明显存在循环引用。
以下是部分 C++ 代码:
struct __Person__invokeBlock_block_impl_0 {
...
Person *const __strong self;
__Person__invokeBlock_block_impl_0(..., Person *const __strong _self, ...) : self(_self)
{...}
};
static void __Person__invokeBlock_block_func_0(struct __Person__invokeBlock_block_impl_0 *__cself) {
Person *const __strong self = __cself->self;
...
}
从上面的代码可以看到,self
以 const __strong
的形式被 block 捕获,应该是编译器搞的鬼吧,因为 self
这个变量本身应该既没有被 __strong
修饰也没有被 __weak
修饰,没有受到 ARC 的管理(参考:Objective-C Automatic Reference Counting (ARC) 和 ARC对self的内存管理)。
在第二篇文章中说到 self
是被 _unsafe_unretained
修饰的,但是我英语渣,貌似没有在原文中发现 self
是被哪个关键词修饰。