1.代码块
代码块对象(通常称为代码块)是对C语言中函数的扩展。除了函数中的代码,代码块还包含变量绑定。代码块有时也称为闭包(closure)。
代码块包含两种类型的绑定:自动型和托管型。自动绑定(automatic binding)使用的是栈中的内存,而托管绑定(managed binding)是通过堆创建的。
1.1 代码块定义和实现
代码块借鉴了函数指针的语法。与函数指针相似,代码块具有以下特征:
- 返回类型可以手动声明,也可以由编译器推导;
- 具有指定类型的参数列表
- 拥有名称
代码:
int (^square_block)( int number ) = ^(int number) { return (number * number); }; int result = square_block(6); NSLog(“Result = %d “,result);
说明:
等号前面的内容:int (^square_block)( int number ),是代码块的定义。
等号后面的内容:是代码块的实现内容。
一般我们可以用如下关系来表示它们:
returntype ( ^ blockname) ( list of arguments ) = ^( arguments ) { body; };
1.2 使用代码块
可以像函数一样使用代码块。例如:
int result = square_block(6);
说明:代码块在使用的时候不需要^(幂符号),只有在定义的时候才需要。
使用代码块的时候通常不需要创建一个代码块变量,而是在代码中内联代码块的内容。通常需要将代码块作为参数的方法或函数。例如:
NSArray *array = [NSArray arrayWithObjects:@“a”,@“b”,@“c”,@“d”,nil]; NSArray *sortedArray = [array sortedArrayUsingComparator:^(NSString *object1, NSString *object2){ return [object1 compare: object2]; }];
1.3 使用typedef关键字
像上面那样那么长的变量定义语句,在输入这些代码的时候很容易引起错误。我们可以用typedef关键字。
typedef double (^ MyBlockName)(double a, double b);
这行代码定义了一个名为MyBlockName的代码块变量类型,它包含两个双浮点类型的参数,并且返回一个双浮点类型的数值。
有了typedef,就可以像下面这样使用这个代码块变量:
MyBlockName *myBlock = ^(double a, double b){ return a * b; }; NSLog(@“%f, %f”, myBlock (2, 4 ) , myBlock (3, 4) );
1.4 代码块和变量
1.4.1 本地变量
本地变量就是和代码块在同一范围内声明的变量。
代码示例:
typedef double (^ MyBlock)(void); double a = 10, b = 20; MyBlock myBlock = ^(void){ return a * b; }; a = 30; b = 20; NSLog(@“%f”,myBlock());
这段代码最后输出地的是100,而不是600.因为变量是本地变量,代码块会在定义的时候复制并保存它们的状态。
1.4.2 参数变量
代码块中的参数变量和函数中的参数变量具有同样的作用。
typedef double (^ MyBlock)(double c, double d); MyBlock myBlock = ^(double a, double b){ return a * b; }; NSLog:(@“%f, %f”,myBlock(12,2), myBlock(2,4));
1.4.3 __block 变量
本地变量会被代码块作为常量获取到。如果你想要修改他们的值,必须将他们声明为可修改的,否则像下面这个实例,编译时会出现错误:
double c = 3; MyBlock myBlock = ^(double a, double b){ c = a * b; };
编译器会报这个错误:
Variable is not assignable (missing __block type specifier)
想要修复这个编译错误,需要将变量c标记为__block。
__block double c = 3; MyBlock myBlock = ^(double a, double b){ c = a * b; };
有些变量是无法声明为__block类型的。
包括:
1)长度可变的数组
2)包含可变长度数组的结构体
1.4.4 代码块内部的本地变量
这些变量与本地变量具有相同的作用:
void (^MyBlock)(void) = ^(void){ double a = 3; double b = 4; NSLog(@“%f”, a * b); }; MyBlcok();
1.5 代码块与内存管理
在代码块中使用Objective-C变量时必须小心 ,以下规则能帮助你处理内存管理。
1)如果引用了一个Objective-C对象,必须要保留它;
2)如果通过引用访问了一个实例变量,要保留一次self(即执行方法的对象);
3)如果通过数值访问了一个实例变量,变量需要保留。
解释规则(1)的示例:
NSString *string1 = ^{ return [_theString stringByAppendingString:_theString]; };
在这个示例中,_theString是声明了代码块的类中的实例变量。因为在代码块中直接访问了实例变量,所以包含它的对象(self)需要保留。
__block NSString *localObject = _theString; NSString *string2 = ^{ return [localObject stringByAppendingString:localObject]; };
在这个例子中,我们是间接访问:创建了一个指向实例变量的本地引用,并在代码块中使用。因此要保留的是localObject,而不是self。
因为代码块是对象,所以可以向它发送任何与内存管理有关的消息。在C语言级别中,必须使用Block_copy()和Block_release()函数来适当地管理内存.
MyBlock block1 = ^{ NSLog(@"Block1”); }; block1(); MyBlock block2 = ^{ NSLog(@“Block2”); }; block2(); Block_release(block2); block2 = Block_copy(block1); block2();
2.并发性
2.1 引入线程的概念
用来运行Xcode的Mac电脑的处理器至少拥有两个核心,也可能更多。现在最新的iOS设备都是多核的。这意味着你可以在同一时间进行多项任务。苹果公司提供了多种可以利用多核特性的API。能够在同一时间执行多项任务的程序称其为并发的(concurrent)程序。
利用并发性最基础的方法是使用POSIX线程来处理程序的不同部分使其能够独立执行。POSIX线程拥有支持C语言和Objective-C的API。编写并发程序需要创建多个线程,而编写线程代码是很具有挑战性的。
线程是级别较低的API,需要手动管理,处理所有的线程是需要技巧的,一旦遇到问题,可能不使用线程会更好一些。
百度百科线程:http://baike.baidu.com/view/1053.htm
2.2 GCD技术
苹果公司为了减轻在多核上变成的负担,引入了Grand Central Dispatch,我们称之为GCD。
- GCD技术减少了不少线程管理的麻烦,如果要使用GCD,你需要提交代码块或者函数作为线程来运行。
- GCD是一个系统级别(system-level)的技术,因此你可以在任意级别的代码中使用它。
- GCD决定需要多少线程来安排他们运行的进度。
- 因为GCD是运行在系统级别的,所以可以平衡应用程序所有内容的加载,这样可以提高计算机或设备的执行效率。
2.2.1 同步
我们如何在由多核组成的通路中管理交通呢?可以使用同步装置,比如在通道入口立一个标记(flag)或者一个互斥(mutex)。
说明:mutex是mutual exclusion 的缩写,它指的是确保两个线程不会在同一时间进入临界区。
Objective-C提供了一个语言级别的(language-level)关键字@synchronized。这个关键字拥有一个参数,通常这个对象是可以修改的。
@synchronized(theObject) { //Critical section }
它可以确保不同的线程会连续地访问临界区的代码。
如果你定义了一个属性,并且没有指定关键字nonatomic作为属性的特性,编译器会生成强制彼此互斥的getter和setter方法,但是这样设置代码和变量,会产生一些消耗,比直接访问慢一些。为了提高性能,可以添加nonatomic特性。
1.选择性能
NSObject提供方法以供一些代码只在后台执行。这些方法中都有performSelector:,最简单的就是performSelectorInBackground:WithObject:,它能在后台执行一个方法。它通过创建一个线程来运行方法。定义这些方法时必须遵从以下限制:
1)这些方法运行在各自的线程里,因此你必须为这些Cocoa对象创建一个自动释放池,而主自动释放池是与主线程相关的。
2)这些方法不能有返回值,并且要么没有参数,要么只有一个参数对象。换句话说,你只能使用以下代码格式中的一种:
-(void)myMethod;
-(void)myMethod:(id)myObject;
示例:
-(void)myBackgroundMethod { @autoreleasepool { NSLog(@“My Background Method”); } }
或:
-(void)myBackgroundMethod:(id)myObject { @autoreleasepool { NSLog(@“My Background Method %@”,myObject); } }
在后台执行你的方法:
[self performSelectorInBackground:@selector(myBackgroundMethod) withObject:nil];
或者:
[self performSelectorInBackground:@selector(myBackgroundMethod) withObject:argumentObjectl];
当方法执行结束之后,Objective-C运行时会特地清理并弃掉线程。需要注意:方法执行结束后并不会通知你,这是比较简单的代码。如果想要做一些更复杂的事情,需要学习调度队列。
2 调度队列
GCD可以使用调度队列(dispatch queue),只需写下你的代码,把它指派为一个队列(百度百科“队列”:http://baike.baidu.com/subview/38959/14411740.htm),系统就会执行它了。可以同步或异步执行任意代码。
有三种类型的队列:
1)连续队列:每个连续队列都会根据指派的顺序执行任务。可以按自己的想法创建任意数量的队列,他们会并行操作任务。
2)并发队列:每个并发队列都能并发执行一个或多个任务。任务会根据指派到队列的顺序开始执行。你无法创建连续队列,只能从系统提供的三个队列内选择一个来使用。
3)主队列:它是应用程序中有效的主队列,执行的是应用程序的主线程任务。
死锁(deadlock):指的是两个或多个任务在等待其他任务执行结束,就像是几辆汽车同时位于一个很拥挤的停车场里。
下面讨论三种队列及其使用:
- 连续队列
当有一连串任务需要按照一定顺序执行的时候,可以使用连续队列。任务执行顺序为先进先出(FIFO):只要任务是异步提交的,队列会确保任务根据预定顺序执行。这些队列都是不会发生死锁的。
使用:
dispatch_queue_t my_serial_queue;
my_serial_queue = dispatch_queue_create(“com.appress.MySerialQueue1”,NULL);
第一个参数是队列的名称,第二个参数负责提供队列的特性(现在用不到,所以必须为NULL)。当队列创建好以后,就可以给他指派任务。
- 并发队列
并发调度队列适合那些可以并行执行的任务。并发队列也遵从先进先出(FIFO)的规范,且任务可以在前一个任务结束前就开始执行。每一次运行同一个程序,并发任务的数量可能是不一样的,因为它会根据其它运行的任务在不同时间变化。
说明:如果需要确保每次运行的任务数量都是一样的,可以通过线程API来手动管理线程。
三种并发队列:
(1)高优先级(high):优先级选项是DISPATCH_QUEUE_PRIORITY_HIGH
(2)默认优先级(default):优先级选项是DISPATCH_QUEUE_PRIORITY_DEFAULT
(3)低优先级(low):优先级选项是DISPATCH_QUEUE_PRIORITY_LOW
如果想要引用他们,可以调用dispatch_get_global_queue方法。
代码:
dispatch_queue_t myQueue; myQueue = dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
说明:第一个参数是优先级选项,对应不同的优先级。第二个参数暂时都用0。因为它们都是全局的,所以无需为他们管理内存。不需要保留这些队列的引用,在需要的时候使用函数来访问就行了。
- 主队列
使用dispatch_get_main_queue可以访问与应用程序主线程相关的连续队列。
dispatch_queue_t main_queue = dispatch_get_current_queue(void);
因为这个队列与主线程相关,所以必须小心安排这个队列中的任务顺序,否则它们可能会阻塞主应用程序运行。通常要以同步的方式使用这个队列,提交过个任务并在它们操作完毕后执行一些动作。
- 获取当前队列
可以通过dispatch_get_current_queue()来找出当前运行的队列代码块。如果在代码块对象之外调用了这个函数,则它将返回主队列。
dispatch_queue_t myQueue = dispatch_get_current_queue();
2.2.2 队列的内存管理
调度队列是引用计数对象。可以使用dispatch_retain()和dispatch_release来修改队列的保留计数值。它们与一般的retain和release语句相似。你只能对你自己创建的队列使用这些函数,而无法用在全局调度队列上。事实上,如果你向全局队列发送这些消息,是会被忽略的。如果你编写了一个使用了垃圾回收机制的OS X应用程序,那么你必须手动管理这些队列。
1.队列的上下文(context)
“在软件工程中,上下文是一种属性的有序序列,它们为驻留在环境内的对象定义环境。在对象的激活过程中创建上下文,对象被配置为要求某些自动服务,如同步、事务、实时激活、安全性等等。又比如计算机技术中,相对于进程而言,上下文就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。”
你可以向调度对象(包括调度队列)指派全局数据上下文,可以在上下文中指派任意类型的数据,比如Objective-C对象或指针。系统只知道上下文包含了于队列相关的数据,上下文数据的内存管理只能由你来做。在为上下文分配内存的时候,可以使用dispatch_set_context()和dispatch_get_context()函数。
代码:
NSMutableDictionary *myContext = [[NSMutableDictionary alloc] initWithCapacity:5]; [myContext setObject:@“My Context” forKey:@“title”]; [myContext setObject:[NSNumber numberWithInt:0] forKey:@“value”]; dispatch_set_context(_serial_queue, (__bridge_retained void *) myContext);
在这个实例中,我们创建一个字典来存储上下文,当然也可以使用其它的指针类型。分配好内存之后就可以使用。
在最后一行代码中,我们必须保证对象是有效的,所以使用了__bridge_retained来给myContext的保留计数器的值加1。
- 清理函数
设置完上下文对象的数据之后,不需要真的知道上下文对象在何时何地会被弃用。可以让对象在它弃用的时候调用一个函数,就像类里面的dealloc函数。函数的格式应该如下所示:
void function_name(void *context);
我们将创建一个会在上下文对象弃用时调用的示例函数,通常称为终结器(finalizer)函数。
void myFinalizerFunction(void *context) { NSLog(@“myFinalizerFunction”); NSMutableDictionary *theData = (__bridge_transfer NSMutableDictionary *)context; [theData removeAllObjects]; }
__bridge_transfer 关键字:
这个关键字将对象的内存管理由全局释放池变换成了我们的函数。当我们的函数结束后,ARC将会给它的保留计数的值减1,如果保留计数的值被减到了0,对象将会被释放。如果对象没有被释放,myContext将会一直留在内存中。
如何在代码块中访问上下文内容?
NSMutableDictionary *myContext = (__bridge NSMutableDictionary *)dispatch_get_context(dispatch_get_current_queue());
这行代码中添加了__bridge关键字。是用来告诉ARC,我们并不想自己管理上下文的内存,而是想交给系统来管理。
- 添加任务
有两种方法可以向队列中添加任务:
(1)同步:队列会一直等待前面任务结束。
(2)异步:添加任务后,不必等待任务,函数会立刻返回。推荐优先使用这种方式,因为它不会阻塞其他代码的运行。
可以选择向队列添加代码块或函数。一共有四个调度函数,分别是代码块和函数各自的同步与异步方式。
注意:为了避免出现死锁,不要给运行在同一队列中的任务调用dispatch_sync或dispatch_sync_f函数。
2.调度程序
(1)通过代码块添加任务
代码块必须是dispatch_block_t这样的类型,要定义为没有参数和返回值才行。
typedef void(^dispatch_block_t)(void);
先添加异步代码块。这个函数拥有两个参数,分别是队列和代码块。
dispatch_async(_serial_queue, ^{ NSLog(@“Serial Task 1”); });
如果是同步添加,使用dispatch_sync函数。
(2)通过函数添加任务
函数的标准原型必须要像下面这样:
void fucntion_name(void argument)
示例函数:
void myDispatchFunction(void *argument) { NSLog(@“Serial Task %@”,(__bridge NSNumber *)argument); NSMutableDictionary *context = (__bridge NSMutableDictionary *)dispatch_get_context(dispatch_get_current_queue()); NSNumber *value = [context objectForKey:@“value”]; NSLog(@“value = %@“,value); }
- 向队列添加这个函数
调用函数拥有三个参数:队列、需要传递的任意上下文以及函数。如果没有信息要发送给函数,也可以只传递一个NULL值。
dispatch_async_f(_serial_queue, (__bridge void *) [NSNumber numberWithInt:3], (dispatch_function_t)myDispatchFunction);
如果想以同步的方式添加到队列中,请调用dispatch_sync_f函数。
- 暂停队列
如果出于某个原因要暂停队列,请调用dispatch_susend()函数并传递队列名称。
dispatch_suspend(_serial_queue);
- 重新启用队列
队列暂停之后,可以调用dispatch_resume()函数来重新启用。
dispatch_resume(_serial_queue);
2.3 操作队列
Objective-C提供一些被称为操作(operation)的API,使队列在Objective-C层级上使用起来更加简单。
如果想要使用操作,首先需要创建一个操作对象,然后将其指派给操作队列,并让队列执行它。一共有三种创建队列的方式。
(1)NSInvocationOperation:
如果已经有一个可以完成工作的类,并且想要在队列上执行它,可以尝试使用这种方法。
(2)NSBlockOperation:
类似于包含了需要执行代码块的dispatch_async函数。
(3)自定义操作:
如果需要更灵活的操作类型,可以创建自己的自定义类型。必须通过NSOperation子类来定义你的操作。
2.3.1 创建调用操作(invocation operation)
NSInvocationOperation会为执行任务的类调用选择器。因此你拥有 一个包含所需方法的类,使用这种方式来创建会非常方便。
@implementation MyCustomClass -(NSOperation *)operationWithData:(id)data { return [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(myWorkerMethod:) object:data]; } //This is the method that does the actual work -(void)myWorkerMethod:(id)data { NSLog(@“My Worker Method %@“,data); } @end
一旦向队列中添加了操作,任务即将执行时便会调用类里面的myWorkerMethod:方法。
- 创建代码块操作 (block operation)
如果你有一个需要执行的代码块,那么可以创建这个操作并让队列执行它。
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWihBlock:^{ //Do my work }];
一旦创建了第一个代码块,你便可以通过addExecutionBlock:方法继续添加更多的代码块。根据队列的类型(连续的还是并发的),代码块会分别以连续或者并发的方式进行。
[blockOperation addExecutionBlock:^{ //dow some more work }];
- 向队列中添加操作
一旦创建了操作,你就需要向队列中添加代码块。NSOperationQueue一般会并发执行。它具有相关性,因此如果某操作是基于其他操作的,它们会相应地执行。
如果要确保你的操作是连续执行的,可以设置最大并发操作数是1,这样任务就会按照先入先出的规范执行。在向队列添加操作之前,需要某个方法来引用到那个队列。可以创建一个新队列或使用之前已经定义过的队列(比如当前运行的队列)。
NSOperationQueue *currentQueue = [NSOperationQueue currentQueue];
或主队列:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
以下就是创建队列的代码:
NSOperationQueue *_operationQueue = [[NSOperationQueue alloc] init]; //添加操作 [_operationQueue addOperation:blockOperation];
也可以添加需要执行的代码块来替代操作对象
[_operationQueue addOperationWithBlock:^{ NSLog(“My Block”); }];
一旦队列中添加了操作,它就会被安排进度并执行。
版权声明:本文为博主原创文章,转载请注明出处。