iOS多线程之NSOperation概念及使用教程
ios多线程之nsoperation概念及使用教程。
nsoperation&&nsoperationqueue的使用姿势全解
经过前面的学习,讲解了最基础的nsthread使用方法,封装更完善的gcd,gcd提供了极其便捷的方法来编写多线程程序,可以自动实现多核的真正并行计算,自动管理线程的生命周期,好处不言而喻,但可定制性就有点不足了,foundation框架提供了nsoperation和nsoperationqueue这一面向对象的多线程类,这两个类与gcd提供的功能类似,nsoperation提供任务的封装,nsoperationqueue顾名思义,提供执行队列,可以自动实现多核并行计算,自动管理线程的生命周期,如果是并发的情况,其底层也使用线程池模型来管理,基本上可以说这两个类提供的功能覆盖了gcd,并且提供了更多可定制的开发方式,开发者可以按需选择。
使用nsoperation和nsoperationqueue来编写多线程程序非常简单,只需要创建一个任务对象,创建一个执行队列或者和获取主线程一样获取一个主任务队列,然后将任务提交给队列即可实现并发,如过你想要串行只需要将队列的并发数设置为一即可。接下来将分别介绍两个类的使用。
nsoperation “任务的封装”
和gcd类似,gcd向队列提交任务,nsoperation就是对任务进行的封装,封装好的任务交给不同的nsoperationqueue即可进行串行队列的执行或并发队列的执行。这里的任务就是nsoperation类的一个方法,main方法或start方法(两个方法有区别,后文会讲),但nsoperation类的这两个方法是空方法,没有干任何事情,因此,我们需要自定义继承nsoperation类并重写相关方法,oc也提供了两个子类供我们使用nsblockoperation和nsinvocationoperation。
接下来看一下nsoperation类中比较重要的属性和方法:
/* 对于并发operation需要重写该方法 也可以不把operation加入到队列中,手动触发执行,与调用普通方法一样 */ - (void)start; /* 非并发operation需要重写该方法 */ - (void)main; //只读属性任务是否取消,如果自定义子类,需要重写该属性 @property (readonly, getter=iscancelled) bool cancelled; /* 设置cancelled属性为yes 仅仅标记cancelled属性,不退出任务,和nsthread的cancel一个机制 自定义子类时需要使用该属性判断是否在外部触发了取消任务的操作,手动退出任务 */ - (void)cancel; //只读属性,任务是否正在执行,如果自定义子类,需要重写该属性 @property (readonly, getter=isexecuting) bool executing; /* 只读属性,任务是否结束,如果自定义子类,需要重写该方法 对于加入到队列的任务来说,当finished设置为yes后,队列会将任务移除出队列 */ @property (readonly, getter=isfinished) bool finished; //是否为并发任务,该属性已经被标识即将弃用,应该使用下面的asynchronous属性 @property (readonly, getter=isconcurrent) bool concurrent; // to be deprecated; use and override 'asynchronous' below /* 只读属性,判断任务是否为并发任务,默认返回no 如果需要自定义并发任务子类,需要重写getter方法返回yes */ @property (readonly, getter=isasynchronous) bool asynchronous; /* 只读属性,任务是否准备就绪 对于加入队列的任务,当ready为yes,标识该任务即将开始执行 如果任务有依赖的任务没有执行完成ready为no */ @property (readonly, getter=isready) bool ready; /* 添加一个nsoperation为当前任务的依赖 如果一个任务有依赖,需要等待依赖的任务执行完成才能开始执行 */ - (void)adddependency:(nsoperation *)op; //删除一个依赖 - (void)removedependency:(nsoperation *)op; //任务在队列里的优先级 typedef ns_enum(nsinteger, nsoperationqueuepriority) { nsoperationqueuepriorityverylow = -8l, nsoperationqueueprioritylow = -4l, nsoperationqueueprioritynormal = 0, nsoperationqueuepriorityhigh = 4, nsoperationqueuepriorityveryhigh = 8 }; //任务在队列里的优先级 @property nsoperationqueuepriority queuepriority; /* 任务完成后的回调方法 当finished属性设置为yes时才会执行该回调 */ @property (nullable, copy) void (^completionblock)(void);
上述内容中有一些属性和方法是在自定义nsoperation子类中必须要重写的,自定义子类能够提供更高的可定制性,因此,编写自定义子类更复杂,自定义子类在后面会讲,如果我们只需要实现gcd那样的功能,提交一个并发的任务,oc为我们提供了两个子类nsblockoperation和nsinvocationoperation,这两个子类已经帮我们完成了各种属性的设置操作,我们只需要编写一个任务的block或者一个方法即可像使用gcd一样方便的编写多线程程序。
接下来举两个创建任务的栗子:
//创建一个nsblockoperation对象,传入一个block nsblockoperation *operation = [nsblockoperation blockoperationwithblock:^{ for (int i = 0; i < 100; i++) { nslog(@"task1 %@ %d", [nsthread currentthread], i); } }]; /* 创建一个nsinvocationoperation对象,指定执行的对象和方法 该方法可以接收一个参数即object */ nsinvocationoperation *invocationoperation = [[nsinvocationoperation alloc] initwithtarget:self selector:@selector(task:) object:@"hello, world!"];
可以发现,创建任务真的很简单,就像gcd中创建任务一样简洁,任务创建完成就可以创建队列了。
nsoperationqueue
nsoperationqueue就是任务的执行队列,看一下该类中有哪些比较重要的属性和方法:
//向队列中添加一个任务 - (void)addoperation:(nsoperation *)op; /* 向队列中添加一组任务 是否等待任务完成,如果yes,则阻塞当前线程直到所有任务完成 如果为false,不阻塞当前线程 */ - (void)addoperations:(nsarray *)ops waituntilfinished:(bool)wait; //向队列中添加一个任务,任务以block的形式传入,使用更方便 - (void)addoperationwithblock:(void (^)(void))block; //获取队列中的所有任务 @property (readonly, copy) nsarray<__kindof nsoperation *> *operations; //获取队列中的任务数量 @property (readonly) nsuinteger operationcount; /* 队列支持的最大任务并发数 如果为1,则只支持同时处理一个任务,即串行队列,主队列就是串行队列使用主线程执行任务 如果为大于1的数,则支持同时处理多个任务,即并发队列,底层使用线程池管理多个线程来执行任务 */ @property nsinteger maxconcurrentoperationcount; //队列是否挂起 @property (getter=issuspended) bool suspended; //队列的名称 @property (nullable, copy) nsstring *name; /* 取消队列中的所有任务 即所有任务都执行cancel方法,所有任务的cancelled属性都置为yes */ - (void)cancelalloperations; //阻塞当前线程直到所有任务完成 - (void)waituntilalloperationsarefinished; //类属性,获取当前队列 @property (class, readonly, strong, nullable) nsoperationqueue *currentqueue; //类属性,获取主队列,任务添加到主队列就会使用主线程执行,主队列的任务并发数为1,即串行队列 @property (class, readonly, strong) nsoperationqueue *mainqueue;
上述属性中比较重要的就是maxconcurrentoperationcount,该属性直接决定了队列是串行的还是并发的,接下来看一个栗子:
- (void)task:(id)obj { for (int i = 0; i < 100; i++) { nslog(@"task2 %@ %d %@", [nsthread currentthread], i, obj); } } - (void)viewwillappear:(bool)animated { nsoperationqueue *queue = [[nsoperationqueue alloc] init]; [queue setmaxconcurrentoperationcount:2]; nsblockoperation *operation = [nsblockoperation blockoperationwithblock:^{ for (int i = 0; i < 100; i++) { nslog(@"task1 %@ %d", [nsthread currentthread], i); } }]; nsinvocationoperation *invocationoperation = [[nsinvocationoperation alloc] initwithtarget:self selector:@selector(task:) object:@"hello, world!"]; [queue addoperation:operation]; [queue addoperation:invocationoperation]; }
上面这个栗子就很简单了,首先创建了一个队列,最大任务并发数设置为2,接下来创建了两个任务并添加进了队列,摘取几个输出如下:
task2 {number = 3, name = (null)} 0 hello, world! task1 {number = 4, name = (null)} 0
从输出中可以发现,两个任务使用了两个不同的线程来执行,如果将最大任务并发数量设置为1这里就会使用同一个线程串行执行,任务2必须得等任务1执行完成才能开始执行,就不再做实验了。使用foundation提供的nsblockoperation和nsinvocationoperation很方便,这两个子类已经帮我们完成了各个重要属性的设置操作,当block或传入的方法任务在执行时会设置executing属性值为yes,执行完成后将executing设置为no并将finished设置为yes,但是,如果在block中使用另一个线程或是gcd异步执行任务,block或方法会立即返回,此时就会将finished设置为yes,但是其实任务并没有完成,所以这种情况下不能使用该属性,当需要更高定制性时需要使用自定义nsoperation子类。
这个栗子很简单,效果就和我们使用gcd编写的多线程程序一样,接下来再举个添加依赖的栗子:
- (void)task:(id)obj { for (int i = 0; i < 100; i++) { nslog(@"task5 %@ %d %@", [nsthread currentthread], i, obj); } } - (void)viewwillappear:(bool)animated { nsoperationqueue *queue = [[nsoperationqueue alloc] init]; [queue setmaxconcurrentoperationcount:4]; nsblockoperation *operation1 = [nsblockoperation blockoperationwithblock:^{ for (int i = 0; i < 100; i++) { nslog(@"task1 %@ %d", [nsthread currentthread], i); } }]; nsblockoperation *operation2 = [nsblockoperation blockoperationwithblock:^{ for (int i = 0; i < 100; i++) { nslog(@"task2 %@ %d", [nsthread currentthread], i); } }]; nsblockoperation *operation3 = [nsblockoperation blockoperationwithblock:^{ for (int i = 0; i < 100; i++) { nslog(@"task3 %@ %d", [nsthread currentthread], i); } }]; nsblockoperation *operation4 = [nsblockoperation blockoperationwithblock:^{ for (int i = 0; i < 100; i++) { nslog(@"task4 %@ %d", [nsthread currentthread], i); } }]; nsinvocationoperation *invocationoperation = [[nsinvocationoperation alloc] initwithtarget:self selector:@selector(task:) object:@"hello, world!"]; [operation2 adddependency:operation1]; [operation3 adddependency:operation1]; [operation4 adddependency:operation3]; [queue addoperation:operation1]; [queue addoperation:operation2]; [queue addoperation:operation3]; [queue addoperation:operation4]; [queue addoperation:invocationoperation]; }
上述栗子添加了五个任务,任务依赖关系如下图所示:
如图所示,任务2依赖任务1,任务3依赖任务1,任务4依赖任务3,而任务5是独立的,所以任务2需要等待任务1完成后才可以开始执行,任务3也是同样,而任务4需要等待任务3完成后才可以开始执行,所以任务34是串行执行的,任务5是独立的没有任何依赖,所以任务5与其他任务并行执行,输出结果就不给出了,我们还可以根据业务的不同设置不同的更复杂的依赖。
自定义nsoperation子类
经过前文的讲解,关于nsoperation和nsoperationqueue的基础使用已经有了一个初步的掌握,如果我们去afnetworking或sdwebimage的时可以发现,这些库中大量用了nsoperation和nsoperationqueue,当然也用了gcd,比如sdwebimage下载图片的任务是自定义的nsoperation子类sdwebimagedownloaderoperation,之所以选择使用自定义子类,正是因为自定义子类可以提供更多定制化的方法,而不仅仅局限于一个block或一个方法,接下来将讲解具体的自定义实现方法。
在官方文档中指出经自定义nsoperation子类有两种形式,并发和非并发,非并发形式只需要继承nsoperation类后实现main方法即可,而并发形式就比较复杂了,接下来会分别介绍两种形式。
非并发的nsoperation自定义子类
官方文档中有说明,非并发的自定义子类只需要实现main方法即可,栗子如下:
@interface testoperation: nsoperation @property (nonatomic, copy) id obj; - (instancetype)initwithobject:(id)obj; @end @implementation testoperation - (instancetype)initwithobject:(id)obj { if (self = [super init]) { self.obj = obj; } return self; } - (void)main { for (int i = 0; i < 100; i++) { nslog(@"task %@ %d %@", self.obj, i, [nsthread currentthread]); } nslog(@"task complete!"); } @end - (void)viewwillappear:(bool)animated { nsoperationqueue *queue = [[nsoperationqueue alloc] init]; [queue setmaxconcurrentoperationcount:4]; testoperation *operation = [[testoperation alloc] initwithobject:@"hello, world!"]; [operation main]; //[operation start]; //[queue addoperation:operation]; }
上述栗子也很简单,就是自定义子类继承了nsoperation并且实现了main方法,在官方文档中指出,非并发任务,直接调用main方法即可,调用之后就和调用普通对象的方法一样,使用当前线程来执行main方法,在本栗中即主线程,这个栗子没有什么特别奇特的地方,但其实也可以将其加入到队列中,但这样存在一个问题,由于我们没有实现finished属性,所以获取finished属性时只会返回no,任务加入到队列后不会被队列删除,一直会保存,而且任务执行完成后的回调块也不会执行,所以最好不要只实现一个main方法就交给队列去执行,即使我们没有实现start方法,这里调用start方法以后依旧会执行main方法。这个非并发版本不建议写,好像也没有什么场景需要这样写,反而更加复杂,如果不小心加入到队列中还会产生未知的错误。
并发的nsoperation自定义子类
关于并发的nsoperation自定义子类就比较复杂了,但可以提供更高的可定制性,这也是为什么sdwebimage使用自定义子类来实现下载任务。
按照官方文档的要求,实现并发的自定义子类需要重写以下几个方法或属性:
start方法: 任务加入到队列后,队列会管理任务并在线程被调度后适时调用start方法,start方法就是我们编写的任务,需要注意的是,不论怎样都不允许调用父类的start方法
isexecuting: 任务是否正在执行,需要手动调用kvo方法来进行通知,这样,其他类如果监听了任务的该属性就可以获取到通知
isfinished: 任务是否结束,需要手动调用kvo方法来进行通知,队列也需要监听该属性的值,用于判断任务是否结束,由于我们编写的任务很可能是异步的,所以start方法返回也不一定代表任务就结束了,任务结束需要开发者手动修改该属性的值,队列就可以正常的移除任务
isasynchronous: 是否并发执行,之前需要使用isconcurrent,但isconcurrent被废弃了,该属性标识是否并发
直接看栗子吧:
@interface myoperation: nsoperation @property (nonatomic, assign, getter=isexecuting) bool executing; @property (nonatomic, assign, getter=isfinished) bool finished; @end @implementation myoperation @synthesize executing = _executing; @synthesize finished = _finished; - (void)start { //在任务开始前设置executing为yes,在此之前可能会进行一些初始化操作 self.executing = yes; for (int i = 0; i < 500; i++) { /* 需要在适当的位置判断外部是否调用了cancel方法 如果被cancel了需要正确的结束任务 */ if (self.iscancelled) { //任务被取消正确结束前手动设置状态 self.executing = no; self.finished = yes; return; } //输出任务的各个状态以及队列的任务数 nslog(@"task %d %@ cancel:%d executing:%d finished:%d queueoperationcount:%ld", i, [nsthread currentthread], self.cancelled, self.executing, self.finished, [[nsoperationqueue currentqueue] operationcount]); [nsthread sleepfortimeinterval:0.1]; } nslog(@"task complete."); //任务执行完成后手动设置状态 self.executing = no; self.finished = yes; } - (void)setexecuting:(bool)executing { //调用kvo通知 [self willchangevalueforkey:@"isexecuting"]; _executing = executing; //调用kvo通知 [self didchangevalueforkey:@"isexecuting"]; } - (bool)isexecuting { return _executing; } - (void)setfinished:(bool)finished { //调用kvo通知 [self willchangevalueforkey:@"isfinished"]; _finished = finished; //调用kvo通知 [self didchangevalueforkey:@"isfinished"]; } - (bool)isfinished { return _finished; } - (bool)isasynchronous { return yes; } @end - (void)cancelbuttonclicked { [self.myoperation cancel]; } - (void)btnclicked { nslog(@"myoperation cancel:%d executing:%d finished:%d queueoperationcount:%ld", self.myoperation.iscancelled, self.myoperation.isexecuting, self.myoperation.isfinished, self.queue.operationcount); } - (void)viewwillappear:(bool)animated { self.queue = [[nsoperationqueue alloc] init]; [self.queue setmaxconcurrentoperationcount:1]; self.myoperation = [[myoperation alloc] init]; [self.queue addoperation:self.myoperation]; }
上面的栗子也比较简单,各个状态需要根据业务逻辑来设置,需要注意的是,一定要正确的设置各个状态,并且在设置状态时需要手动触发kvo进行通知,因为可能有其他对象在监听任务的某一个状态,比如finished属性,队列就会监听任务的属性,start方法内部很可能会有异步方法的执行,所以start方法返回并不代表任务结束,队列不能根据start方法是否返回来判断任务是否结束,所以需要开发者手动修改相关属性并触发kvo通知。
上述栗子的输出如下:
//任务的输出内容 task 95 {number = 3, name = (null)} cancel:0 executing:1 finished:0 queueoperationcount:1 //任务正在执行的时候,点击按钮的输出 myoperation cancel:0 executing:1 finished:0 queueoperationcount:1 //当任务执行完成后,点击按钮的输出 myoperation cancel:0 executing:0 finished:1 queueoperationcount:0
从输出中可以看到任务和执行队列的相关属性的变化。
接下来举一个下载文件的栗子,使用自定义的nsoperation子类,提供了取消下载的功能,具体代码如下:
//filedownloadoperation.h文件代码 #ifndef filedownloadoperation_h #define filedownloadoperation_h #import @class filedownloadoperation; //定义一个协议,用于反馈下载状态 @protocol filedownloaddelegate @optional - (void)filedownloadoperation:(filedownloadoperation *)downloadoperation downloadprogress:(double)progress; - (void)filedownloadoperation:(filedownloadoperation *)downloadoperation didfinishwithdata:(nsdata *)data; - (void)filedownloadoperation:(filedownloadoperation *)downloadoperation didfailwitherror:(nserror *)error; @end @interface filedownloadoperation: nsoperation //定义代理对象 @property (nonatomic, weak) id delegate; //初始化构造函数,文件url - (instancetype)initwithurl:(nsurl*)url; @end #endif /* filedownloadoperation_h */
filedownloadoperation.m文件源码如下:
#import "filedownloadoperation.h" @interface filedownloadoperation() //定义executing属性 @property (nonatomic, assign, getter=isexecuting) bool executing; //定义finished属性 @property (nonatomic, assign, getter=isfinished) bool finished; //要下载的文件的url @property (nonatomic, strong) nsurl *fileurl; //使用nsurlconnection进行网络数据的获取 @property (nonatomic, strong) nsurlconnection *connection; //定义一个可变的nsmutabledata对象,用于添加获取的数据 @property (nonatomic, strong) nsmutabledata *filemutabledata; //记录要下载文件的总长度 @property (nonatomic, assign) nsuinteger filetotallength; //记录已经下载了的文件的长度 @property (nonatomic, assign) nsuinteger downloadedlength; @end @implementation filedownloadoperation @synthesize delegate = _delegate; @synthesize executing = _executing; @synthesize finished = _finished; @synthesize fileurl = _fileurl; @synthesize connection = _connection; @synthesize filemutabledata = _filemutabledata; @synthesize filetotallength = _filetotallength; @synthesize downloadedlength = _downloadedlength; //executing属性的setter - (void)setexecuting:(bool)executing { //设置executing属性需要手动触发kvo方法进行通知 [self willchangevalueforkey:@"executing"]; _executing = executing; [self didchangevalueforkey:@"executing"]; } //executing属性的getter - (bool)isexecuting { return _executing; } //finished属性的setter - (void)setfinished:(bool)finished { //同上,需要手动触发kvo方法进行通知 [self willchangevalueforkey:@"finished"]; _finished = finished; [self didchangevalueforkey:@"finished"]; } //finished属性的getter - (bool)isfinished { return _finished; } //返回yes标识为并发operation - (bool)isasynchronous { return yes; } //内部函数,用于结束任务 - (void)finishtask { //中断网络连接 [self.connection cancel]; //设置finished属性为yes,将任务从队列中移除 //会调用setter方法,并触发kvo方法进行通知 self.finished = yes; //设置executing属性为no self.executing = no; } //初始化构造函数 - (instancetype)initwithurl:(nsurl *)url { if (self = [super init]) { self.fileurl = url; self.filemutabledata = [[nsmutabledata alloc] init]; self.filetotallength = 0; self.downloadedlength = 0; } return self; } //重写start方法 - (void)start { //任务开始执行前检查是否被取消,取消就结束任务 if (self.iscancelled) { [self finishtask]; return; } //构造nsurlconnection对象,并设置不立即开始,手动开始 self.connection = [[nsurlconnection alloc] initwithrequest:[[nsurlrequest alloc] initwithurl:self.fileurl cachepolicy:nsurlrequestreloadignoringcachedata timeoutinterval:25] delegate:self startimmediately:no]; //判断是否连接,没有连接就结束任务 if (self.connection == nil) { [self finishtask]; return; } //成功连接到服务器后检查是否取消任务,取消任务就结束 if (self.iscancelled) { [self finishtask]; return; } //设置任务开始执行 self.executing = yes; //获取当前runloop nsrunloop *currentrunloop = [nsrunloop currentrunloop]; //将任务交由runloop规划 [self.connection scheduleinrunloop:currentrunloop formode:nsrunloopcommonmodes]; //开始从服务端获取数据 [self.connection start]; //判断执行任务的是否为主线程 if (currentrunloop != [nsrunloop mainrunloop]) { //不为主线程启动runloop cfrunlooprun(); } } //mark - nsurlconnectiondelegate 方法 - (void)connection:(nsurlconnection *)connection didreceiveresponse:(nsurlresponse *)response { //获取并设置将要下载文件的长度大小 self.filetotallength = response.expectedcontentlength; } - (void)connection:(nsurlconnection *)connection didfailwitherror:(nserror *)error { //网络获取失败,调用代理方法 if ([self.delegate respondstoselector:@selector(filedownloadoperation:didfailwitherror:)]) { //需要将代理方法放到主线程中执行,防止代理方法需要修改ui dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate filedownloadoperation:self didfailwitherror:error]; }); } } - (void)connection:(nsurlconnection *)connection didreceivedata:(nsdata *)data { //收到数据包后判断任务是否取消,取消则结束任务 if (self.iscancelled) { [self finishtask]; return; } //添加获取的数据 [self.filemutabledata appenddata:data]; //修改已下载文件长度 self.downloadedlength += [data length]; //调用回调函数 if ([self.delegate respondstoselector:@selector(filedownloadoperation:downloadprogress:)]) { //计算下载比例 double progress = self.downloadedlength * 1.0 / self.filetotallength; //同上,放在主线程中调用,防止主线程有修改ui的操作 dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate filedownloadoperation:self downloadprogress:progress]; }); } } - (void)connectiondidfinishloading:(nsurlconnection *)connection { //网络下载完成前检查是否取消任务,取消就结束任务 if (self.iscancelled) { [self finishtask]; return; } //调用回调函数 if ([self.delegate respondstoselector:@selector(filedownloadoperation:didfinishwithdata:)]) { //同理,放在主线程中调用 dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate filedownloadoperation:self didfinishwithdata:self.filemutabledata]; }); } //下载完成,任务结束 [self finishtask]; } @end
上述代码的注释很详尽,就不再赘述了,只提供了取消下载的功能,还可以添加暂停和断点下载的功能,读者可自行实现。具体效果如下图,点击取消后就会结束任务:
上一篇: 详解Oracle数据库各类控制语句的使用
下一篇: 程序开发面试题