Objective-C:Block

1、Block基础

可以将Object-c的代码块理解为C++语言的函数指针,通过代码块就能够像对待对象一样,指定要在方法和函数中传递的任意代码部分。

1.1 声明代码块

代码块的声明与函数指针的声明类似,都定义了参数和返回值;不同的是函数指针使用"*",而代码块使用"^"。在声明代码块后,需要给其赋值将要执行内容,给赋值的代码块可以省略返回值类型,因为编译器可以从存储代码块的变量确定返回值类型;但是必须再次在括号中提供代码块的参数说明。

1 void (^myBlock)(NSString *x); //声明代码块变量

2 myBlock = ^(NSString *x) //给代码块变量赋值

3 {

4 NSLog(@”%@”, x);

5 };

6 myBlock(“hello world”); //运行代码块,类似调用函数

7 void (^myBlock)(NSString *x) = ^(NSString *x)

8 {

9 NSLog(@”%@”, x);

10 }

在一个表达式内可以同时进行代码块变量的声明和初始化。这点也和使用普通的变量一样。你可以先声明变量,之后再初始化,也可以一次完成。

1.2 使用代码块

使用代码块涉及三个方面:调用代码块、代码块作为函数参数、代码块作为方法参数。

1) 调用代码块

执行代码块与执行函数指针一样,都是在调用时传递相应参数,从而即可执行代码块体或函数体。

1 int main(int argc, const char* argv[])

2 {

3 void (^myBlock)(NSString *x); //声明代码块变量

4 myBlock = ^(NSString *x) //给代码块变量赋值

5 {

6 NSLog(@”%@”, x);

7 };

8 myBlock(“hello world”); //运行代码块,类似调用函数

9 return 0;

10 }

2) 作为函数参数

代码块在函数参数列表中的声明,与正常情况下的声明一样,无任何差别,如下所示:

1 void fBlock( int (^theBlock)(NSString *value) )

2 {

3 int result = theBlock(@”hello”);

4 }

3) 作为方法参数

将代码块作为参数传入到对象或类方法(不是函数的参数)时,语法稍有不同。如下所示。

1 -(void) method: (int (^)(NSString *)) block

2 {

3 int result = block(@”hello”);

4 }

注意:

我们是在代码块定义之后才传入了代码块参数的名称(保存代码块的变量在方法体内使用的名称),因此,通常在代码块定义时需要提供代码块变量名的位置却仅传入了^符号。

1.3 代码块作用域

代码块内程序既可以访问代码块内的局部变量,也可以访问代码块外的变量,但作用域不一样。

1) 本地变量

本地变量就是与代码块在同一范围内声明的变量,即本地变量是在代码块外定义的变量。

1 int main(int argc, const char * argv[])

2 {

3 int a =1,b = 2;

4 int (^mult)(void); //声明代码块指针

5

6 a =10;b = 20;

7 mult=^{ //给代码块指针赋值

8 return a*b;

9 };

10

11 a = 20;

12 b = 50;

13

14 NSLog(@"%d",mult()); //调用代码块指针

15

16 return 0;

17 }

18 2016-04-25 20:00:05.663 propertyProject[890:61223] 200

因为本地变量是本地的,代码块会在定义时(赋值时)捕获创建点时的状态,即在创建代码块时会复制并保存外部变量的状态(变量值),所以输出的是200,不是2,也不是1000.

2) 全局变量

代码块外部的全局变量(静态变量)是全局的,所以其可以在任何时候被修改,即在创建代码块是不复制全局变量;并且全局变量可以代码块进行修改,从而反应到原来全局的变量中。

1 int main(int argc, const char * argv[])

2 {

3 static int a =1,b = 2;

4 int (^mult)(void); //声明代码块指针

5

6 a =10;b = 20;

7 mult=^{ //给代码块指针赋值

8 return a*b;

9 };

10

11 a = 20;

12 b = 50;

13

14 NSLog(@"%d",mult()); //调用代码块指针

15

16 return 0;

17 }

18 2016-04-25 20:00:05.663 propertyProject[890:61223] 1000

3) __block变量

在创建代码块时会复制本地变量的值,从而将本地变量作为代码块中的局部变量(常量),如果想要修改它们的值,可以将本地变量声明为全局变量(static),或者是添加语言指令__block声明可读写的。

1 int main(int argc, const char * argv[]) {

2 __block int a =1; //或为声明为:static int a =1

3 int (^mult)(void); //声明代码块指针

4 mult=^{ //给代码块指针赋值

5 a = 22;

6 return a;

7 };

8 mult(); //调用代码块指针

9 NSLog(@"%d",a);

10

11 return 0;

12 }

13 2016-04-25 20:42:01.208 propertyProject[1034:77740] 22

4) 参数变量

代码块的参数变量与函数的形式参数一样,都只是临时变量,对参数变量修改后,不能反应到原来的变量中。

5) 局部变量

局部变量是指在代码块中的定义的变量,与函数中定义的局部变量类似。

2、内存区域

对于C++对象的存储区域有全局、栈、堆区域,由于Objective-C的block也是一种对象类型,所以其存储区域也有全局、栈和堆区域。

2.1栈区域

类似C/C++语言,在局部区域定义的block也是存储在栈区域,也就是说,block只在其定义的范围有效。如下面这段代码就有危险:

1 void (^block)();

2 if()

3 {

4 block = ^{

5 NSLog(@"Block A");

6 };

7 }

8 else

9 {

10 block = ^{

11 NSLog(@"Block B");

12 };

13 }

14 block();

定义在if及else语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆盖掉。于是,这两个块只能保证在对应的if或else语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确,时而错误。若编译器未覆盖待执行的block,则程序正常执行,若覆盖,则程序崩溃。

2.2 堆区域

为解决上述例子中的问题,可将栈中的block拷贝(copy)到堆中,因为堆中的block在任意其它范围也有效。而且一旦复制到堆上,block就成了带引用计数的对象,所以后续的复制操作都不会真的执行复制,之时递增block对象的引用计数。如果不再使用这个block,那就应将其释放,在ARC环境下会自动释放,而手动管理引用计数时,则需要自己来调用release方法。当引用计数降为0后,"分配在堆上的block"会像其它对象一样,被系统回收;而"分配在栈上的block"则无须明确释放,因为栈内存本来就会自动回收。

如下的代码就安全了,但如果是手动管理引用计数,还需进行释放:

1 void (^block)();

2 if(/* DISABLES CODE */ (1))

3 {

4 block = [^{

5 NSLog(@"Block A");

6 } copy];

7 }

8 else

9 {

10 block = [^{

11 NSLog(@"Block B");

12 } copy];

13 }

14 block();

2.3 全局区域

全局block不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与,block所使用的整个内存区域在编译期已经完全确定了,因此,全局block可以声明在全局内存里,而不需要在每次用到的时候在栈中创建。另外,全局block的拷贝操作是个空操作,因为全局block决不可能为系统所回收。

如下例子,这种block就相当是一个单利:

1 void (^block)() =^{

2 NSLog(@"hello world");

3 };

4

5 int main(int argc, const char * argv[]) {

6 @autoreleasepool {

7 block();

8 }

9 }

由于运行该block所需的全部信息都能在编译期确定,所以可把它做成全局block。这完全是种优化技术:若把如此简单的block当成复杂的block来处理,那就会在复制及丢弃该block时执行一些无谓的操作。

3、使用问题

3.1 内存泄露体现

block在copy时都会对block内部用到的对象进行强引用(ARC)或者retainCount增1(非ARC)。在ARC与非ARC环境下对block使用不当都会引起循环引用问题。一般表现为,某个类将block作为自己的属性变量,然后该类在block的方法体里面又使用了该类本身,简单说就是:

1 self.someBlock = ^(Type var){

2 [self dosomething];

3 self.otherVar = XXX;

4 或者_otherVar = ...

5 };

如下所示:

1 @interface myClass : NSObject<NSCopying>

2 @property(nullable) NSString* name;

3 @property void (^block)(void);

4 -(void) myMethod;

5 @end

6 @implementation myClass

7 -(void) myMethod

8 {

9 self.block = ^()

10 {

11 self.name = @"hello"; //会被编译器捕捉到并及时提醒:Projects/propertyProject/myClass.m:28:9: Capturing 'self' strongly in this block is likely to lead to a retain cycle

12 };

13 }

14 @end

注意:

在[3]文章中介绍:在block代码中没有显式地出现"self",也会出现循环引用。但经测试如果不使用self是不会出现循环引用提醒的,即在上述例子中,若使用"_name = @"hello""表达式,是不会有警告的。

3.2 解决办法

1) ARC环境下

ARC环境下可以通过使用__weak来声明一个中间变量,并将self赋值给这个中间变量,而在block中通过这个中间变量来访问self的成员属性,通过这种方式告诉block,不要在block内部对self进行强制strong引用,如下所示的例子编译器将不会出现提醒:

1 -(void) myMethod

2 {

3 __weak typeof(self) weakSelf=self;

4 self.block = ^()

5 {

6 weakSelf.name = @"hello";

7 };

8 }

2) MRC环境下

解决方式与上述基本一致,只不过将__weak关键字换成__block即可,这样的意思是告诉block:小子,不要在内部对self进行retain了!

3.3 委托delegate

在委托问题上也容易出现循环引用问题,规避该问题的杀手锏也是简单到哭,一字诀:声明delegate时请用assign(MRC)或者weak(ARC),千万别手贱玩一下retain或者strong,毕竟这基本逃不掉循环引用了!

4、参考文献

[1] objective-C基础教程

[2] Effective Objective-C 2.0

[3] Block以及对应的使用方法

[4] 谈Objective-C block的实现

[5] iOS中block实现的探究