多线程是我们程序开发中不得不面对的问题。iOS开发中主要有三种多线程实现机制:NSThread,NSOperationQueue,GCD,抽象层次分别增高,抽象层越高,使用就越方面。我在前面的5篇博客中《GCD实践——串行队列/并发队列与iOS多线程详解》等讲解了如何使用GCD,今天我们来学习一下NSOperationQueue的使用。本示例代码提交在 https://github.com/chenyufeng1991/NSOperationQueue。
在OS X10.6 和iOS 4之前,NSOperation/NSOperationQueue不同于GCD,他们使用了完全不同的机制。
从OS X10.6 和iOS4之后,NSOPeration和NSOperationQueue是建立在GCD上的。作为惯例,苹果推荐使用最高级别的抽象。
首先了解下什么是NSOperation,NSOperation就是一个操作,准确的说就是一个任务,也相当于一个函数块、block块。然后,任务便会有开始执行(start),取消(cancel),是否取消(isCancel),是否完成(isFinishing),暂停(pause)等状态函数,NSOperation本身就是一个基类,不能直接使用,必须继承它。最重要的是只有被加入到OperationQueue中才会被执行。
NSOperation中比较重要的是start和main函数,一般结合NSOperationQueue来使用。NSOperationQueue是一个队列,我们需要把一个任务加入到一个队列中。OperationQueue实质上也就是数组管理,对添加进去的operation进行创建、取消、执行等操作。添加到queue中的operation,queue会默认调用operation的start函数来执行任务,而start函数默认又是调用main函数的。
定义一个任务的步骤如下:
(1)继承自NSOperation类;
(2)重写main方法;
(3)在main方法中创建一个autoreleasepool;
(4)把你需要的代码放到autoreleasepool中执行;
在线程操作中,我们从来不能确定一个线程什么时候开始,会持续多少时间。下面我通过一段简单地代码演示如何使用NSOperation和NSOperationQueue:
(1)定义一个MyTask类,继承自NSOperation,并声明一个属性,用来标识线程ID,头文件如下:
-
#import <Foundation/Foundation.h> -
@interface MyTask : NSOperation -
@property(nonatomic,assign) int operationID; -
@end
(2)在实现文件中重写main方法,在main中的autoreleasepool中写入需要执行的方法,MyTask.m实现如下:
-
#import "MyTask.h" -
@implementation MyTask -
- (void)main{ -
@autoreleasepool { -
NSLog(@"task %i 开始 … ",self.operationID); -
[NSThread sleepForTimeInterval:3]; -
NSLog(@"task %i 结束 ",self.operationID); -
} -
} -
@end
(3)在ViewController的viewDidLoad方法中创建两个task,并加入到NSOperationQueue中,实现如下:
-
#import "ViewController.h" -
#import "MyTask.h" -
@interface ViewController () -
//声明一个NSOperationQueue队列; -
@property(nonatomic,strong) NSOperationQueue *queue; -
@end -
@implementation ViewController -
- (void)viewDidLoad { -
[super viewDidLoad]; -
self.queue = [[NSOperationQueue alloc] init]; -
MyTask *task = [[MyTask alloc] init]; -
//设置任务ID为1,区别其他的操作; -
task.operationID = 1; -
//加入到队列中; -
[self.queue addOperation:task]; -
//创建一个NSOperation对象(任务),放到NSOperationQueue中,也就是放到一个队列中; -
MyTask *task02 = [[MyTask alloc] init]; -
task02.operationID = 2; -
[self.queue addOperation:task02]; -
} -
@end
(4)运行效果如下:
结果1:
。
结果2:
以上两次截然不同的运行结果印证了我们上述的说法,线程开始的时间是不确定的,并且持续时间也是不一定的。
下面讲解下NSOperation中的主要方法和属性:
开始(start):一般我们不会重写该方法,当我们把一个任务加入到队列后,会自动调用start方法
从属性(dependency):可以让一个任务从属于其他操作。任何任务都可以从属于任意数量的操作。当一个B任务从属于A任务时,即使调用了B任务的start方法,也会等到A执行结束后才开始执行。这个类似于GCD中的线程组或者信号量实现同步机制。
优先级(priority):有时候你可能希望后台运行的任务有较低的优先级,前台任务有较高的优先级,都可以通过priority来设置。方法为:[task setQueuePriority:];
下面讲解NSOperationQueue,NSOperationQueue不需要继承,也不需要重写任何方法,只要进行简单的创建即可。还可以给队列起一个名字。
并发操作:队列和线程是两个不同的概念。一个队列可以有多个线程,每个队列中的操作会在所属的线程中运行。如果我们创建了一个并发队列,然后添加三个操作到里面。队列会把这三个操作分别放到三个不同的线程中,然后让所有操作在各自的线程中并发运行。
NSOperationQueue可以设置并发操作的最大并发数。
添加:一个操作一旦被添加到一个队列中,然后队列就会负责这个操作。所以说,什么时候调用操作的start方法,默认是由队列决定的。
待处理:任何时候你可以知道一个队列哪个操作在里面,并且总共有多少操作在里面。
暂停队列:可以通过setSuspended方法来暂停一个队列。这样会暂停所有在队列中的操作。注意:我们只能暂停一个队列,而不是暂停一个操作。
取消:可以同时取消一个队列中的所有操作,执行方法:cancelAllOperations即可。一个NSOperation对象(操作)可以通过哦isCancelled方法判断自己是否被取消。
在了解了Operation和OperationQueue的基本属性和方法后,我们再来对上述代码进行更新,在代码中尝试使用这些属性。注意内部的注释。ViewController内的代码如下:
-
#import "ViewController.h" -
#import "MyTask.h" -
@interface ViewController () -
//声明一个NSOperationQueue队列; -
@property(nonatomic,strong) NSOperationQueue *queue; -
@property (weak, nonatomic) IBOutlet UIImageView *imageView; -
@end -
@implementation ViewController -
- (void)viewDidLoad { -
[super viewDidLoad]; -
self.queue = [[NSOperationQueue alloc] init]; -
//给队列起一个名字; -
self.queue.name = @"还可以给队列起一个名字"; -
//设置队列的最大并发数; -
[self.queue setMaxConcurrentOperationCount:5]; -
MyTask *task = [[MyTask alloc] init]; -
MyTask *task02 = [[MyTask alloc] init]; -
task.operationID = 1; -
[task setQueuePriority:NSOperationQueuePriorityVeryLow]; -
task02.operationID = 2; -
//设置优先级; -
[task02 setQueuePriority:NSOperationQueuePriorityVeryHigh]; -
//设置任务间的从属关系;task02会在task执行完之后才开始执行;当然也可以移除从属关系; -
[task02 addDependency:task]; -
[self.queue addOperation:task]; -
[self.queue addOperation:task02]; -
//查看当前队列中的所有任务; -
NSArray *operationArr = [self.queue operations]; -
NSLog(@"队列中的任务:%@",operationArr); -
//任务1执行完的回调; -
[task setCompletionBlock:^{ -
NSLog(@"task结束了"); -
}]; -
//任务2执行完的回调; -
[task02 setCompletionBlock:^{ -
NSLog(@"task02结束了"); -
}]; -
//取消队列中的suoyou任务; -
[self.queue cancelAllOperations]; -
} -
@end
下面我写一个使用NSOperationQueue来请求网络图片的实例,里面并没有定义Operation操作,为了方便,而是使用block代码块的方式来作为Operation。如果你想要定义NSOperation对象,可以吧block中的语句放到operation中的main方法中执行。主要学习的是使用线程来请求网络操作,并在主线程中进行更新。代码如下:
-
/** -
* 下面定义一个下载队列; -
*/ -
NSOperationQueue *downloadQueue = [[NSOperationQueue alloc] init]; -
downloadQueue.name = @"下载图片队列"; -
[downloadQueue addOperationWithBlock:^{ -
NSURL *url = [[NSURL alloc] initWithString:@"http://gb.cri.cn/mmsource/images/2007/12/13/sw071213011.jpg"]; -
NSData *data = [[NSData alloc]initWithContentsOfURL:url]; -
//在主界面更新UI; -
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ -
self.imageView.image = [UIImage imageWithData:data]; -
}]; -
}];
运行效果如下:
。
温馨提示,在iOS9开发中,网络请求默认使用了https,如果你按原来的方式进行请求,网络请求会失败,并报下面的错:
App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.
。
修改方式需要在Info.plist文件中添加下面两个属性:
NSAppTransportSecurity Dictionary 里面再添加:
NSAllowsArbitraryLoads Boolean Yes ,如图:
github主页:https://github.com/chenyufeng1991 。欢迎大家访问!