block是C语言级别的语法和运行时特性,应用到Objective-C中可以增强函数功能。在合适场景中灵活应用block技术,对实际开发大有裨益。
block是对C语言中函数的扩展,除了函数中的代码,还包含变量的绑定。block有时也被称为闭包(closure),闭包就是一个函数,或者一个指向函数的指针,加上这个函数执行的非局部变量。通俗一点,就是闭包允许一个函数访问声明该函数运行上下文中的变量,甚至可以访问不同运行上文中的变量。
脚本语言:
function funA(callback){
alert(callback());
}
function funB(){
var str = "Hello World"; //函数funB的局部变量,函数funA的非局部变量
funA(
function(){
return str;
}
);
}
通过上面的代码我们可以看出,按常规思维来说,变量str是函数funB的局部变量,作用域只在函数funB中,函数funA是无法访问到str的。但是上述代码示例中函数funA中的callback可以访问到str,就是因为闭包性。
block实际上就是Objective-C语言对于闭包的实现。block配合dispatch_queue,可以方便地实现简单的多线程编程和异步编程。
block原型及定义
block本质上是和其他变量类似。不同的是block存储的数据是一个函数体。使用block时,你可以像调用其他标准函数一样,传入参数,并得到返回值。
脱字符(^)是block的语法标记,按照我们熟悉的参数语法规约所定义的返回值以及block的主体(也就是可以执行的代码)。下图讲解了如何把block变量赋值给一个变量的语法:
按照调用函数的方式调用block对象变量就可以了:
int result = myBlock(4); //result是28
使用typedef关键字
由于block数据类型的语法会降低整个代码的阅读性,所以常使用typedef来定义block类型。
typedef double (^Multiply2BlockRef)(double c, double d);
这行语句定义了一个名为Multiply2BlockRef的block变量,它包含两个double类型参数并返回一个double类型数值。有了typedef定义,就可以像下面这样使用这个变量:
Multiply2BlockRef multiply2 = ^(double c, double d) {
return c * d;
}
printf(“%f”, multiply2(4, 5));
Block和变量
block被声明后会捕捉创建点时的状态。block可以访问函数用到的标准类型的变量:
全局变量;
全局函数(这个是可以调用);
封闭范围内的变量;
与block声明时同级别的__block变量(这是可以修改的);
封闭范围内的非静态变量会被获取为常量;
Objective-C对象;
block内部变量;
3.1 本地变量
本地变量就是与block在同一范围内声明的变量。
typedef double (^Multiply2BlockRef)(double c, double d);
double a = 10, b = 10;
Multiply2BlockRef multiply = ^(void) { return a * b;}
a = 20;
b = 20;
NSLog(@“%f”, multiply());
这个NSLog会输出什么值?400?不是,为什么?
因为对于本地变量,block定义时copy变量的值,在block中作为常量使用,所以即使变量的值在block外改变,也不影响他在block中的值。NSLog只会输出100。
3.2 全局变量
全局变量或静态变量在内存中的地址是固定的,block在读取该变量值的时候是直接从其所在内存读出,获取到的是最新值,而不是在定义时copy的常量。
3.3 Block变量
本地变量会被block作为常量获取到,如果想要修改它们的值,必须将它们声明为可修改的,否则会编译出错。
double c = 3;
Multiply2BlockRef multiply2 = ^(double a, double b){ c = a * b; } // 编译会报错
如果想要在block代码里修改本地变量,需要将变量标记为__block。基于之前的代码,给变量c添加__block关键字,如下:
__block double c = 3;
对于用__block修饰的外部变量引用,block是复制其引用地址来实现访问的。
3.4Objective-C对象
一般来说我们总会在设置block之后,在合适的时间回调block,而不希望回调block的时候block已经被释放了,所以我们需要对block进行copy,copy倒堆中,以便后用。
当一个block被Copy的时候,如果你在block里进行了一些调用,那么将会有一个强引用指向这些调用方法的调用者,有两个规则:
如果是通过引用来访问一个实例变量,那么将强引用至self;
如果是通过值来访问一个实例变量,那么将直接强引用至这个“值”变量;
苹果官方文档里有两个例子来说明这两种情况:
dispatch_async(queue, ^{
// instanceVariable is used by reference, a strong reference is made to self
doSomethingWithObject(instanceVariable);
});
id localVariable = instanceVariable;
dispatch_async(queue, ^{
/*
localVariable is used by value, a strong reference is made to localVariable
(not to self)
*/
doSomethingWithObject(localVariable);
});
上面第一种情况相当于通过self.xxx来访问实例变量,所以强引用指向了self;第二种情况把实例变量变成了本地临时变量,强引用将直接指向这个本地的临时变量。有时第一种情况可能会造成循环引用(在后面的注意事项中会解释),要避免强引用到self的话,用__weak把self重新引用一下,在block中使用weakSelf就行了,比如:
__weak UIViewController *weakSelf = self;
Block类型
block有几种不同的类型,这里列出常见的三种类型:
_NSConcreteGlobalBlock:全局的静态block,不会访问任何外部变量,不会涉及到任何拷贝,比如一个空的block。例如:
int main()
{
^{ printf("Hello, World!\n"); } ();
return 0;
}
_NSConcreteStackBlock:保存在栈中的block,当函数返回时被销毁。例如:
int main()
{
char a = 'A';
^{ printf("%c\n",a); } ();
return 0;
}
_NSConcreteMallocBlock:保存在堆中的block,当引用计数为0时被销毁。该类型的block都是由_NSConcreteStackBlock类型的block从栈中复制到堆中形成的。例如下面代码中,在exampleB_addBlockToArray方法中的block还是_NSConcreteStackBlock类型的,在exampleB方法中就被复制到了堆中,成为_NSConcreteMallocBlock类型的block:
void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{
printf("%c\n", b);
}];
}
void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
小结
_NSConcreteGlobalBlock类型的block要么是空block,要么是不访问任何外部变量的block。它既不在栈中,也不在堆中,可以理解为它在内存的text区。
_NSConcreteStackBlock类型的block有闭包行为,也就是有访问外部变量,并且该block有且只有一次执行,因为栈中的空间是可重复使用的,所以当栈中的block执行一次之后就被清除出栈了,所以无法多次使用。
_NSConcreteMallocBlock类型的block有闭包行为,并且该block需要被多次执行。当需要多次执行时,就会把该block从栈中复制到堆中,供以多次执行。
使用block的注意事项
避免在block里用self造成循环引用
block在copy时都会对block内部用到的对象进行强引用(ARC)或者retainCount增加1(MRC)。在ARC与MRC环境下对block使用不当都会引起循环引用问题。一般表现为,某个类将block作为自己的属性变量,然后该类在block的方法体里面又使用了该类本身,简单说就是block的这种循环引用会被编译器捕捉到并及时提醒。
self.someBlock = ^(Type var) {
[self doSomething];
//或self.otherVar = XXX;
//或_otherVar = ...
};
即使在block代码中没有显式地出现"self",也会出现循环引用!只要在block里用到了self所拥有的东西!
对于这种情况,可以通过添加__weak声明(ARC)或者__block声明(MRC)去禁止block对self进行强引用或者强制增加引用计数。
开发过程中该选择block还是delegate
如果对象有超过一个以上不同的事件源,使用delegation;一般的delegate方法会有返回值;delegate的回调更多的面向过程,而block则是面向结果的。如果需要得到一条多步进程的通知,应该使用delegation。而只是希望得到请求的信息(或者获取信息时的错误提示),应该使用block。
总结
可见灵活安全地使用block,必定会使编码工作事半功倍。block经常被用作回调函数,取代传统的回调方式,可以使得编码时更顺畅,不用中途换到另一个地方写一个回调函数。采用block,可以在调用函数时直接写后续处理代码,将其作为参数传递过去,供其任务执行结束时回调。block通常适用于以下场景:任务完成时回调;处理消息监听回调处理;错误回调处理;枚举回调;视图动画、变换;排序等。
本文作者:张生辉(点融黑帮),来自点融北京技术团队,曾在索贝做过3年C++开发,目前主要做iOS开发,对前端技术及新兴技术比较感兴趣。
网友评论