谈谈 Block

2017-01-13 15:17:26来源:http://www.jianshu.com/p/63453780ac24作者:afishhhhh人点击


主要内容:


捕获基本类型或对象
__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,各自有成员变量 valuearray。这两个结构体又作为 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_layoutisa 的值,该值也可以是 _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;
...
}

从上面的代码可以看到,selfconst __strong 的形式被 block 捕获,应该是编译器搞的鬼吧,因为 self 这个变量本身应该既没有被 __strong 修饰也没有被 __weak 修饰,没有受到 ARC 的管理(参考:Objective-C Automatic Reference Counting (ARC) 和 ARC对self的内存管理)。


在第二篇文章中说到 self 是被 _unsafe_unretained 修饰的,但是我英语渣,貌似没有在原文中发现 self 是被哪个关键词修饰。




最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台