Objective-C:Block
1、Block基础
可以将Object-c的代码块理解为C++语言的函数指针,通过代码块就能够像对待对象一样,指定要在方法和函数中传递的任意代码部分。
1.1 声明代码块
代码块的声明与函数指针的声明类似,都定义了参数和返回值;不同的是函数指针使用"*",而代码块使用"^"。在声明代码块后,需要给其赋值将要执行内容,给赋值的代码块可以省略返回值类型,因为编译器可以从存储代码块的变量确定返回值类型;但是必须再次在括号中提供代码块的参数说明。
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) 调用代码块
执行代码块与执行函数指针一样,都是在调用时传递相应参数,从而即可执行代码块体或函数体。
2 {
3 void (^myBlock)(NSString *x); //声明代码块变量
4 myBlock = ^(NSString *x) //给代码块变量赋值
5 {
6 NSLog(@”%@”, x);
7 };
8 myBlock(“hello world”); //运行代码块,类似调用函数
9 return 0;
10 }
2) 作为函数参数
代码块在函数参数列表中的声明,与正常情况下的声明一样,无任何差别,如下所示:
2 {
3 int result = theBlock(@”hello”);
4 }
3) 作为方法参数
将代码块作为参数传入到对象或类方法(不是函数的参数)时,语法稍有不同。如下所示。
2 {
3 int result = block(@”hello”);
4 }
注意:
我们是在代码块定义之后才传入了代码块参数的名称(保存代码块的变量在方法体内使用的名称),因此,通常在代码块定义时需要提供代码块变量名的位置却仅传入了^符号。
1.3 代码块作用域
代码块内程序既可以访问代码块内的局部变量,也可以访问代码块外的变量,但作用域不一样。
1) 本地变量
本地变量就是与代码块在同一范围内声明的变量,即本地变量是在代码块外定义的变量。
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) 全局变量
代码块外部的全局变量(静态变量)是全局的,所以其可以在任何时候被修改,即在创建代码块是不复制全局变量;并且全局变量可以在代码块中进行修改,从而反应到原来全局的变量中。
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声明为可读写的。
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只在其定义的范围有效。如下面这段代码就有危险:
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"则无须明确释放,因为栈内存本来就会自动回收。
如下的代码就安全了,但如果是手动管理引用计数,还需进行释放:
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就相当是一个单利:
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的方法体里面又使用了该类本身,简单说就是:
2 [self dosomething];
3 self.otherVar = XXX;
4 或者_otherVar = ...
5 };
如下所示:
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引用,如下所示的例子编译器将不会出现提醒:
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以及对应的使用方法
[5] iOS中block实现的探究