Block对象

block这一部分的内容真的是太难啃了,看的我脑壳疼,尽管网上很多人表示这本书的翻译很好,但看完这一章之后我是真的不敢苟同,也许是原版是日本人写的缘故,有些句子真的是九曲十八弯,譬如“所谓‘截获自动变量值‘意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例中”、“在由Block语法生成的值Block上,可以存有超过其变量作用域的被截获对象的自动变量”。碰上这样的句子我不得不用笔来手动断句,不然只读一两遍是肯定看不明白的,另外Block部分的内容我也只是一知半解,毕竟最重要的是实践,我还没有在实践中感知到Block的妙处,只能写一写粗浅的理解

形态

Block是OC中的一种特殊对象,既然说是对象,我们就来看一看它是如何声明和初始化的。

Block的声明:

int (^blockname)(int);

从左至右,int代表block的返回值、blockname是Block的名称、int是block的参数类型。

可以看到与普通的对象声明不同,block的声明更接近函数声明。

-(int) func:(int)param;

Block的定义:

int (^blockname)(int) = ^int (int num){ return num; };

赋值符号右侧的部分我们称为Block表达式。

注意其中的 ^ 字符,可以称为“脱字符”,在声明部分中,它写在block名称的前面,在Block表达式中它写在最前面,因为Block在OC中会大量应用到,因此这个字符可以使Block更容易识别和查找。

与函数不同的是,在Block的声明中并不需要写出参数的名称,参数名是在Block表达式中写出的。

以上的Block表达式是完整的写法,事实上有两个部分在某些条件下是可以省略的。

  • 返回值类型:当省略返回值类型的时候,系统会自动判断Block表达式的返回值类型,唯一需要注意的是当有多个返回值时,必须要保证返回值类型是相同的。
  • 参数列表:当Block没有传参时可以省略参数列表。

而声明则不能省略部分,即使为空也要写上void,即:

void (^blockname)(void)

如果省略传参则会得到一个警告

This block declaration is not a prototype

我们可以将Block视为一个普通的变量来使用,例如将它作为函数传参、返回值等等。

那么到现在为止,Block看上去并没有多么“特别”,当然,它还有一些特性。

捕获局部变量

C语言基本数据类型

先来看一小段代码

int val = 10;
void (^blk)(void) = ^{NSlog(@"%d", val);};
val = 20;
blk();
NSlog(@"%d", val);

输出结果:

10
20

可以看到,NSlog(@”%d”, val)这句话在Block内外执行得到了不同的结果,我们猜想,在执行到定义Block表达式的那一行,Block内的val实际上已经在某种程度上与外部的val分离了。

我们再来修改一下这段代码。

int val = 10;
void (^blk)(void) = ^{val = 30; NSlog(@"%d", val);};
val = 20;
blk();
NSlog(@"%d", val);

发现此时xcode编译报错

Variable is not assignable (missing __block type specifier)

我们发现无法对Block外界的局部变量赋值,除非在声明它的时候加上__block说明符。

int __block val = 10;
void (^blk)(void) = ^{val = 30; NSlog(@"%d", val);};
val = 20;
blk();
NSlog(@"%d", val);

输出结果

30
30

可以看到,Block内部的val并非和外部完全分离,当我们在Block中重新赋值时,还是会影响到外部的val。

C语言数组类型

那C语言中的数组可以捕获吗,答案是不行,编译时会提示

Cannot refer to declaration with an array type inside block

我们只能用指针代替数组。

OC对象

那么对象呢?

        NSString *str = @"2";
        NSMutableArray *arr = [[NSMutableArray alloc]init];
        a[0] = 1;
        void (^blk)(void) = ^{
            [arr addObject:str];
            for (NSString *str1 in arr) {
                NSLog(@"blk_%@", str1);
            }
        };
        str = @"1";
        [arr addObject:str];
        blk();
        for (NSString *str1 in arr) {
            NSLog(@"%@", str1);
        }

输出结果

blk_1
blk_2
1
2

可以看到,我们在Block中不直接改变数组的指向,只是使用addobject是可以的。

但如果要改变arr数组的指向,那么就必须在arr前加上__block说明符。

被捕获对象的作用域

        void (^blk)(void);
        if(true)
        {
            int val = 1;
            blk = ^{
                NSLog(@"%d", val);
            };
        }
        blk();

输出结果

1

可以看到在上述片段中, blk的生存周期要大于val, 在if语句结束的时候val变量就已经被回收了,但blk仍然保存了它。

总结一下上述现象:

  • block中不可以使用c语言的数组类型局部变量。

  • block中的局部变量值在定义Block表达式的那一刻就已经确定,之后在内部无法重新对它的赋值,在外部的修改也不影响内部的赋值。

  • 要在Block内重新对捕获的局部变量赋值,必须要在它们声明时加上__block说明符。

  • Block捕获的局部变量,即使在外部的生命周期已经结束,在内部仍然得以保存。

以上只是现象,要想深入了解Block,必须要知道Block的实质,由于实质部分涉及大量C语言代码,我只能在这里做简单的介绍,如果有时间日后会补充。

实质简述

Block如何捕获对象

我们知道Block是一种对象,OC的对象在运行时对应的实际上是c语言中的一系列结构体。

在Block中用到外部的局部变量时,Block的结构体成员中会追加所有用到的局部变量,并在Block的方法体内定义同名的局部变量,将结构体中捕获的局部变量成员的值赋给内部的局部变量,所以事实上方法体内使用的局部变量是Block自己在内部定义的变量。

这样一来我们就可以解决上面的三个问题

  • block中不可以使用c语言的数组类型局部变量。
  • block中的局部变量值在定义Block表达式的那一刻就已经确定,之后在内部无法重新对它的赋值,在外部的修改也不影响内部的赋值。
  • Block捕获的局部变量,即使在外部的生命周期已经结束,在内部仍然保存。

由于捕获局部变量包含了一个从结构体成员到局部变量的赋值过程,因此如果Block要捕获c语言数组时,它要这么做

int a[10] = __cself->a;

其中__cself代表Block的结构体实例,然而这种操作在c语言规范中是不允许的,因此编译时系统就会告诉你不要在Block中使用c数组。

在一般情况下,Block实际上在内部复制了它捕获的变量,因此内外的变量看似相同实际上已经没有关联,互不影响,因此Block也不允许在内部修改捕获到的变量,因为这种操作没有意义。

那为什么加上__block后的局部变量可以在Block中修改呢?

__block说明符的作用

当Block捕获到__block修饰的局部变量时,结构体成员中就不再是简单的追加这个变量,而是追加了一个结构体,这个追加的结构体中保存了被捕获的局部变量的值,以及它的地址,因此Block可以通过这个地址访问到原本的局部变量,当我们在Block内部修改被捕获的局部变量值时,Block还会自动通过地址去修改外部的局部变量值。

Block的存储域

我们在内存管理的第一篇文中提到过,局部定义的OC对象都是存储在堆上的,然而Block作为一种特殊的对象,它在默认情况下是存储在栈上的。

我们知道栈上的变量在超过作用域后系统会自动回收它们。但在

        void (^blk)(void);
        if(true)
        {
            int val = 1;
            blk = ^{
                NSLog(@"%d", val);
            };
        }
        blk();

这段代码中,我们看到Block超过了作用域但仍然存在,原因是此时Block已经被复制到了堆上,注意是复制,而不是移动,事实上,在以下四种情况中,Block都会从栈复制到堆上。

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch 的API中传递Block时

由于我们在声明blk时,系统默认加上了__strong修饰符,因此Block被复制到了堆上。

实际上因为这条性质,在ARC下使用copy修饰Block变量以及用copy复制Block到堆上都是不必要的,这些是MRC时代的遗留,我们不需要再关心。

本篇到此为止,希望这对你有帮助,如果有错误或是有需要补充的地方,望告知。


种一棵树最好的时间是在十年前,而后是现在。

Loading Disqus comments...
Table of Contents