【iOS】多线程以及GCD和NSOperation

iOS多线程

  • 线程基础
    • 进程与线程
    • NSThread类
  • GCD
    • 认识GCD
    • 任务
    • 队列
    • 队列与任务结合
    • 线程间通信
    • 队列组dispatch group
    • 延迟执行操作
  • NSOperation
    • 简介
    • 基本使用
    • NSOperation、NSOperationQueue 常用属性和方法归纳
      • NSOperation 常用属性和方法
      • NSOperationQueue 常用属性和方法
    • 小结

线程基础

现代的操作系统,包括iOS,都支持线程(Thread)的概念。每个进程可以包含多个线程,借助多个CPU的内核,可以同时处理多个任务。即便是在早期单核CPU的时代,操作系统也可以把CPU资源在多个线程之间进行切换,给用户的感觉是在“同时”处理多个任务。

进程与线程

每个应用程序都有一个进程。例如在Mac系统中,可以通过活动监视器来查看当前Mac正在运行的进程。每个进程可以包含多个线程,这些线程可以同时运行,并各自处理一些任务。早期的CPU只有一个核心,因此会交替处理各个线程中的任务,让用户产生一种多任务同时处理的“感觉”。当CPU拥有多个内核时,各个线程就可以分配到不同的CPU内核中进行处理,即为真正意义上的多任务同时处理,如图所示:
【iOS】多线程以及GCD和NSOperation_第1张图片
对于任意一个iOS应用,程序运行起来后,默认会产生一个主线程(MainThread)。主线程专门用来处理UIKit对象的操作,如界面的显示与更新、处理用户交互事件等(请牢记这点,所有与UI相关的操作都要在主线程中进行)。

对于一个应用来说,之所以引入多个线程,很大程度上是由于存在一些操作是非常耗时的,例如:发送网络请求并等待服务器的响应,这种耗时操作是不能够放在主线程中进行操作的,因为在等待的时间内主线程被使用,用户是不能做任何交互动作的,因而会极大影响用户体验。对于耗时的操作,需要再另外创建一个线程,放到后台处理,当处理完成得到结果后,再返回主线程去设置UI界面,这就涉及线程间通信的相关知识。

多线程同时处理任务,还会涉及线程安全(thread-safe)的问题,当多个线程对一个对象进行同时操作时,就会影响结果的正确性。因此,线程对某个对象操作时,需要使用到“锁”的机制,即当一个线程操作一个对象的过程中,会给这个对象上锁,其他线程就不能够访问该对象了。加锁虽然解决了线程安全的问题,但带来的另外一个弊端就是影响了程序运行的效率。

当给一个自定义类中添加属性时,属性关键字其中就有atomicnonatomic的区分,其中,atomic是线程安全的,当有线程访问这个属性时,会为该属性的setter方法加锁,atomic是默认值。但是在实际的开发中,都会把给属性设置nonatomic关键字,因为对于移动设备来说,效率更加重要,但也需要程序员注意线程安全问题。

NSThread类

NSThred类是苹果官方提供的管理线程的类,提供了一些线程管理的方法。但是随着GCD和NSOperation的推出,NSThread的使用场景已经大大减少。在实际的开发中,偶尔会使用NSThread类来获取一些线程信息。常用的一些方法如下:

  • 获取当前线程对象。
+ (NSThread *) currentThread;
  • 获取主线程对象。
+ (NSThread *) mainThread;
  • 使主线程休眠ti秒。
+ (void)sleepForTimeInterval:(NSTimeInterval)ti

GCD

GCD(Grand Central Dispatch)技术,苹果首先应用于OS X中,随后在iOS中也引入了GCD技术。特别是苹果公司把CPU升级为多核后,GCD的使用变得更加广泛和重要。相比于NSThread,GCD中已经完全屏蔽了有关线程的概念,而是引入了任务和队列,把任务放到队列中执行,指定任务和队列的类型。其他有关线程管理的事务完全交由GCD来处理,大大简化了多任务开发的难度。

认识GCD

GCD是苹果公司推出的专门用于简化多线程编程的技术。在GCD中,程序员已经不再需要去关心有关线程的操作(如:线程创建、线程销毁、线程调度),而是引入了任务和队列两个核心概念。

由于GCD是苹果公司推出的技术,因此GCD能够很好地调度苹果设备的CPU资源,不论是在Mac平台,还是在iOS平台。特别是在iPhone中引入多核CPU之后,GCD的使用就变得越发重要。

由于GCD对线程管理进行了封装,因此,当工程师使用GCD时,只需要把任务(通常封装在一个Block中)添加到一个队列中执行,有关线程调度的工作完全交由GCD完成。

在使用GCD处理多任务执行时,只要按照如下步骤执行即可。

  • 在Block中定义需要执行的任务内容。
  • 把任务添加到队列queue中。

GCD对队列中的任务,按照“先进先出”的原则,根据任务添加到队列的顺序来对队列进行处理,GCD会根据任务和队列的类型,自动在多个线程之间分配工作。

任务

在GCD中,需要处理的事务统一使用Block封装起来,称为任务。任务有两种类型,同步任务和异步任务。通过调用不同的函数,来设置任务的类型。同时,任务编写在函数的Block参数中。

异步任务:执行任务时,会在另外的线程中执行,即可能会创建新的线程。

dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

同步任务:执行任务时,会在当前的线程中执行,当前线程有可能是主线程,也有可能是子线程。

dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

队列

GCD中,队列是一个重要概念。系统提供了若干预定义的队列,其中包括可以获取应用程序的主队列(任务始终在主线程上执行,与更新UI相关的操作必须在主队列中完成)。另外,工程师可以自由创建不同类型的队列,例如:并行队列和串行队列,队列的类型决定了任务的执行方式。GCD队列严格按照“先进先出”的原则,添加到GCD队列中的任务,始终会按照加入队列的顺序被执行。

并行队列:并行队列中的任务可以在多个线程之间分配执行,分配的原则由GCD控制,因此,并行队列中的任务,虽然执行时按照先进先出进行分配的,但由于各个任务被分配到不同的线程执行,因此其完成时间有可能不同,即:后分配的任务有可能先执行完成;并发队列一定需要和异步执行的任务(使用dispatch_async())结合起来使用才有意义。

串行队列:串行队列中的任务是按照顺序一个一个完成的,当一个任务完成后,才去执行下一个任务;因此,串行队列对应一个线程执行。

主队列:主队列也是一个串行队列,主队列中的任务都在主线程中执行。

程序员可以通过如下的函数来创建不同类型的队列。

  • 获取系统定义的并行队列。一般来说,开发中通常不需要自己创建并行队列,使用系统提供的即可。
dispatch_get_global_queue(intptr_t identifier, uintptr_t flags);
  • 创建队列。可以创建并行队列,也可以创建串行队列。该方法常用于创建串行队列。
dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t  _Nullable attr);
  • 获取主队列。
dispatch_get_main_queue(void);

队列与任务结合

在GCD中存在队列和任务两个核心概念,同时队列又分为并行队列、串行队列和主队列,任务包括异步任务和同步任务。GCD的使用方法就是把任务放到队列中执行,因而根据不同的任务类型和队列类型,就会存在6种组合。

  1. 异步任务+并行队列
    把异步任务放到并行队列进行执行,异步任务会在不同的线程中执行,这是最常使用的一种组合。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //获取一个并行队列,其队列优先级为默认,保留参数置为0
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //创建异步任务,并放到并行队列中执行
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

运行结果如图所示。异步任务+并行队列组合情况下,每个任务会在不同的线程中同时执行。

【iOS】多线程以及GCD和NSOperation_第2张图片

  1. 异步任务+串行队列
    对于异步任务放在串行队列中执行时,任务只会在一个新开的线程中,按照顺序进行执行。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //获取一个串行队列,其字符串标识符为com.1101iOS,枚举类型设为NULL会创建一个serial队列
    dispatch_queue_t queue = dispatch_queue_create("com.1101iOS", NULL);
    //创建异步任务,并放到串行队列中执行
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

运行结果如图所示。可以看到,所有任务都在一个线程中执行,并且完成一个后,再执行下一个。【iOS】多线程以及GCD和NSOperation_第3张图片

  1. 异步任务+主队列
    把异步任务放在主队列中执行,由于主队列是一个特殊的串行队列,因此任务是串行执行的,但由于主队列对应序号为1的线程,因此,即便是异步任务,也不会再创建新的线程。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //创建异步任务
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

运行结果如下所示。
【iOS】多线程以及GCD和NSOperation_第4张图片

  1. 同步任务+并行队列
    同步任务的执行是在当前线程中完成的,因此,即便是把同步任务放在并行队列中执行,由于只有1个线程,任务也是一个一个按顺序执行(串行执行)的。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //获取并行队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //同步执行
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

运行结果如图所示,由于viewDidLoad方法是在主线程中执行的,因此,创建的同步任务也在主线程中执行。
【iOS】多线程以及GCD和NSOperation_第5张图片

  1. 同步任务+串行队列
    同步任务放在串行队列中执行,任务会在当前线程依次执行。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.1101iOS", NULL);
    //同步执行
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

运行结果如图所示,由于viewDidLoad方法是在主线程中执行的,因此,创建的同步任务也在主线程中执行。
【iOS】多线程以及GCD和NSOperation_第6张图片

  1. 同步任务+主队列
    这种情况下,主线程会被阻塞,程序会挂死,不能使用。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //同步执行
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task1:%d", i);
        }
        NSLog(@"task1----%@", [NSThread currentThread]);
    });
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task2:%d", i);
        }
        NSLog(@"task2----%@", [NSThread currentThread]);
    });
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"task3:%d", i);
        }
        NSLog(@"task3----%@", [NSThread currentThread]);
    });
}

当调用dispatch_sync会阻塞线程,直到任务(Block)执行完成之后才能继续执行。主线程中初始时需要执行的代码如图A所示。主线程需要执行1、2、3这几块代码。

【iOS】多线程以及GCD和NSOperation_第7张图片
图A

执行完1之后主线程需要执行的代码如图B所示。将任务(Block)添加到主线程中去执行。同时阻塞主线程,直到任务(Block)执行完成。此时Block 1等待3的代码执行完成,而代码1需要等Block 1执行完成。这就造成了1和Block 1相互等待,成了死锁。
【iOS】多线程以及GCD和NSOperation_第8张图片
图B

线程间通信

由于UI的更新和操作是由主线程负责的,因此,当使用子线程获取到数据时(例如通过网络获取得到了服务器返回的数据),需要返回主线程对UI界面进行更新,这时就涉及线程间的通信。

  1. 线程间通信简介
    在涉及网络数据获取的过程中,一般会使用异步任务+并发队列进行数据获取。当获取到网络服务器返回的数据后,需要在主线程中把数据显示在屏幕上,因此这就涉及线程间的通信。在并行队列中的任务还可以嵌套子任务,嵌套的子任务可以设置为在主线程中执行的任务。

  2. 示例代码
    下面的示例代码模拟了一个从网络获取一张图片的数据,并在屏幕上显示在一个UIImageView控件上的过程。在这个案例中,设置一个网络下载的异步任务,并放在并行队列中执行。此时,GCD会在一个新的线程中执行下载任务,当网络图片下载完成后,由于涉及更新UI,所以必须嵌套一个子任务在主队列中执行,如图所示。

由于涉及发送网络请求,需要提前在info.plist文件中设置App Transport SecuritySettings参数,如下所示。

请添加图片描述

在按钮的点击方法中,在异步任务+并行队列中实现网络图片的下载操作,并嵌套一个子任务在主线程中更新界面:

- (void) downloadImage {
    //获取并行队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSLog(@"0---%@", [NSThread currentThread]);
    
    //异步任务
    dispatch_async(queue, ^{
       //从网络下载图片
        NSString *urlString = @"https://img-blog.csdnimg.cn/75ccdf747f2a4d259bbb2b1c26110878.jpeg";
        NSURL *url = [NSURL URLWithString:urlString];
        NSData *imageData = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:imageData];
        NSLog(@"1---%@", [NSThread currentThread]);
        
        //返回主线程设置UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            NSLog(@"2---%@", [NSThread currentThread]);
        });
        
    });
    NSLog(@"3---%@", [NSThread currentThread]);
}

通过日志打印的顺序可以看出程序执行的顺序,即:网络下载任务是在新的线程中执行的,并且图片下载完毕后,回到主线程设置图片显示。

队列组dispatch group

在使用GCD进行任务操作时,有时会希望若干个任务执行之间有先后执行的依赖关系,例如,当A、B两个异步任务完成后,再去完成C任务,这时就可以使用队列组dispatch group来完成。

  1. 队列组基本介绍
    在串行队列中,任务是按照进入队列的顺序依次执行,因此任务和任务之间是有明确的先后顺序的。但是对于并行队列的任务来说,由于任务会被自动分配到不同的线程中执行,因此任务完成的顺序是不确定的。假如希望给并行队列中的任务设置执行顺序,例如,当任务A和任务B完成后,再去完成任务C,就需要使用到任务组dispatch group。

在GCD中,苹果官方提供了如下一些有关队列组操作的函数。

创建队列组:

dispatch_group_t group = dispatch_group_create();

向队列组中插入一个异步任务:

dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

队列组中其他任务执行完成后,执行的任务,通常可以用来设置UI界面:

dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

队列组中的异步任务执行会在另外的进程中执行,等所有的任务都执行完成后,会通知执行dispatch_group_notify()中的任务。

延迟执行操作

在开发过程中,有时会希望把一些操作封装起来延迟一段时间后再执行。iOS开发中,有两种常用的方法可以实现延迟执行:一种是使用GCD;另外一种是使用NSRunLoop类中提供的方法。

  1. 使用GCD实现延迟执行
    在GCD中可以使用dispatch_after()函数,封装一段代码到Block中,在设置的延迟时间dispatch_time_t之后执行。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0*NSEC_PER_SEC));
    dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"延迟2.0秒");
    });
  1. 使用NSRunLoop类中的方法实现延迟执行
    在NSRunLoop类中,也提供了有关延迟执行的方法。由于这些方法是对NSObject类的扩展,因此,所有的类都可以使用。
@interface NSObject (NSDelayedPerforming)
- (void) performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void) performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
@end

下面的代码中也实现了延迟2.0秒打印一段日志的操作。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(printLog) withObject:nil afterDelay:2.0];
}
- (void) printLog {
    NSLog(@"延迟2秒后打印出来的日志");
}

NSOperation

GCD的核心是C语言写的系统服务,而NSOperation底层是通过GCD实现,也可以说NSOperation是GCD更高层次的抽象,这是他们之间最本质的区别。因此如果希望自定义任务,建议使用NSOperation。

简介

NSOperation在iOS 4后也基于GCD实现,但是相对于GCD来说可控性更强,并且可以加入操作依赖。NSOperation是一个抽象类,因此系统提供了NSBlockOperation和NSInvocationOperation两个子类,并且可以创建继承自NSOperation的自定义类。相比于GCD,NSOperation更加面向对象,开发者除了不需要去了解线程相关的概念之外,甚至连GCD中需要了解的异步/同步、并行/串行都不太需要深入了解,开发者只要懂得任务和队列即可。

基本使用

  1. NSOperation的类族
    由于NSOperation是一个抽象类,因此不能够直接使用NSOperation,但苹果提供了两个NSOperation的子类,NSBlockOperation和NSInvocationOperation,除此之外,还可以使用自定义CustomOperation类。如下:

【iOS】多线程以及GCD和NSOperation_第9张图片
对于NSBlockOperation类来讲,可以把任务封装在一个Block块之内。NSBlockOperation类提供了如下操作方法,用于创建一个NSBlockOperation对象,任务封装在Block中。

+ (instancetype)blockOperationWithBlock:(void (^)(void))block;

对于NSInvocationOperation类来说,需要执行的任务直接指定已定义的方法。

- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
  1. 无队列情况下执行任务
    在不创建队列的情况下,可以直接调用NSOperation类的start方法来执行某个任务。该任务是在当前线程进行执行的,如果是在主线程调用的start方法,那么则会在主线程中执行任务。

下面的示例代码中,在当前线程中串行执行两个任务。

//调用operation的start方法,在当前线程中串行执行,无队列
- (void) executeInCurrentThread {
    NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task1-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task2-----%@", [NSThread currentThread]);
    }];
    //调用start方法,会在当前线程中串行执行
    [task1 start];
    [task2 start];
}

运行结果:
请添加图片描述

  1. 在队列中执行任务
    当把任务放到队列(NSOperationQueue类)中进行执行时,系统会自动并行执行所有任务,对于开多少条线程之类的事务,完全交由系统处理,开发者只要把任务添加到队列中即可。另外,可以通过设置队列的maxConcurrentOperationCount属性,来设置并行执行任务的数量。
    下方的示例代码中,在一个队列中插入了5个任务,这5个任务并行执行。系统会同时开启多个线程,多个任务并行执行。另外,maxConcurrentOperationCount决定了“并发”任务的数量,而不是创建线程的数量。即便设置为1,不同的任务也有可能在不同的线程中执行。
- (void) executeInQueue {
    NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task1-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task2-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task3-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task4 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task4-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task5 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task5-----%@", [NSThread currentThread]);
    }];
    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //设置队列属性
    queue.maxConcurrentOperationCount = 5;
    //添加任务到队列
    [queue addOperation:task1];
    [queue addOperation:task2];
    [queue addOperation:task3];
    [queue addOperation:task4];
    [queue addOperation:task5];
}

运行结果如图所示。可见系统同时执行5个任务,执行的顺序是不确定的,并且创建了5条线程。
【iOS】多线程以及GCD和NSOperation_第10张图片

  1. 在任务中添加新任务
    通过调用NSOperation类的addExecutionBlock方法,可以为某个NSOperation对象增加额外的任务。当把这些新增的任务放到队列中执行时,也是并行执行的。
- (void)addExecutionBlock:(void (^)(void))block;

下面的示例代码中在任务中添加新任务,所有的任务都会并行执行。

- (void) addTaskInOperation {
    NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task1-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task2-----%@", [NSThread currentThread]);
    }];
    //task1中添加task
    [task1 addExecutionBlock:^{
        NSLog(@"task1 add task-----%@", [NSThread currentThread]);
    }];
    //task2中添加task
    [task2 addExecutionBlock:^{
        NSLog(@"task2 add task-----%@", [NSThread currentThread]);
    }];
    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //添加任务到队列
    [queue addOperation:task1];
    [queue addOperation:task2];
}

运行结果如图:

【iOS】多线程以及GCD和NSOperation_第11张图片

  1. 在队列中直接添加任务
    通过调用NSOperationQueue的addOperationWithBlock方法,可以向队列中直接添加任务Block。
- (void) addTaskInQueue {
    NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task1-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task2-----%@", [NSThread currentThread]);
    }];
    //task1中添加task
    [task1 addExecutionBlock:^{
        NSLog(@"task1 add task-----%@", [NSThread currentThread]);
    }];
    //task2中添加task
    [task2 addExecutionBlock:^{
        NSLog(@"task2 add task-----%@", [NSThread currentThread]);
    }];
    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //添加任务到队列
    [queue addOperation:task1];
    [queue addOperation:task2];
    //在queue中添加任务
    [queue addOperationWithBlock:^{
        NSLog(@"queue task-----%@", [NSThread currentThread]);
    }];
}

如下,向队列queue中直接添加任务,也是并行执行的。

【iOS】多线程以及GCD和NSOperation_第12张图片

  1. 在任务中创建completionBlock
    在NSOperation类中,可以通过设置completionBlock来创建所有任务执行完成后,自动调用的一个Block。该Block的执行是在任务执行后被调用的,有先后顺序
- (void) addCompletionBlock {
    NSBlockOperation *task1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task1-----%@", [NSThread currentThread]);
    }];
    NSBlockOperation *task2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"task2-----%@", [NSThread currentThread]);
    }];
    //task1中添加task
    [task1 addExecutionBlock:^{
        NSLog(@"task1 add task-----%@", [NSThread currentThread]);
    }];
    task1.completionBlock = ^{
        NSLog(@"task1 end!!! %@", [NSThread currentThread]);
    };
    //task2中添加task
    [task2 addExecutionBlock:^{
        NSLog(@"task2 add task-----%@", [NSThread currentThread]);
    }];
    task2.completionBlock = ^{
        NSLog(@"task2 end!!! %@", [NSThread currentThread]);
    };
    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //添加任务到队列
    [queue addOperation:task1];
    [queue addOperation:task2];
}

结果如图:
【iOS】多线程以及GCD和NSOperation_第13张图片

  1. NSOperation与GCD的区别
    NSOperation与GCD都是苹果公司提供的用于多线程操作的技术,但是在使用过程中也稍有区别。
    GCD是底层的C语言构成的API,而NSOperationQueue及相关对象是Objective-C的对象。在GCD中,在队列中执行的是由Block构成的任务,这是一个轻量级的数据结构;而NSOperation作为一个对象,提供了更多的选择。
  • 在NSOperationQueue中,可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而GCD没法停止已经加入queue的Block(其实是有的,但需要许多复杂的代码)。
  • NSOperation能够方便地设置依赖关系,可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行。而对于GCD,没有办法设置依赖关系,但是可以通过dispatch_barrier_async来实现这种效果
  • KVO可以应用在NSOperation中,可以监听一个Operation是否完成或取消,这样能比GCD更加有效地掌控执行的后台任务。
  • 在NSOperation中,能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行。而在GCD中,只能区分不同任务队列的优先级,如果要区分Block任务的优先级,也需要大量的复杂代码。
  • 用户能够对NSOperation进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将Block任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。
  • 效率上:直接使用GCD确实会更高效,而NSOperation会多一些开销,但是相比起上面这么多的优势,这使得使用NSOperation仍然是很划算的选择,具体怎么选择要看我们自己。

NSOperation、NSOperationQueue 常用属性和方法归纳

NSOperation 常用属性和方法

1.取消操作方法

- (void)cancel; 可取消操作,实质是标记 isCancelled 状态
2.判断操作状态方法

- (BOOL)isFinished; 判断操作是否已经结束
- (BOOL)isCancelled; 判断操作是否已经标记为取消
- (BOOL)isExecuting; 判断操作是否正在在运行
- (BOOL)isReady; 判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关
3.操作同步

- (void)waitUntilFinished; 阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步
- (void)setCompletionBlock:(void (^)(void))block; 会在当前操作执行完毕时执行 completionBlock
- (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成
- (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖
@property (readonly, copy) NSArray *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组

NSOperationQueue 常用属性和方法

1.取消/暂停/恢复操作

- (void)cancelAllOperations; 可以取消队列的所有操作
- (BOOL)isSuspended; 判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态
- (void)setSuspended:(BOOL)b; 可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列
2.操作同步

- (void)waitUntilAllOperationsAreFinished; 阻塞当前线程,直到队列中的操作全部执行完毕
3.添加/获取操作

- (void)addOperationWithBlock:(void (^)(void))block; 向队列中添加一个 NSBlockOperation 类型操作对象
- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; 向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束
- (NSArray *)operations; 当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)
- (NSUInteger)operationCount; 当前队列中的操作数
4.获取队列

+ (id)currentQueue; 获取当前队列,如果当前线程不是在 NSOperationQueue 上运行则返回 nil
+ (id)mainQueue; 获取主队列

小结

根据实际开发来说,GCD使用情况较多,简单高效。从编程原则上来看,应该是使用高层次的抽象,避免使用低层次的抽象。那么无疑我们应当选择NSOperation,因为复杂的任务可以自己通过NSOperation实现,日常还是GCD的天下,毕竟GCD有更高的并发和执行能力。。

你可能感兴趣的:(ios,objective-c)