深入理解oc中的block
苹果在Mac OS X10.6 和iOS 4之后引入了block语法。这一举动对于许多OC使用者的编码风格改变很大。就我本人而言,感觉block用起来还是很爽的,但一直以来,都是知其然,而不知所以然。这篇文章一共有两篇,其中基础篇讲解了block的基本的使用和创建,以及一些注意事项。在深入篇中,我将会对block的一些原理陈述出来,探讨block的内部。
基础篇
1.block是什么
首先,对于Block,我在苹果的文档中找到以下描述:
Block objects are a C-level language construct that you can incorporate into your C and Objective-C code. A block object is essentially an anonymous function and the data that goes with that function, something which in other languages is sometimes called a closure or lambda. Blocks are particularly useful as callbacks or in places where you need a way of easily combining both the code to be executed and the associated data.
block对象是一个C语言结构体,可以并入到C和Objective-C代码中。Block对象本质上是一个匿名函数,以及与该函数一起使用的数据,其他语言中有时称为闭包或lambda。 Block特别适用于回调,或者是在你为了让代码看起来具有更清晰的逻辑进行代码的组合时使用。
上面的解释,告诉我们: 首先,block是一个OC中的对象;并且,这个对象时一个C语言的结构体,它可以使用在C语言和OC中;同时,block本质上是一个匿名函数和其包含的数据集中。这应该就是block的定义了。之前经常在一些博客上看到,将block描述成一个结构体,或者是一个匿名函数,抑或是直接说成一个对象。这些描述都有道理,但是并不全面。(在第二部分,我会详细说明为什么)
2.为什么要使用blobk
苹果的文档中还描述:
In iOS, blocks are commonly used in the following scenarios:
As a replacement for delegates and delegate methods
As a replacement for callback functions
To implement completion handlers for one-time operations
To facilitate performing a task on all the items in a collection
Together with dispatch queues, to perform asynchronous tasks
在iOS中,在以下情况下通常使用block:
- 代替代理和委托方法
- 作为回调函数的替代
- 一次性编写多次处理的任务
- 方便对集合中的所有项执行任务
- 与GCD队列一起执行异步任务
这里告诉我们在以上情况下,我们都能够使用block。我感受最深刻的是使用block进行回调。很多情况下,我们可能只需要对某个事件进行一个单一的回调,也许仅仅就一次,如果我使用代理的话,我需要创建类、编写协议,仅仅对于一个小地方的回调成本很高,那么block登场就恰到好处了。 除此之外,block的特性可以让代理集中在某处,我们只需要在一个地方就可以完成回调之前和回调时的代码,相比,使用回调函数和代理都没有这个优势!另外,我们可以想到,OC中封装好了一些集合的方法,比如,数组的排序,仔细会发现,这里就使用block进行回到操作的。
3.block怎么使用
首先我们看看怎么创建一个block吧。
//这样创建 相当平常的block 有参数,有返回值
//申明一个block 名字叫oneFrom ,右边的float类型是参数类型,左边的float是返回值的类型。 当这两者中的任意一个为空的时候都可使用void代替
float (^oneFrom)(float); oneFrom = ^(float aFloat) {
float result = aFloat - 1.0;
NSLog(@"调用时"); return result;
};
如果我们需要创建一个全局block(进阶篇将会细说,全局block和其他的block的区别),应该像这样 :
#import <stdio.h> int GlobalInt = ;
int (^getGlobalInt)(void) = ^{ return GlobalInt; }; //这种方式将申明和实现一起写
当我们调用的时候,我们应当这样做:
//这样创建 相当平常的block 有参数,有返回值
//申明一个block 名字叫oneFrom ,右边的float类型是参数类型,左边的float是返回值的类型。 当这两者中的任意一个为空的时候都可使用void代替
float (^oneFrom)(float); oneFrom = ^(float aFloat) {
float result = aFloat - 1.0;
NSLog(@"调用时"); return result;
};
//调用
float par = 5.0;
NSLog(@"调用之前源对象:%f",par);
float result = oneFrom(par);
NSLog(@"调用的处理结果:%f",result);
NSLog(@"调用之后源对象:%f",par);
运行结果:
为了更加清楚的体现block的回调效果,我特意进行了打印。可以发现,尽管block的代码早就声明了,但是并没有立刻调用,而是在block调用的时候才被执行。 相信通过这里,对于block的回调应该有一定的理解了。
也许,这样用起来,不觉得太简单了嘛?根本就没什么卵用啊! 别急,再看看另外两种block的使用。之前说了,block时OC中的一个对象,既然是对象,我们就可以把它当作一个类的属性咯,应该是也可以很其他属性一样,被当作一个方法的参数吧。没错,是这样的,相信block之所以被大家认可也就在于这里吧。 看看这个例子。
假设 我有一个这样的类:包含一个block属性,testBlock。包含一个调用自己的block 属性的方法- blockDo.
我在另一个地方实现这样的代码
#import "ViewController.h" @interface Computer () @property (nonatomic,copy) NSString* (^testBlock)(NSString*) ; //将一个block作为 computer 的属性 - (void)blockDo; @end @implementation Computer - (void)blockDo{ NSString *testStr = @"testData_Old";
if(self.testBlock){ NSLog(@"%@",self.testBlock(testStr)); //调用并打印
}
}
@end
在另一个地方写下这些代码:
Computer* computer = [[Computer alloc]init];
computer.testBlock = ^(NSString *parStr){ NSLog(@"%@",parStr);
parStr = @"testData_New";
return parStr;
};
[computer blockDo]; //执行block
运行结果:
仔细看看,这里的调用就比之前复杂了。 因为我给它添加了一个方法,并且将block自身的调用交给了Computer,我只是实现了block而已,最后启动调用他的方法。 是的,我在另一个地方对Computer类模拟了一个方法,这个方法没有在Computer类中实现,我甚至可以在任何地方去实现它,而最后我又可以在其他的地方调用,但是它确实具备了一些功能。 这就是block的神奇的地方。 或许这样你还是觉得它不过如此。
再来看一个:
#import "ViewController.h" @interface Computer () - (void)doSomethingFeedBack:(NSString*(^)(NSString*))handle; @end @implementation Computer - (void)doSomethingFeedBack:(NSString*(^)(NSString*))handle{ NSString *handleStr = @"Old";
sleep(3.0);
NSLog(@"%@",handle(handleStr));
}
@end
其他地方调用:
[computer doSomethingFeedBack:^NSString *(NSString *parStr) {
NSLog(@"%@",parStr);
NSString *returnStr = [[NSString alloc]initWithFormat:@"add %@",parStr];
return returnStr;
}];
打印结果:
这里将block作为一个参数放在 doSomethingFeedBack 函数面。体现了block 的对象性质,相比之下,代码量很是简洁。这种实现回调的方式的逻辑更加清晰,明朗。
上面三个例子,展示block的三种不同的使用方式。它们分别是:
- 将block定义成变量
- 将block定义成属性
- 将block作为参数
4.block的使用注意点 __block和__week
block使用的过程中,并没有需要特别注意的地方,只需要注意两个关键词。__block和__weak修饰词。
我在讲解创建第一种block的时候,运行程序打印的结果看到一个现象,栈block(后续会解释什么是栈block,这里主要区分全局bolck)虽然拿到了外部变量,但是对与变量的修改确实没有效果的———前后的源数据的打印值没有发生改变。如果我们需要在block中对外部的变量进行修改,应该在这个变量之前加上__block修饰,原因在下一品文章中详解。
__weak关键词用于解决使用block导致强引用循环的问题。block在使用的过程中,当block属于某个对象,如果在block函数中,又包含了这个对象,或者包含其属性,都会因为block持有对象,对象又持有block导致对象得不到释放的情况。 这是只需要在申请一个用 weak修饰的对象替代源对象,将引用循环打断,保证正常释放,具体的内部原因在下篇中将会详细介绍。
5.总结:
通过上面演示block的用法发现,block每次的回调是通过它的匿名函数来进行的,也就是每一次最多执行一个回调,在需要进行大批量的回调的时候,就需要写很多不同的block回调,这样的方式显然是不合适的,相比之下,这是时候使用协议和代理的方式就自然多了。除此之外,block还比较适合用在线程之间的切换回调。GCD就是采用了多线程结合block来做的。在多数情况下,应当充分考虑block的可以携带环境的优点使代码的逻辑更加的清晰。
基础篇简单的介绍下block的使用。并没有深入研究其内部细节,在接下来的深入篇中,我将介绍:
- 为什么说block是一个结构体,也是一个对象,同时还是携带数据的匿名函数
- 全局block ,栈block以及堆区block的区别和他们之间的联系,探究block的内存管理
- 为什么使用__block 就可以使得block可以修改外部变量
- 引起强引用循环的原因是什么,我们解决它的方法和原理又是什么
2017-04-01 18:54:21