美文网首页
iOS多线程之NSOperation<三>

iOS多线程之NSOperation<三>

作者: 随风流逝 | 来源:发表于2017-08-25 16:52 被阅读30次

    我们在前面两节分别讲了iOS多线程的Pthrea、NSThreadGCD,那么我们关于多线程的学习就剩下最后一个内容,就是NSOperation。
    NSOperation NSOperation其实是对GCD的封装,表示了一个独立的计算单元。NSOperation本身是一个抽象类,对我们来说并没有什么实用价值,但是系统帮我们封装了两个它的子类,并且我们也可以自己去封装,它的之类都是用线程安全的方式来建立状态、优先级、依赖性和取消等的模型。
    很多执行任务类型的案例都很好的运用了NSOperation,包括网络请求,图像压缩,自然语言处理或者其他很多需要返回处理后数据的、可重复的、结构化的、相对长时间运行的任务。
    前面学习GCD的时候我们知道,GCD有两个核心概念就是任务和队列,不能只创建任务,然后置之不理,那样并没有什么用处,NSOperation也是一样的,我们不能仅仅把计算单元做好之后就不管它了,我们还是和GCD一样把它放进一个队列中进行调度,这样我们的计算单元才会运作起来,这时候我们就需要另一个概念:NSOperationQueue。
    NSOperationQueue 控制着这些操作的执行,它扮演者任务调度的角色,它总是能在遵循先进先出的原则下‘让高优先级操作’能先于‘低优先级操作’运行,使它管理的操作能基本。
    接下来我们先不着急看NSOperation和NSOperationQueue给了我们什么接口,等我们先学习NSOperation有哪些操作可以使用。

    状态

    NSOperation包含了一个十分优雅的状态机来描述每一个操作的执行。

    isReady → isExecuting → isFinished

    为了替代不那么清晰的state属性,状态直接由上面那些keypath的KVO通知决定,也就是说,当一个操作在准备好被执行的时候,它发送了一个KVO通知给isReady的keypath,让这个keypath对应的属性isReady在被访问的时候返回YES。

    每一个属性对于其他的属性必须是互相独立不同的,也就是同时只可能有一个属性返回YES,从而才能维护一个连续的状态: - isReady: 返回 YES 表示操作已经准备好被执行, 如果返回NO则说明还有其他没有先前的相关步骤没有完成。 - isExecuting: 返回YES表示操作正在执行,反之则没在执行。 - isFinished : 返回YES表示操作执行成功或者被取消了,NSOperationQueue只有当它管理的所有操作的isFinished属性全标为YES以后操作才停止出列,也就是队列停止运行,所以正确实现这个方法对于避免死锁很关键。

    取消

    早些取消那些没必要的操作是十分有用的。取消的原因可能包括用户的明确操作或者某个相关的操作失败。

    与之前的执行状态类似,当NSOperation的-cancel状态调用的时候会通过KVO通知isCancelled的keypath来修改isCancelled属性的返回值,NSOperation需要尽快地清理一些内部细节,而后到达一个合适的最终状态。这个时候isCancelled和isFinished的值将是YES,而isExecuting的值则为NO。

    优先级

    不可能所有的操作都是一样重要,通过以下的顺序设置queuePriority属性可以加快或者推迟操作的执行:

    NSOperationQueuePriorityVeryHigh
    NSOperationQueuePriorityHigh
    NSOperationQueuePriorityNormal
    NSOperationQueuePriorityLow
    NSOperationQueuePriorityVeryLow

    此外,有些操作还可以指定threadPriority的值,它的取值范围可以从0.0到1.0,1.0代表最高的优先级。鉴于queuePriority属性决定了操作执行的顺序,threadPriority则指定了当操作开始执行以后的CPU计算能力的分配,如果你不知道这是什么,好吧,你可能根本没必要知道这是什么。

    依赖性

    根据你应用的复杂度不同,将大任务再分成一系列子任务一般都是很有意义的,而你能通过NSOperation
    的依赖性实现。
    比如说,对于服务器下载并压缩一张图片的整个过程,你可能会将这个整个过程分为两个操作(可能你还会用到这个网络子过程再去下载另一张图片,然后用压缩子过程去压缩磁盘上的图片)。显然图片需要等到下载完成之后才能被调整尺寸,所以我们定义网络子操作是压缩子操作的依赖,通过代码来说就是:

    [resizingOperation addDependency:networkingOperation];//设置resizingOperation依赖networkingOperation
    [operationQueue addOperation:networkingOperation];//操作添加进队列
    [operationQueue addOperation:resizingOperation];//操作添加进队列
    

    除非一个操作的依赖的isFinished返回YES,不然这个操作不会开始。时时牢记将所有的依赖关系添加到操作队列很重要,不然会像走路遇到一条大沟,就走不过去了哟。
    此外,确保不要意外地创建依赖循环,像A依赖B,B又依赖A,这也会导致杯具的死锁。

    completionBlock

    有一个在iOS 4和Snow Leopard新加入的十分有用的功能就是completionBlock属性。

    每当一个NSOperation执行完毕,它就会调用它的completionBlock属性一次,这提供了一个非常好的方式让你能在视图控制器(View Controller)里或者模型(Model)里加入自己更多自己的代码逻辑。比如说,你可以在一个网络请求操作的completionBlock来处理操作执行完以后从服务器下载下来的数据。

    对于现在Objective-C程序员必须掌握的工具中,NSOperation依然是最基本的一个。尽管GCD对于内嵌异步操作十分理想,NSOperation依旧提供更复杂、面向对象的计算模型,它对于涉及到各种类型数据、需要重复处理的任务又是更加理想的。在你的下一个项目里使用它吧,让它及带给用户欢乐,你自己也会很开心的。

    之前了解过NSOperation的同学们可能看出来了,没错,上面就是翻译Mattt Thompson的,学过iOS的应该都知道这位大神。

    下面我们开始学NSOperation和NSOperationQueue给我们提供的接口,然后针对常用接口的用法示例。

    执行操作
    //如果你子类化NSOperation类,你要重写start方法,如果你直接没有子类化,直接使用start,默认是在主线程执行的。
    - (void)start;
    
    //如果你子类化NSOperation类,你可以重写main方法,并且实现它。如果你这样做,你就没有必要调用super。
    //你绝不能直接调用mian方法,你应该通过调用start方法启动你的线程。
    //如果直接调用好像是会在主线程执行
    - (void)main;
    
    //在NSOperation的任务完成之后要执行的方法块
    @property(copy) void (^completionBlock)(void);
    
    获取Operation状态
    //Operation是否被取消,getter方法用isCancelled
      @property (readonly, getter=isCancelled) BOOL cancelled;
    
    //取消一个Operation
    - (void)cancel;
    
    //Operation是否在执行中,getter方法用isExecuting
    @property (readonly, getter=isExecuting) BOOL executing;
    
    //Operation操作是否完成,getter方法用isFinished
    @property (readonly, getter=isFinished) BOOL finished;
    
    //Operation操作是否是异步的,getter方法用isConcurrent,不建议使用,被下边的asynchronous方法取代了
    @property (readonly, getter=isConcurrent) BOOL concurrent;
    
    //同concurrent
    @property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);
    
    //Operation操作是否已经准备就绪,getter方法用isFinished
    @property (readonly, getter=isReady) BOOL ready;
    
    依赖关系

    依赖关系可以有效的安排操作的顺序

    //添加依赖,就是在你添加的依赖Operation完成之后当前Operation才能开始执行
    - (void)addDependency:(NSOperation *)op;
    
    //删除一个之前添加的依赖Operation
    - (void)removeDependency:(NSOperation *)op;
    
    @property (readonly, copy) NSArray<NSOperation *> *dependencies;
    
    优先级
    typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
        NSOperationQueuePriorityVeryLow = -8L,
        NSOperationQueuePriorityLow = -4L,
        NSOperationQueuePriorityNormal = 0,
        NSOperationQueuePriorityHigh = 4,
        NSOperationQueuePriorityVeryHigh = 8
    }; //优先级
    //NSOperationQueue的优先级,对应上面的枚举
    @property NSOperationQueuePriority queuePriority;
    
    //这是线程优先级,已经被弃用了
    @property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);
    
    //这个东西在NSThread的时候已经说过了
    @property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);
    

    在NSThread的时候讲的NSQualityOfService,想看就看一下,在最后。

    等待NSOperation完成
    //知道operation完成之前,后面的代码都不会在执行,
    //所以为了避免死锁,在operation启动之前不要调用该方法
    - (void)waitUntilFinished;
    
    NSOperationQueue
    //添加一个操作
    - (void)addOperation:(NSOperation *)op;
    
    //添加一组Operation并且等到数组中的操作全部完成才会继续执行
    - (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);
    
    //快速添加一个operation任务
    - (void)addOperationWithBlock:(void (^)(void))block NS_AVAILABLE(10_6, 4_0);
    
    //队列中的operations
    @property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
    
    //操作的数量
    @property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
    
    //最大并发数
    @property NSInteger maxConcurrentOperationCount;
    
    //队列挂起,getter方法为isSuspended
    @property (getter=isSuspended) BOOL suspended;
    
    //队列名字
    @property (nullable, copy) NSString *name NS_AVAILABLE(10_6, 4_0);
    
    //说过了,去看NSThread最那里有写http://www.jianshu.com/p/b1962d8543ca
    @property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);
    
    //正在执行的操作的调度队列,默认值是nil,只有在队列中没有正在执行或排队的操作才能被设置这个属性,否则跑出NSInvalidArgumentException异常。
    @property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue NS_AVAILABLE(10_10, 8_0);
    
    //取消队列中的所有操作,正在执行的操作不取消
    - (void)cancelAllOperations;
    
    //和Operations的waitUntilFinished方法一样,这个是等待队列中所有的操作完成
    - (void)waitUntilAllOperationsAreFinished;
    
    //获取当前的操作队列,只读属性
    @property (class, readonly, strong, nullable) NSOperationQueue *currentQueue NS_AVAILABLE(10_6, 4_0);
    
    //获取主队列,只读属性
    @property (class, readonly, strong) NSOperationQueue *mainQueue NS_AVAILABLE(10_6, 4_0);
    

    前面说了系统帮我们封装好了NSOperation的两个子类,下面我们就用这两个子类实现一下NSOperation的常用方法。
    终于说到了NSOperation的子类,看一下NSInvocationOperation和NSBlockOperation两个的基本用法。
    首先是不配合NSOperationQueue,看一下什么效果

    NSLog(@"----");
        NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil]; //创建一个NSInvocationOperation 类型的Operation,绑定funcation方法
        [invocation start];
        NSLog(@"----");
        NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1]; //线程睡眠
            NSLog(@"---NSBlockOperation--%@", [NSThread currentThread]);
        }]; //创建一个NSBlockOperation 类型的Operation,添加块方法
        [block start];
    

    看一下输出结果

    2017-08-25 15:27:25.785 NSOperation[2944:455806] ----
    2017-08-25 15:27:26.787 NSOperation[2944:455806] ---invocation---<NSThread: 0x60800007bf40>{number = 1, name = main}
    2017-08-25 15:27:26.788 NSOperation[2944:455806] ----
    2017-08-25 15:27:27.789 NSOperation[2944:455806] ---NSBlockOperation--<NSThread: 0x60800007bf40>{number = 1, name = main}
    

    从输出结果看都是在主线程执行中同步执行的,所以不配合NSOperationQueue并没有什么意义。

    NSInvocationOperation
    - (void)invocation {
        NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
        NSInvocationOperation *invocation2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
        NSInvocationOperation *invocation3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
        //1.创建队列
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        [queue addOperation:invocation];
        [queue addOperation:invocation2];
        [queue addOperation:invocation3];
    }
    

    看一下输出结果

    2017-08-25 15:41:34.088 NSOperation[2989:469234] ---invocation---<NSThread: 0x60000007d440>{number = 4, name = (null)}
    2017-08-25 15:41:34.088 NSOperation[2989:469216] ---invocation---<NSThread: 0x608000260800>{number = 5, name = (null)}
    2017-08-25 15:41:34.088 NSOperation[2989:469218] ---invocation---<NSThread: 0x6080002607c0>{number = 3, name = (null)}
    

    输出结果显示每个Operation都开辟了新线程,并且异步执行,那NSBlockOperation会不会是一样的呢,来看一下吧

    - (void)block {
        NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"1----%@",[NSThread currentThread]);
        }];
        NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"2----%@",[NSThread currentThread]);
        }];
        NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"3----%@",[NSThread currentThread]);
        }];
        //创建队列
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        [queue addOperation:block1];
        [queue addOperation:block2];
        [queue addOperation:block3];
    }
    

    一起看一下输出结果

    2017-08-25 15:49:41.484 NSOperation[3013:475949] 1----<NSThread: 0x60000007ea80>{number = 5, name = (null)}
    2017-08-25 15:49:41.484 NSOperation[3013:475951] 2----<NSThread: 0x608000070740>{number = 4, name = (null)}
    2017-08-25 15:49:41.484 NSOperation[3013:475975] 3----<NSThread: 0x60000007ea00>{number = 3, name = (null)}
    

    和NSInvocationOperation的结果一样呢,那接下来我们看一下还有什么好用的方法吧。
    我们看到NSBlockOperation还有个addExecutionBlock方法,这是干嘛的呢?

    - (void)block {
        NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"1----%@",[NSThread currentThread]);
        }];
        NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"2----%@",[NSThread currentThread]);
        }];
        [block2 addExecutionBlock:^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"4---%@", [NSThread currentThread]);
        }];
        NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"3----%@",[NSThread currentThread]);
        }];
        //创建队列
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        [queue addOperation:block1];
        [queue addOperation:block2];
        [queue addOperation:block3];
    }
    

    先来看下打印结果再说

    2017-08-25 16:10:35.735 NSOperation[3114:492996] 3----<NSThread: 0x608000071180>{number = 5, name = (null)}
    2017-08-25 16:10:35.735 NSOperation[3114:493009] 2----<NSThread: 0x6000000711c0>{number = 4, name = (null)}
    2017-08-25 16:10:35.735 NSOperation[3114:493008] 1----<NSThread: 0x60800006a500>{number = 3, name = (null)}
    2017-08-25 16:10:35.735 NSOperation[3114:492993] 4---<NSThread: 0x600000071280>{number = 6, name = (null)}
    

    从结果来看似乎和新建一个NSBlockOperation效果一样啊,都是异步执行。
    看看队列有中有什么好用的方法吧

    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue setMaxConcurrentOperationCount:2];
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];
    
    输出结果:
    2017-08-25 15:53:02.280 NSOperation[3034:479336] 1----<NSThread: 0x60800007b7c0>{number = 3, name = (null)}
    2017-08-25 15:53:02.280 NSOperation[3034:479338] 2----<NSThread: 0x600000263d00>{number = 4, name = (null)}
    2017-08-25 15:53:03.353 NSOperation[3034:479335] 3----<NSThread: 0x60800007b740>{number = 5, name = (null)}
    

    我们在刚刚的NSBlockOperation测试的基础上加上了一句[queue setMaxConcurrentOperationCount:2],结果变得不一样了呢,3要比1和2晚一秒钟哦,刚好是3的睡眠时间,那就说明3是在1和2完成之后开始的,这就是最大并发量的作用,设置的数字就是它允许开辟的最大操作数数。
    最后,我们发现除了刚开始在线程中任务顺序执行外,我们一直没讲串行,因为NSOperationQueue是并行队列,我们想要串行就把最大并发量设置为1就可以了,是不是简单多了,不需要再怕不小心把串行并行写错了。
    前面有句话看清楚哦,是最大任务数,不是线程数,看看下面这种情况你就清楚其中的区别了

    //创建队列
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        [queue setMaxConcurrentOperationCount:1];//设置最大并发量
    //    [block1 addDependency:block3];//设置block1依赖block3
        block1.queuePriority = NSOperationQueuePriorityHigh;
        [queue addOperation:block1];
        [queue addOperation:block2];
        [queue addOperation:block3];
    
    输出结果:
    2017-08-25 16:44:21.361 NSOperation[3366:527290] 1----<NSThread: 0x6000002644c0>{number = 3, name = (null)}
    2017-08-25 16:44:22.435 NSOperation[3366:527290] 4---<NSThread: 0x6000002644c0>{number = 3, name = (null)}
    2017-08-25 16:44:22.435 NSOperation[3366:527274] 2----<NSThread: 0x600000267c40>{number = 4, name = (null)}
    2017-08-25 16:44:23.482 NSOperation[3366:527274] 3----<NSThread: 0x600000267c40>{number = 4, name = (null)}
    

    所以看时间我们能看到2和4是同一时间执行的,所以setMaxConcurrentOperationCount只能控制人物数量。

    前面说了好多遍依赖关系,看一下什么是依赖关系吧

    //创建队列
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        [queue setMaxConcurrentOperationCount:2];//设置最大并发量
        [block1 addDependency:block3];//设置block1依赖block3
        [queue addOperation:block1];
        [queue addOperation:block2];
        [queue addOperation:block3];
    
    输出结果:
    2017-08-25 16:00:12.378 NSOperation[3058:484669] 2----<NSThread: 0x608000267d40>{number = 3, name = (null)}
    2017-08-25 16:00:12.378 NSOperation[3058:484671] 3----<NSThread: 0x608000267d80>{number = 4, name = (null)}
    2017-08-25 16:00:13.380 NSOperation[3058:484668] 1----<NSThread: 0x60000007ae80>{number = 5, name = (null)}
    

    诶,队列不是先进先出的吗?为什么线程不够的时候不是1和2先执行呢?反而3先执行了。噢,我们设置了1依赖3,所以当cpu要杀1的时候,1说“不行,要杀我,先杀3”,所以先执行了2和3,最后执行1。
    也可以不初始化NSBlockOperation,直接在队列中添加block操作哦。

    //创建队列
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    //    [queue setMaxConcurrentOperationCount:1];//设置最大并发量
    //    [block1 addDependency:block3];//设置block1依赖block3
        block1.queuePriority = NSOperationQueuePriorityHigh;
        [queue addOperation:block1];
        [queue addOperation:block2];
        [queue addOperation:block3];
        
        [queue addOperationWithBlock:^{
            sleep(1);
            NSLog(@"5----%@",[NSThread currentThread]);
        }];
    
    输出结果:
    2017-08-25 16:41:30.151 NSOperation[3346:524342] 5----<NSThread: 0x60000007c980>{number = 3, name = (null)}
    2017-08-25 16:41:30.222 NSOperation[3346:524343] 1----<NSThread: 0x60000007cb80>{number = 4, name = (null)}
    2017-08-25 16:41:30.222 NSOperation[3346:524345] 2----<NSThread: 0x608000071340>{number = 5, name = (null)}
    2017-08-25 16:41:30.222 NSOperation[3346:524360] 3----<NSThread: 0x600000075c00>{number = 6, name = (null)}
    2017-08-25 16:41:30.222 NSOperation[3346:524359] 4---<NSThread: 0x60800007c780>{number = 7, name = (null)}
    

    看结果和用NSBlockOperation没什么区别哦

    最后的最后,关于iOS多线程就到这里了,希望对后学者能有一些帮助,那将是我的荣幸。

    相关文章

      网友评论

          本文标题:iOS多线程之NSOperation<三>

          本文链接:https://www.haomeiwen.com/subject/ppfudxtx.html