Block分析

2016-12-21 07:20许莉鑫金海佳李玛田英爱
数码世界 2016年12期
关键词:C语言声明自动

许莉鑫 金海佳 李玛 田英爱

北京信息科技大学计算机学院

Block分析

许莉鑫 金海佳 李玛 田英爱

北京信息科技大学计算机学院

Block是苹果公司在iOS4后引入的对C语言的扩展。把Block的功能概括来说,即带有自动变量(即局部变量)的匿名函数指针。本文将对Block在这几个方面进行解读:一、Block的语法。二、Block作为函数参数使用的方法。三、Block对自动变量的截获。四、__Block说明符、存储域。五、循环引用导致内存泄漏的问题。本文旨在使阅读者深入认识Block并更好地使用Block。

Block Objective-C iOS

1 引言

在编程中闭包是非常常见的一种技术手段,在Objective-C中被称做Block。Block因其简洁的语法,特殊的存储方式,被广泛地使用在Objective-C工程中。很好地使用Block并不简单,本文将针对Block进行深入分析。

Block本质是一个函数指针,它的使用方法和C语言函数指针一样,可以传入参数,且有返回值。但和函数指针相比,Block功能更强大,所以Block也复杂很多,它与函数指针的区别主要表现在以下方面:语法上存在区别、Block是一个匿名指针、Block会截获自动变量、内存管理与释放的区别。

2 Block语法

2.1 声明Block

在C语言中,可以将一个函数的地址赋值给函数指针类型变量中,形式如:

int functionName(int count){

return count;

因为Block本质是一个匿名函数指针,所以声明一个Block和C语言中声明函数指针十分类似,形式如:

与C语言中声明函数指针相比,声明Block的区别即将“*”替换成。

Block类型变量和一般的C语言变量的使用方法完全相同,它可作为自动变量、函数参数、静态变量、静态全局变量、全局变量等使用。

2.2 对Block赋值

形式如:

“^”符号表明这是一个Block,“^”后的括号中包含着参数,花括号中可以进行一些操作,并根据需要在确定时候返回。

2.3 使用Block

可以像使用一个C语言函数一样来使用Block:

int count = blo(10);

3 Block可作为函数参数

Block比C语言中的函数强大,比如Block可以作为函数参数。可以用以下方式声明一个Objective-C的方法:

然后以以下方式调用这个方法:

这里hander变成了回调,事实上Apple的大量api接口也是这么设计的。在functionName方法中也许进行了大量的计算,开辟了很多线程,等待了很长的时间,但所有这些复杂的过程对于用户(方法的使用者)来说都是不关心的,用户关心的只有在hander中返回的“count”参数。

这个方法可以被写得更加漂亮,即添加一个Block类型变量,这其中用到C语言中的typedef。

typedef void(blo)(int count);

上例给带有“count”参数的闭包起了一个blo的别名,所以在接下来的函数声明中就可以使用blo来代替原本的参数类型,如下:

-(void)functionName:(blo)hander;

4 Block的截获自动变量特性

以以下代码为例,

此例中,blo();执行时控制台将输出“I am Eric”,即便name代表的字符串在Block后已被修改成“I am Strong”。这就是Block对自动变量的截获,简单来说,Block对自动变量的截获是指在编译Block时,Block会保存(截获)其中使用到的变量,不论Block中的变量的值在其后的语句中是否会被修改,Block中记录的该变量的值永远不会改变。

5 关于使用__block说明符

Block对自动变量的截获只能用于获取变量的值,而不能对其进行更改。当尝试去更改截获的自动变量值的时候,编译器将报错。例如下面这种情况,

此时,编译器会报出以下错误:

Variable is not assignable (missing __block type specifier)

这个错误提示我们,若想在Block中修改截获的自动变量的值,则需给变量加上“__Block”修饰符,如下所示,

使用附有__Block说明符的自动变量可以在Block中赋值,该变量称为__block变量。

再举一例,

上例在Block中对arr变量进行了初始化的赋值操作,执行会发生错误,同样需要给arr变量加__block修饰符来解决。

但不是所有在Block中变更的对象都需要加上__Block说明符。如果在Block中仅对OC对象进行操作,而不对其进行赋值,这样的变更就不会报错,故无需加上__Block说明符。例如,

此例截获的变量是一个NSMutableArray类型的变量,Block中对一个可变数组进行了操作,而没有进行赋值,所以可以正常执行。

用C语言指针来解释以上情形,即未附有__Block说明符的自动变量不能在Block中更改变量指针的指向,但可以对变量进行操作(改变地址内容)。

谈到C语言指针,还要注意在Block中对C语言数组的使用方法。例如:

执行上面这段代码,编译器会发出以下错误:

Cannot refer to declaration with an array type inside block

Implicit conversion of an Objective-C pointer to ‘const char *’ is disallowed with ARC

这是因为在现在的Block中,截获自动变量的方法没有实现对C语言的截获。对于这个问题,可以使用指针来解决,如下:

6 Block存储域

存储域一共分为三种:_NSConcreteStackBlock、_ NSConcreteGlobalBlock、_NSConcreteMallocBlock。即“栈存储域”、“全局存储域”、“堆存储域”。Block与OC变量不同,它不全存储在“栈存储域”。

a.Block存储在“全局作用域”中的情况:

如上所示,当我们声明一个全局的Block,Block就将被存储在_NSConcreteGlobalBlock中。因为这种情况下在Block中无法对自动变量进行截获,即Block的内容不依赖于运行时的状态,因此将Block放在“全局作用域”中是最合适的。事实上,只要Block的内容不依赖于运行时的状态,也就是不对自动变量进行截获,那么不管Block的声明实现位置在哪,这个Block都将被存储在“全局作用域”当中。

b.Block存储在“堆作用域”中的情况:

当将Block作为回调使用时,可以发现当Block超出了块作用域时仍可以被使用,例如在网络回调中:

我们经常会使用类似上面这种方式进行网络数据请求,在Block中对请求返回数据进行处理。由于网络请求是一个异步过程,所以在请求返回之后,已经超出了Block的作用域。之所以这种情况下Block仍可以被使用,是因为这种情况下Block将被复制在“堆存储域”中,包括Block中的自动变量也将会被拷贝到堆存储域当中。

还有一种情况是当将Block作为函数返回值返回时,Block同样会被拷贝到“堆存储域”中,再来进行返回。

大多数情况下,XCode(IDE)会自动帮编程者判断Block在什么情况下需要被拷贝到“堆存储域”中,但是在某些情况下编程者需要手动进行这个过程,使用“copy”命令把Block从“栈”拷贝到“堆”中。

7 Block循环作用域

前文提到Block在引用自动变量时将把变量从栈中拷贝到堆中,所以,比如当拷贝__strong属性变量时,十分容易引起循环引用,进而造成内存泄漏。下面这段代码就会引起循环引用:

其中ViewController持有一个变量Block,但在Block中再次截获了self,也就是Block持有self,ViewController的释放需要Block来释放self,而Block同样需要ViewController释放才会释放,这是标准的循环引用。解决这个问题可以使用__weak说明符,如下:

当使用__weak说明符后,Block不再持有self,于是打破了循环引用。

事实上并不是在Block中显示的出现self以后才会发生循环引用,下面这种情况也会发生循环引用:

上例Block中没有出现self,但在这种情况下也会发生循环引用。原因是这种情况虽然没有使用get方法来获取变量,但是直接通过内存地址获取了变量,等同于以下代码:

这解释了为什么第二种情况也会发生循环应用。解决这样的循环引用,同样可以使用__weak说明符:

需要注意的是,如果一个Block在运行时没有被调用,但是在Block中发生了循环引用,就也会发生内存泄漏。原因是Block将自动变量拷贝到“堆存储域”的动作是在编译时期完成的,所以即便没有调用Block,XCode也已经在编译时期将自动变量拷贝到了“堆存储域”当中。

解决Block的循环引用问题的方法除以上所述使用__weak说明符外,还有另外一种方式。为了解决循环引用我们必须打破双方其中一方的引用,所以上例中使用了__weak说明符,但下面的代码也可以达到相同的目的:

以上代码中声明了一个名为myObject的类,这个类中的Block发生了循环引用,如果声明了这个类的一个实列对象,那么这么对象因为循环引用而不会被释放。

如上,当声明一个Object的myObject类后,Object就已经发生了内存泄漏,但是如果在合适的位置来释放Block就可以解决这个问题:

如上,当将Block置空以后,block就失去了对Object的引用,所以这种情况不会再发生循环引用。但这样直接将Block置空的方式是十分危险的,因为改变了Block初始化的值,后面的代码运行结果就可能不同于所预料的了。所以选择置空Block的时刻非常关键。

8 总结

本文从Block的语法,Block作为函数参数使用的方法,Block对自动变量的截获,Block的使用方式:__Block说明符、存储域这些方面全面介绍了Block,并针对因循环引用导致内存泄漏的问题提出了解决办法。通过本文帮助读者深入认识Block并更好地理解Block。

[1] Kazuki Sakamoto,Tomohiko Furumoto.Objective-C 高级编程.人民邮电出版社,2013-06-01

本项目由北京信息科技大学2016年人才培养质量提高经费(5111610800)支持。

猜你喜欢
C语言声明自动
本刊声明
本刊声明
自动捕盗机
基于Visual Studio Code的C语言程序设计实践教学探索
本刊声明
51单片机C语言入门方法
本刊声明
基于C语言的计算机软件编程
让小鸭子自动转身
自动摇摆的“跷跷板”