还在用GCD?来看看NSOperation吧

作者: _奔跑的炸鸡 | 来源:发表于2016-03-28 19:20 被阅读8252次
    2016年03月29日10:42:36更新

    感谢@皮特尔 的提醒


    在iOS开发中,谈到多线程,大家第一时间想到的一定是GCD。GCD固然是一套强大的多线程解决方案,能够解决绝大多数的多线程问题,但是他易于上手难于精通且到处是坑的特点也注定了想熟练使用它有一定的难度。而且很多人嘴上天天挂着GCD,实际上对它的实际应用也不甚了解。
    再者说,在现在的主流开发模式下,能用到多线程的绝大多数就是网络数据请求和网络图片加载,这两点上AFNetwork+SDWebImage已经能满足几乎所有的需求。而剩下的一小部分,简单好用的NSOperation无疑是比GCD更有优势的。
    因此,如果你还是坚持『GCD大法好』,那看到这里就不必再看了。如果你想试一试更简单的方法,那就随我来吧。


    什么是NSOperation?

    和GCD一样,NSOperation也是苹果提供给我们的一套多线程解决方案。实际上它也是基于GCD开发的,但是比GCD拥有更强的可控性和代码可读性。
    NSOperation是一个抽象基类,基本没有什么实际使用价值。我们使用最多的是系统封装好的NSInvocationOperationNSBlockOperation
    不过NSOperation一些通用的方法你要知道

    NSOperation * operation = [[NSOperation alloc]init];
    //开始执行
    [operation start];
    //取消执行
    [operation cancel];
    //执行结束后调用的Block
    [operation setCompletionBlock:^{
        NSLog(@"执行结束");
    }];
    
    使用NSInvocationOperation

    NSInvocationOperation的使用方式和给Button添加事件比较相似,需要一个对象和一个Selector。使用方法非常简单。
    我们先来写一个方法
    - (void)testNSOperation
    {
    NSLog(@"我在第%@个线程",[NSThread currentThread]);
    }
    然后调用它

    //创建
    NSInvocationOperation * invo = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(testNSInvocationOperation) object:nil];
    //执行
    [invo start];
    

    得到这样的执行结果

    执行结果

    我们可以看到NSInvocationOperation其实是同步执行的,因此单独使用的话,这个东西也没有什么卵用,它需要配合我们后面介绍的NSOperationQueue去使用才能实现多线程调用,所以这里我们只需要记住有这么一个东西就行了。

    使用NSBlockOperation
    • 终于到了我们今天的第一个重点
      NSBlockOperation也是NSOperation的子类,支持并发的实行一个或多个block,使用起来简单又方便
      执行以下代码
      NSBlockOperation * blockOperation = [[NSBlockOperation
      blockOperationWithBlock:^{
      NSLog(@"1在第%@个线程",[NSThread currentThread]);
      }];
      [blockOperation addExecutionBlock:^{
      NSLog(@"2在第%@个线程",[NSThread currentThread]);
      }];
      [blockOperation addExecutionBlock:^{
      NSLog(@"3在第%@个线程",[NSThread currentThread]);
      }];
      [blockOperation addExecutionBlock:^{
      NSLog(@"4在第%@个线程",[NSThread currentThread]);
      }];
      [blockOperation addExecutionBlock:^{
      NSLog(@"5在第%@个线程",[NSThread currentThread]);
      }];
      [blockOperation addExecutionBlock:^{
      NSLog(@"6在第%@个线程",[NSThread currentThread]);
      }];
      这里我们多执行两次并比较结果
    第一次执行的结果 第二次执行的结果 第三次执行的结果
    • 通过三次不同结果的比较,我们可以看到,NSBlockOperation确实实现了多线程。但是我们可以看到,它并非是将所有的block都放到放到了子线程中。通过上面的打印记录我们可以发现,它会优先将block放到主线程中执行,若主线程已有待执行的代码,就开辟新的线程,但最大并发数为4(包括主线程在内)。如果block数量大于了4,那么剩下的Block就会等待某个线程空闲下来之后被分配到该线程,且依然是优先分配到主线程。
    • 另外,同一个block中的代码是同步执行的

    为了证明以上猜想,我们为它增加更多block,并给每条block添加两行代码。

     NSBlockOperation * blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1在第%@个线程",[NSThread currentThread]);
        NSLog(@"1haha");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"2在第%@个线程",[NSThread currentThread]);
        NSLog(@"2haha");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"3在第%@个线程",[NSThread currentThread]);
        NSLog(@"3haha");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"4在第%@个线程",[NSThread currentThread]);
        NSLog(@"4haha");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"5在第%@个线程",[NSThread currentThread]);
        NSLog(@"5haha");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"6在第%@个线程",[NSThread currentThread]);
        NSLog(@"6haha");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"7在第%@个线程",[NSThread currentThread]);
        NSLog(@"7haha");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"8在第%@个线程",[NSThread currentThread]);
        NSLog(@"8haha");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"9在第%@个线程",[NSThread currentThread]);
        NSLog(@"9haha");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"10在第%@个线程",[NSThread currentThread]);
        NSLog(@"10haha");
    }];
    
    [blockOperation start];
    

    然后我们看一下执行结果

    执行结果

    ]

    • 我们可以看到,最大并发数为4,使用同一个线程的block一定是等待前一个block的代码全部执行结束后才执行,且同步执行。

    关于最大并发数
    在刚才的结果中我们看到最大并发数为4,但这个值并不是一个固定值。4是我在模拟器上运行的结果,而如果我使用真机来跑的话,最大并发数始终为2。因此,具体的最大并发数和运行环境也是有关系的。我们不必纠结于这个数字

    所以NSBlockOperation也不是一个理想的多线程解决方案,尽管我们可以在第一个block中创建UI,在其他Block做数据处理等操作,但还是感觉哪里不舒服。
    别着急,我们继续往下看

    自定义NSOperation

    是的,你没看错,NSOperation是可以自定义的。如果NSInvocationOperationNSBlockOperation无法满足你的需求,你可以选择自定义一个NSOperation。
    经过上面的分析,我们发现,系统提供的两种NSOperation是一定满足不了我们的需求的。
    那我们是不是需要自定义一个NSOperation呢?
    答案是,不需要。
    自定义NSOperation并不难,但是依然要写不少代码,这违背了我们简单实现多线程的初衷。况且,接下来我会介绍我们今天真正的主角--NSOperationQueue。所以,我打算直接跳过这一个环节。
    如果确实有同学需要的话,可以私信我。。。 如果很多人需要的话。。 我会额外写一篇。。。
    (读者:你TM不讲还这么多废话(╯‵□′)╯︵┻━┻)

    NSOPerationQueue
    简单使用

    终于轮到我们今天的主角了。
    顾名思义,NSOperationQueue就是执行NSOperation的队列,我们可以将一个或多个NSOperation对象放到队列中去执行。
    比如我们上面介绍过的NSInvocationOperation,我们来将它放到队列中来

    //依然调用上面的那个方法
    NSInvocationOperation * invo = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(testNSInvocationOperation) object:nil];
    
    NSOperationQueue * queue = [[NSOperationQueue alloc]init];
    [queue addOperation:invo];
    

    看一下执行结果

    执行结果

    现在它已经被放到子线程中执行了

    我们把刚才写的NSBlockOperation也加到这个Queue中来

     ...原来的代码
    //[blockOperation start];
    [queue addOperation:blockOperation];
    

    然后我们再来看执行情况

    执行结果

    我们看到,NSInvocationOperation 和 NSBlockOperation是异步执行的,NSBlockOperation中的每一个Block也是异步执行且都在子线程中执行,每一个Block内部也依然是同步执行。

    是不是简单好用又强大?

    放入队列中的NSOperation对象不需要调用start方法,NSOPerationQueue会在『合适』的时机去自动调用

    更简单的使用方式

    除了上述的将NSOperation添加到队列中的使用方法外,NSOperationQueue提供了一个更加简单的方法,只需以下两行代码就能实现多线程调用

       NSOperationQueue * queue = [[NSOperationQueue alloc]init];
    [queue addOperationWithBlock:^{
        //这里是你想做的操作
    }];
    

    你可以同时添加一个或这个多个Block来实现你的操作
    怎么样,是不是简单的要死?
    (原来这篇文章只需要看这两句就行了是嘛?😡😡😡😡😡😡😡😡😡😡)

    添加依赖关系

    如果NSOperationQueue仅能做到这些,那我也不必大费周章了。
    NSOperationQueue最吸引人的无疑是它的添加依赖的功能。
    举个例子,假如A依赖于B,那么在B执行结束之前,A将永远不会执行
    示例代码如下

    {
    NSInvocationOperation * op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(testNSInvocationOperation1) object:nil];
    NSInvocationOperation * op2 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(testNSInvocationOperation2) object:nil];
    NSOperationQueue * queue = [[NSOperationQueue alloc]init];
    [op2 addDependency:op1];
    [queue addOperation:op1];
    [queue addOperation:op2];
    
    }
    - (void)testNSInvocationOperation1
    {
    NSLog(@"我是op1  我在第%@个线程",[NSThread currentThread]);
    }
    - (void)testNSInvocationOperation2
    {
    NSLog(@"我是op2 我在第%@个线程",[NSThread currentThread]);
    }
    

    然后无论你运行多少次,得到的一定是这样的结果

    必然结果

    这就是依赖关系的好处,op2必定会在op1之后执行,这样会大大方便我们的流程控制。

    使用依赖关系有三点需要注意
    1.不要建立循环依赖,会造成死锁,原因同循环引用
    2.使用依赖建议只使用NSInvocationOperation,NSInvocationOperation和NSBlockOperation混用会导致依赖关系无法正常实现。
    3.依赖关系不光在同队列中生效,不同队列的NSOperation对象之前设置的依赖关系一样会生效

    2016年03月29日11:16:00更新

    之前放的代码有一点小小的问题 添加依赖的代码最好放到添加队列之前
    前面说过,NSOperationQueue会在『合适』的时间自动去执行任务,因此你无法确定它到底何时执行,有可能前一秒添加的任务,在你这一秒准备添加依赖的时候就已经执行完了,就会出现依赖无效的假象。代码已更正,谢谢评论区各位提醒

    设置优先级

    每一个NSOperation的对象都一个queuePriority属性,表示队列优先级。它是一个枚举值,有这么几个等级可选

    优先级可选等级

    大家可以去设置试试,不过它并不总是起作用,目前我还没有找到原因。所以还是建议用依赖关系来控制流程。
    如果有小伙伴知道怎么让优先级始终生效的办法,请告知我。。。

    其他操作及注意事项

    NSOperationQueue提供暂停和取消两种操作。
    设置暂停只需要设置queue的suspended属性为YESNO即可
    取消你可以选择调用某个NSOperation的cancle方法,也可以调用Queue的cancelAllOperations方法来取消全部线程

    这里需要强调的是,所谓的暂停和取消并不会立即暂停或取消当前操作,而是不在调用新的NSOperation。

    改变queue的maxConcurrentOperationCount可以设置最大并发数。
    这里依然有两点需要注意
    1.最大并发数是有上限的,即使你设置为100,它也不会超过其上限,而这个上限的数目也是由具体运行环境决定的
    2.设置最大并发数一定要在NSOperationQueue初始化后立即设置,因为上面说过,被放到队列中的NSOperation对象是由队列自己决定何时执行的,有可能你这边一添加立马就被执行。因此要想让设置生效一定要在初始化后立即设置


    结束语
    到这里,NSOperation的知识我们已经介绍完毕,如果你尝试用一两次的话,你一定会爱上他。

    作者水平有限,不正确的地方请指出

    如果我的文章对你有帮助,请点赞或评论

    相关文章

      网友评论

      • 不是坏人的自来卷:GCD和operation在底层没有什么区别,使用Operation的好处是将任务对象化了,管理更加方便。
      • 3b1ae4c6ef21:真的是啥话都敢说啊。
        NSOperation无疑是比GCD更有优势,这个结论是怎么来的?
      • 狗狗臭鸡蛋:基本线程的使用还是GCD方便点,一些架构类的东西,方便进行管理的,使用queue的形式好点
      • 邢泽:学习了。
      • brilliance_Liu:开启新的线程,执行完任务之后,如何回归主线程刷新UI?GCD开启线程之后,可以获取主线程,回归主线程,刷新UI。NSOperation怎么做?我在学习 :stuck_out_tongue_closed_eyes:
        啊左:用:- (void)setCompletionBlock:(void (^)(void))block;
        没有梦想_何必远方:@_奔跑的炸鸡 这样不对。你不乱说。
        _奔跑的炸鸡:@brilliance_Liu 不好意思,现在才回复,按照我个人经验,直接用就OK,刷新UI的操作会被自动添加到主线程。
      • Nonnil:最初,NSThread,NSOperation、GCD是由具体到抽象,由用户态到内核态的清晰变化。但后来苹果对NSOperation实现机制做了重写,用GCD来实现NSOperaton。也就是说,如今的NSOperaton是GCD的封装,没多大可比性。要比的话,只是直接使用API与封装的比较。直接用必然更灵活,更轻量。封装则更易用,更健壮。
      • 范佳昌ryan:这个好
      • ce673fab38bb:博主写的真好,之前看过很多GCD总是似懂非懂,看到这个确实容易理解 :+1:
        ce673fab38bb:@_奔跑的炸鸡 :smiley: :smile: 支持,期待!
        _奔跑的炸鸡:@墨小尘觞 哈哈😄😄😄 我会继续坚持写出简单易懂的文章的
      • Young森:
        NSInvocationOperation* invocationA = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(invocationA) object:nil];
        NSInvocationOperation* invocationB = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(invocationB) object:nil];

        NSOperationQueue* queue = [[NSOperationQueue alloc]init];
        [queue addOperation:invocationA];

        [queue addOperation:invocationB];
        // B依赖于A,那么在A执行结束之前,B永远不会执行
        [invocationB addDependency:invocationA];
        我设置的并没有什么用,还是随机的
        _奔跑的炸鸡:@静静waiting 那就好 :relieved: :relieved: :relieved:
        Young森::heart_eyes:ok了
        _奔跑的炸鸡:@静静waiting 把添加依赖的语句写到 添加队列之前 因为你先添加的invocationA 可能刚添加完 在你添加依赖之前就已经执行了
      • adf61a070c47: 加了依赖关系不会顺序执行。 我就执行了5次。至少有一次是依赖方先执行。
        adf61a070c47:@_奔跑的炸鸡 可以了 3q
        _奔跑的炸鸡:@farelod 把添加依赖的语句写到 添加队列之前 因为你先添加的操作 可能刚添加完 在你添加依赖之前就已经执行了
      • 起个啥名字呢:写的不错哦,可以在介绍另外2个多线程的使用和区别么?
        _奔跑的炸鸡:@起个啥名字呢 谢谢 :relieved: :relieved: :relieved:
        起个啥名字呢:@_奔跑的炸鸡 嗯,期待你接下来的文章👻
        _奔跑的炸鸡:@起个啥名字呢 因为接下来要写的东西已经基本确定了,如果要写的话可能也会稍微靠后一点。我觉得如果是为了实际使用,NSOperation已经完全够用了 :relieved:
      • a05832db24ea:另外NSOperationQueue中的operation是顺序执行,还是对每个operation都创建了子线程去并发执行?
        _奔跑的炸鸡:@屎壳郎123 恩 其实GCD和NSOperation实际使用的时候是十分相似的,包括执行效率之类的个人也没有感觉出什么差异。所以用哪个其实更取决于个人喜好。我虽然向大家推荐了NSOperation,但是我个人其实也蛮喜欢GCD的。
        a05832db24ea:@_奔跑的炸鸡 谢谢。说下我的情况,我是用swift开发OS X程序给自己用。比如一次SQL查询,可能有4、5个,要开多个线程,我是用多个自定义对象,对象里实现GCD去执行每一个SQL查询
        _奔跑的炸鸡:@屎壳郎123 每个NSOperation都会创建到子线程中并发执行 如果是 NSBlockOpreation,它的每个block也会被放到单独的子线程,前提都是有空闲可用的子线程,没有的话就等到有了再去执行。
      • a05832db24ea:我是新手,一直用GCD,用于异步连接数据库,执行查询SQL等操作,作者能否说说它的坑?如果换成NSOperation有何优势?
        a05832db24ea:@_奔跑的炸鸡 确实,dispatch那几个函数,我到现在都不记得具体的名字,每次都是copy,哈哈
        _奔跑的炸鸡:@屎壳郎123 比如不能往主队列添加同步任务否则导致无限阻塞,比如dispatch_once_t必须是static或全局变量,比如dispatch_queue_create的第二个参数到底是传NULL还是别的。 注意事项还蛮多的,有兴趣的话可以自己去查一些资料。NSOperation最大的优势就是简单好用,而且他是纯OC的语法,对于新手来说可读性和可操作性都比GCD强。但是从开发的角度来讲的话,个人认为两者并没有很大差异,全凭个人喜好选择。比如我们天天在用的AFNetwork,多线程上用的其实也是NSOperationQueue。
      • 潇湘wei雨:这么多介绍多线程的文章,这篇是我第一次看懂看完的。作者写的通俗易懂。特别好
        _奔跑的炸鸡:@潇湘wei雨 哈哈😄 这也是我写东西的初衷
      • 皮特尔:线程优先级代表的是线程获取CPU时间片的能力,高优先级的执行概率高,不是执行顺序靠前。是两个概念。
        狗狗臭鸡蛋:cpu会尽力将资源给高优先级的,尽量使高优先级的先执行,但优先级具有随机行,并不是高的就一定先执行
        ValienZh:GCD:有一次执行,延迟执行,调度组。
        NSOperation:可设置最大并发数,暂停取消队列,可直接指定操作间依赖。
        _奔跑的炸鸡:@皮特尔 😌😌好像确实是这样 谢谢指正

      本文标题:还在用GCD?来看看NSOperation吧

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