SDWebImage源码阅读(七)SDWebImageDownloaderOperation(上)

  SDWebImageDownloaderOperation 继承自 NSOperation。

  这里首先学习 NSOperation 类。

  Foundation 框架下的 NSOperation 类:

  在 iOS 开发中,通常会把比较耗时的操作放在主线程之外的子线程里面去执行。而开辟子线程基本都是使用 API 相对简单易懂的 GCD 去操作,把所有的操作代码都放在 block 里面去书写,相对于其它开辟子线程的方法降低了一定的开发难度,当然这里的降低难度只是相对于 API 的使用而言的,会使用和知道其底层原理是有天壤之别的。NSOperation 在 iOS 4 后也是基于 GCD 实现的。

  相对于 GCD 来说 NSOperation 拥有更强的可控性和代码可读性,并且可以加入操作依赖。

  NSOperation 是一个抽象基类,表示一个独立的计算单元,可以给它的子类提供有用且线程安全的建立状态、优先级、依赖、取消等操作。并且系统在 NSOperation.h 里面已经封装了两个继承自 NSOperation 的类,NSBlockOperation 和 NSInvocationOperation,这两个实体类使用起来也非常简单,和 NSOperationQueue 搭配使用已基本能胜任所有开发过程中的多线程需求,不过在开发过程中也有使用继承自 NSOperation 的自定义类并定制符合开发需求的自定义操作,本篇的主角就是继承自 NSOperation。

  下面主要学习和 NSOperation 类相关的知识:

  NSOperation.h

1 #define NSOperationQualityOfService NSQualityOfService
2 #define NSOperationQualityOfServiceUserInteractive NSQualityOfServiceUserInteractive
3 #define NSOperationQualityOfServiceUserInitiated NSQualityOfServiceUserInitiated
4 #define NSOperationQualityOfServiceUtility NSQualityOfServiceUtility
5 #define NSOperationQualityOfServiceBackground NSQualityOfServiceBackground

  这里定义了5个宏用来表示 Quality of Service。

 1 /* The following Quality of Service (QoS) classifications are used to indicate to the system the nature and importance of work.  They are used by the system to manage a variety of resources.  Higher QoS classes receive more resources than lower ones during resource contention. */
 2 typedef NS_ENUM(NSInteger, NSQualityOfService) {
 3     /* UserInteractive QoS is used for work directly involved in providing an interactive UI such as processing events or drawing to the screen. */
 4     NSQualityOfServiceUserInteractive = 0x21,
 5     
 6     /* UserInitiated QoS is used for performing work that has been explicitly requested by the user and for which results must be immediately presented in order to allow for further user interaction.  For example, loading an email after a user has selected it in a message list. */
 7     NSQualityOfServiceUserInitiated = 0x19,
 8     
 9     /* Utility QoS is used for performing work which the user is unlikely to be immediately waiting for the results.  This work may have been requested by the user or initiated automatically, does not prevent the user from further interaction, often operates at user-visible timescales and may have its progress indicated to the user by a non-modal progress indicator.  This work will run in an energy-efficient manner, in deference to higher QoS work when resources are constrained.  For example, periodic content updates or bulk file operations such as media import. */
10     NSQualityOfServiceUtility = 0x11,
11     
12     /* Background QoS is used for work that is not user initiated or visible.  In general, a user is unaware that this work is even happening and it will run in the most efficient manner while giving the most deference to higher QoS work.  For example, pre-fetching content, search indexing, backups, and syncing of data with external systems. */
13     NSQualityOfServiceBackground = 0x09,
14 
15     /* Default QoS indicates the absence of QoS information.  Whenever possible QoS information will be inferred from other sources.  If such inference is not possible, a QoS between UserInitiated and Utility will be used. */
16     NSQualityOfServiceDefault = -1
17 } NS_ENUM_AVAILABLE(10_10, 8_0);

  Queality of Service 是在 iOS 8 和 OS X 10.10 出现的一个新的概念,它为调度系统创造一致的高层语义。

  对于NSOperation来说,为了支持这一新的 quality Of Service属性,threadProperty 这一属性便废弃了。

  通过设置不同的枚举值告诉系统当前在进行什么样的工作,然后系统会通过合理的资源控制来最高效的执行任务代码,其中主要涉及到 CPU 调度的优先级、IO 优先级、任务运行在哪个线程以及运行的顺序等等,我们通过一个抽象的 Quality of Service 枚举值来表明服务质量的意图以及类别。

  一个高质量的服务就意味着更多的资源得以提供来更快的完成操作。

  它的每一个枚举值用以表示一个操作的性质和紧迫性。
  应用程序选择最合适的值用以操作,以确保一个良好的用户体验。

  1.NSQualityOfServiceUserInteractive 与用户交互的任务,这些任务通常跟 UI 级别的刷新相关,比如动画,这些任务需要在一瞬间完成。

  2.NSQualityOfServiceUserInitiated 由用户发起的并且需要立即得到结果的任务,比如滑动 scrollView时去加载数据用于后续 cell 的显示,这些任务通常跟后续的用户交互相关,在几秒或者更短的时间内完成。

  3.NSQualityOfServiceUtility 用于表述执行一项工作后,用户并不需要立即得到结果。这一工作通常用户已经请求过或者在初始化的时候已经自动执行,不会阻碍用户用户的进一步交互,通常在用户可见的时间尺度和可能由一个非模态的进度指示器展示给用户。一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载的任务,这些任务可能花费几秒或者几分钟的时间

  4.NSQualityOfServiceBackground 这些任务对用户不可见,比如后台进行备份的操作,这些任务可能需要较长的时间,几分钟甚至几个小时

  5.NSQualityOfServiceDefault 默认的QoS表明QoS信息缺失。尽可能的从其它资源推断可能的QoS信息。如果这一推断不成立,一个位于 NSQualityOfServiceUserInitiated 和 NSQualityOfServiceUtility 之间的 QoS 将得以使用。

  NSOperation 定义:

  状态

1 NS_CLASS_AVAILABLE(10_5, 2_0)
2 @interface NSOperation : NSObject {
3 @private
4     id _private;
5     int32_t _private1;
6 #if __LP64__
7     int32_t _private1b;
8 #endif
9 }

5 @property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below
6 @property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);
1 @property (readonly, getter=isCancelled) BOOL cancelled;
2 
3 @property (readonly, getter=isExecuting) BOOL executing;
4 @property (readonly, getter=isFinished) BOOL finished;
5 @property (readonly, getter=isReady) BOOL ready;

  表示 NSOperation 状态的属性。NSOperation提供了 ready、cancelled、executing、finished 这几个状态变化,我们的开发也是必须处理自己关心的其中的状态。这些状态都是基于 keypath 的 KVO 通知决定,所以在你手动改变自己关心的状态时,请别忘了手动发送通知。这里面每个属性都是相互独立的,同时只可能有一个状态是 YES。 finished 这个状态在操作完成后请及时设置为 YES,因为 NSOperationQueue 所管理的队列中,只有 isFinished 为 YES 时才将其移除队列,这点在内存管理和避免死锁很关键。

  依赖

1 - (void)addDependency:(NSOperation *)op;
2 - (void)removeDependency:(NSOperation *)op;
3 
4 @property (readonly, copy) NSArray<NSOperation *> *dependencies;

  表示 NSOperation 之间添加和移除依赖关系。假如 opA 依赖于 opB,那么在 opB 执行结束之前,opA 将永远不会执行。

  NSOperation 中我们可以为操作分解为若干个小的任务,通过添加他们之间的依赖关系进行操作,这点在设计上是很有意义的。比如我们最常用的图片异步加载,第一步我们是去通过网络进行加载,第二步我们可能需要对图片进行下处理(调整大小或者压缩保存)。

  注意的是不能添加相互依赖,像 opA 依赖 opB,opB 依赖 opA,这样会导致死锁!还有一点必须要注意的时候,在每个操作完成时,请将 isFinished 设置为 YES,不然后续的操作是不会开始执行的。

  执行

1 - (void)start;
2 - (void)main;

  表示 NSOperation 开始执行。

  执行一个 operation 有两种方法:

  第一种是自己手动的调用 start 这个方法,这种方法调用会在当前调用的线程进行同步执行,所以在主线程里面自己一定要小心的调用,不然就会把主线程给卡死,还不如直接用 GCD 呢。

  第二种是将 operation 添加到 operationQueue 中去,这个也是我们用得最多的也是提倡的方法。NSOperationQueue 会在我们添加进去 operation 的时候尽快进行执行。当然如果 NSOperationQueue 的 maxConcurrentOperationCount 如果设置为 1 的话,进相当于 FIFO了。

  队列是怎么调用我们的执行的操作的呢?如果你只是想弄一个同步的方法,那很简单,你只要重写 main 这个函数,在里面添加你要的操作。如果想定义异步的方法的话就重写 start 方法。在你添加进 operationQueue 中的时候系统将自动调用你这个 start 方法,这时将不再调用 main 里面的方法。

  取消

1 - (void)cancel;

  NSOperation 允许我们取消一个操作的执行。当然,这个操作并不是我们所想象的取消。

  这个取消的步骤是这样的:

  如果这个操作在队列中没有执行,那么这个时候取消并将状态 finished 设置为 YES,那么这个时候的取消就是直接取消了。

  如果这个操作已经在执行了,那么我们只能等其操作完成。当我们调用 cancel 方法的时候,他只是将 isCancelled 设置为 YES。所以,在我们的操作中,我们应该在每个操作开始前,或者在每个有意义的实际操作完成后,先检查下这个属性是不是已经设置为 YES。如果是 YES,则后面操作都可以不用再执行了。

  completionBlock

1 @property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);

  completionBlock 在 iOS 4 后添加这个 block 属性,在这个操作完成时,将会调用这个 block 一次,这样也非常方便的让我们在主线程对 view 进行更新或者添加自己的其它业务逻辑代码。

  优先级

1 typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
2     NSOperationQueuePriorityVeryLow = -8L,
3     NSOperationQueuePriorityLow = -4L,
4     NSOperationQueuePriorityNormal = 0,
5     NSOperationQueuePriorityHigh = 4,
6     NSOperationQueuePriorityVeryHigh = 8
7 };
8 
9 @property NSOperationQueuePriority queuePriority;

  表示 NSOperation 的优先级。operationQueue 有 maxConcurrentOperationCount 设置,当队列中operation 很多时而你想让后续的操作提前被执行的时候,你可以为 operation 设置优先级。

  NSOperation 通用的方法

 1     NSOperation *operation = [[NSOperation alloc] init];

     NSLog(@"当前主线程 %@", [NSThread mainThread]);

 2     // 执行结束后调用
 3     [operation setCompletionBlock:^{
 4         NSLog(@"执行结束 %@", [NSThread currentThread]);

        NSLog(@"主线程 %@", [NSThread mainThread]);

 5     }];
 6     
 7     // 开始执行
 8     [operation start];
 9     // 取消执行
10     [operation cancel];

  打印结果:

2017-06-01 05:53:39.340 NSOperation_DEMO[96150:3821714] 主线程 <NSThread: 0x600000260680>{number = 1, name = main}

2017-06-01 05:53:39.341 NSOperation_DEMO[96150:3821858] 执行结束 <NSThread: 0x608000273ec0>{number = 3, name = (null)}

2017-06-01 05:53:39.342 NSOperation_DEMO[96150:3821858] 主线程 <NSThread: 0x600000260680>{number = 1, name = (null)}

  NSInvocationOperation 

 1 NS_CLASS_AVAILABLE(10_5, 2_0)
 2 NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available")
 3 @interface NSInvocationOperation : NSOperation {
 4 @private
 5     id _inv;
 6     id _exception;
 7     void *_reserved2;
 8 }
 9 
10 - (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
11 - (instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER;
12 
13 @property (readonly, retain) NSInvocation *invocation;
14 
15 @property (nullable, readonly, retain) id result;
16 
17 @end

  18 FOUNDATION_EXPORT NSExceptionName const NSInvocationOperationVoidResultException NS_AVAILABLE(10_5, 2_0);


  19 FOUNDATION_EXPORT NSExceptionName const NSInvocationOperationCancelledException NS_AVAILABLE(10_5, 2_0);

   NSInvocationOperation 的使用方式和给 Button 添加事件比较相似,需要一个对象和一个 Selector。

 1     // 创建 NSInvocationOperation
 2     NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testNSInvocationOperation) object:nil];
 3     // 执行
 4     [invocationOperation start];
 5 
 6 
 7 - (void)testNSInvocationOperation {
 8     NSLog(@"当前线程 %@", [NSThread currentThread]);
 9     NSLog(@"主线程 %@", [NSThread mainThread]);
10 }

  打印结果:

1 2017-05-31 06:23:21.222 NSOperation_DEMO[54413:3219363] 当前线程 <NSThread: 0x608000069300>{number = 1, name = main}
2 2017-05-31 06:23:21.222 NSOperation_DEMO[54413:3219363] 主线程 <NSThread: 0x608000069300>{number = 1, name = main}

  可以看到 NSInvocationOperation 其实是同步执行的,单独使用的话,并不能开辟子线程,它需要配合 NSOperationQueue 去使用才能实现多线程调用。

  NSBlockOperation

 1 NS_CLASS_AVAILABLE(10_6, 4_0)
 2 @interface NSBlockOperation : NSOperation {
 3 @private
 4     id _private2;
 5     void *_reserved2;
 6 }
 7 
 8 + (instancetype)blockOperationWithBlock:(void (^)(void))block;
 9 
10 - (void)addExecutionBlock:(void (^)(void))block;
11 @property (readonly, copy) NSArray<void (^)(void)> *executionBlocks;
12 
13 @end

  NSBlockOperation 也是 NSOperation 的子类,支持并发的实行一个或多个 block。

 1     NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
 2         NSLog(@"1 在第 %@ 个线程", [NSThread currentThread]);
 3     }];
 4     
 5     [blockOperation addExecutionBlock:^{
 6         NSLog(@"2 在第 %@ 个线程", [NSThread currentThread]);
 7     }];
 8     
 9     [blockOperation addExecutionBlock:^{
10         NSLog(@"3 在第 %@ 个线程", [NSThread currentThread]);
11     }];
12     
13     [blockOperation addExecutionBlock:^{
14         NSLog(@"4 在第 %@ 个线程", [NSThread currentThread]);
15     }];
16     
17     [blockOperation addExecutionBlock:^{
18         NSLog(@"5 在第 %@ 个线程", [NSThread currentThread]);
19     }];
20     
21     [blockOperation addExecutionBlock:^{
22         NSLog(@"6 在第 %@ 个线程", [NSThread currentThread]);
23     }];
24     
25     [blockOperation start];

  执行多次,第一次执行结果:

1 2017-05-31 06:45:56.827 NSOperation_DEMO[54466:3235350] 1 在第 <NSThread: 0x60800007f640>{number = 1, name = main} 个线程
2 2017-05-31 06:45:56.828 NSOperation_DEMO[54466:3235350] 5 在第 <NSThread: 0x60800007f640>{number = 1, name = main} 个线程
3 2017-05-31 06:45:56.827 NSOperation_DEMO[54466:3235396] 2 在第 <NSThread: 0x60000027c440>{number = 5, name = (null)} 个线程
4 2017-05-31 06:45:56.828 NSOperation_DEMO[54466:3235350] 6 在第 <NSThread: 0x60800007f640>{number = 1, name = main} 个线程
5 2017-05-31 06:45:56.827 NSOperation_DEMO[54466:3235394] 3 在第 <NSThread: 0x608000278000>{number = 4, name = (null)} 个线程
6 2017-05-31 06:45:56.827 NSOperation_DEMO[54466:3235393] 4 在第 <NSThread: 0x608000266f80>{number = 3, name = (null)} 个线程

  第二次执行结果:

1 2017-05-31 06:50:30.807 NSOperation_DEMO[54479:3239479] 4 在第 <NSThread: 0x608000274d80>{number = 4, name = (null)} 个线程
2 2017-05-31 06:50:30.807 NSOperation_DEMO[54479:3239434] 1 在第 <NSThread: 0x60000007c3c0>{number = 1, name = main} 个线程
3 2017-05-31 06:50:30.807 NSOperation_DEMO[54479:3239476] 3 在第 <NSThread: 0x600000277d00>{number = 5, name = (null)} 个线程
4 2017-05-31 06:50:30.808 NSOperation_DEMO[54479:3239434] 5 在第 <NSThread: 0x60000007c3c0>{number = 1, name = main} 个线程
5 2017-05-31 06:50:30.807 NSOperation_DEMO[54479:3239477] 2 在第 <NSThread: 0x60800026a140>{number = 3, name = (null)} 个线程
6 2017-05-31 06:50:30.808 NSOperation_DEMO[54479:3239479] 6 在第 <NSThread: 0x608000274d80>{number = 4, name = (null)} 个线程

  第三次执行结果:

1 2017-05-31 06:57:00.966 NSOperation_DEMO[54496:3244098] 1 在第 <NSThread: 0x608000263580>{number = 1, name = main} 个线程
2 2017-05-31 06:57:00.966 NSOperation_DEMO[54496:3244098] 5 在第 <NSThread: 0x608000263580>{number = 1, name = main} 个线程
3 2017-05-31 06:57:00.966 NSOperation_DEMO[54496:3244098] 6 在第 <NSThread: 0x608000263580>{number = 1, name = main} 个线程
4 2017-05-31 06:57:00.966 NSOperation_DEMO[54496:3244179] 2 在第 <NSThread: 0x60800027b740>{number = 4, name = (null)} 个线程
5 2017-05-31 06:57:00.966 NSOperation_DEMO[54496:3244161] 4 在第 <NSThread: 0x608000461c40>{number = 5, name = (null)} 个线程
6 2017-05-31 06:57:00.966 NSOperation_DEMO[54496:3244159] 3 在第 <NSThread: 0x60000026e800>{number = 3, name = (null)} 个线程

  三次运行结果的比较,可以看到,NSBlockOperation 确实实现了多线程。但是也可以看到,它并非是将所有的 block 都放到放到了子线程中。通过上面的打印记录可以发现,它会优先将 block 放到主线程中执行,若主线程已有待执行的代码,就开辟新的线程,但最大并发数为 4(包括主线程在内)。如果 block 数量大于了4,那么剩下的 Block 就会等待某个线程空闲下来之后被分配到该线程,且依然是优先分配到主线程。

  同一个block中的代码是同步执行的。

  增加更多的 block,并给每条 block 添加两行代码。

 1     NSBlockOperation * blockOperation = [NSBlockOperation blockOperationWithBlock:^{
 2         NSLog(@"1 在第%@个线程",[NSThread currentThread]);
 3         NSLog(@"1 HML");
 4     }];
 5     
 6     [blockOperation addExecutionBlock:^{
 7         NSLog(@"2 在第%@个线程",[NSThread currentThread]);
 8         NSLog(@"2 HML");
 9     }];
10     
11     [blockOperation addExecutionBlock:^{
12         NSLog(@"3 在第%@个线程",[NSThread currentThread]);
13         NSLog(@"3 HML");
14     }];
15     
16     [blockOperation addExecutionBlock:^{
17         NSLog(@"4 在第%@个线程",[NSThread currentThread]);
18         NSLog(@"4 HML");
19     }];
20     
21     [blockOperation addExecutionBlock:^{
22         NSLog(@"5 在第%@个线程",[NSThread currentThread]);
23         NSLog(@"5 HML");
24     }];
25     
26     [blockOperation addExecutionBlock:^{
27         NSLog(@"6 在第%@个线程",[NSThread currentThread]);
28         NSLog(@"6 HML");
29     }];
30     
31     [blockOperation addExecutionBlock:^{
32         NSLog(@"7 在第%@个线程",[NSThread currentThread]);
33         NSLog(@"7 HML");
34     }];
35     
36     [blockOperation addExecutionBlock:^{
37         NSLog(@"8 在第%@个线程",[NSThread currentThread]);
38         NSLog(@"8 HML");
39     }];
40     
41     [blockOperation addExecutionBlock:^{
42         NSLog(@"9 在第%@个线程",[NSThread currentThread]);
43         NSLog(@"9 HML");
44     }];
45     
46     [blockOperation addExecutionBlock:^{
47         NSLog(@"10 在第%@个线程",[NSThread currentThread]);
48         NSLog(@"10 HML");
49     }];
50     
51     [blockOperation start];

  执行结果:

 1 2017-05-31 07:22:11.460 NSOperation_DEMO[54567:3261733] 1 在第<NSThread: 0x60800007e640>{number = 1, name = main}个线程
 2 2017-05-31 07:22:11.460 NSOperation_DEMO[54567:3261799] 2 在第<NSThread: 0x600000268100>{number = 3, name = (null)}个线程
 3 2017-05-31 07:22:11.460 NSOperation_DEMO[54567:3261733] 1 HML
 4 2017-05-31 07:22:11.461 NSOperation_DEMO[54567:3261733] 5 在第<NSThread: 0x60800007e640>{number = 1, name = main}个线程
 5 2017-05-31 07:22:11.460 NSOperation_DEMO[54567:3261800] 4 在第<NSThread: 0x60000027d4c0>{number = 5, name = (null)}个线程
 6 2017-05-31 07:22:11.461 NSOperation_DEMO[54567:3261733] 5 HML
 7 2017-05-31 07:22:11.461 NSOperation_DEMO[54567:3261733] 6 在第<NSThread: 0x60800007e640>{number = 1, name = main}个线程
 8 2017-05-31 07:22:11.462 NSOperation_DEMO[54567:3261733] 6 HML
 9 2017-05-31 07:22:11.460 NSOperation_DEMO[54567:3261802] 3 在第<NSThread: 0x600000271100>{number = 4, name = (null)}个线程
10 2017-05-31 07:22:11.462 NSOperation_DEMO[54567:3261733] 7 在第<NSThread: 0x60800007e640>{number = 1, name = main}个线程
11 2017-05-31 07:22:11.463 NSOperation_DEMO[54567:3261733] 7 HML
12 2017-05-31 07:22:11.463 NSOperation_DEMO[54567:3261733] 8 在第<NSThread: 0x60800007e640>{number = 1, name = main}个线程
13 2017-05-31 07:22:11.463 NSOperation_DEMO[54567:3261733] 8 HML
14 2017-05-31 07:22:11.463 NSOperation_DEMO[54567:3261802] 3 HML
15 2017-05-31 07:22:11.462 NSOperation_DEMO[54567:3261800] 4 HML
16 2017-05-31 07:22:11.463 NSOperation_DEMO[54567:3261733] 9 在第<NSThread: 0x60800007e640>{number = 1, name = main}个线程
17 2017-05-31 07:22:11.461 NSOperation_DEMO[54567:3261799] 2 HML
18 2017-05-31 07:22:11.464 NSOperation_DEMO[54567:3261733] 9 HML
19 2017-05-31 07:22:11.463 NSOperation_DEMO[54567:3261802] 10 在第<NSThread: 0x600000271100>{number = 4, name = (null)}个线程
20 2017-05-31 07:22:11.464 NSOperation_DEMO[54567:3261802] 10 HML

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

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

  所以 NSBlockOperation 也不是一个理想的多线程解决方案,尽管我们可以在第一个 block (第一个 block 始终在主线程执行)中创建 UI,在其他 Block 做数据处理等操作,但还是感觉哪里不舒服。

  自定义 NSOperation 

   NSOperation 是可以自定义的。如果 NSInvocationOperation 和 NSBlockOperation 无法满足开发需求,可以选择自定义一个 NSOperation。
  经过上面的分析,我们发现,系统提供的两种 NSOperation 是一定满足不了我们的需求的。
  那我们是不是需要自定义一个NSOperation呢?
  答案是,不需要。
  自定义 NSOperation 并不难,但是依然要写不少代码,这违背了我们简单实现多线程的初衷。况且,接下来介绍我们今天真正的主角 NSOperationQueue。

  NSOperationQueue 

 1 static const NSInteger NSOperationQueueDefaultMaxConcurrentOperationCount = -1;
 2 
 3 NS_CLASS_AVAILABLE(10_5, 2_0)
 4 @interface NSOperationQueue : NSObject {
 5 @private
 6     id _private;
 7     void *_reserved;
 8 }
 9 
10 - (void)addOperation:(NSOperation *)op;
11 - (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);
12 
13 - (void)addOperationWithBlock:(void (^)(void))block NS_AVAILABLE(10_6, 4_0);
14 
15 @property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
16 @property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
17 
18 @property NSInteger maxConcurrentOperationCount;
19 
20 @property (getter=isSuspended) BOOL suspended;
21 
22 @property (nullable, copy) NSString *name NS_AVAILABLE(10_6, 4_0);
23 
24 @property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);
25 
26 @property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue NS_AVAILABLE(10_10, 8_0);
27 
28 - (void)cancelAllOperations;
29 
30 - (void)waitUntilAllOperationsAreFinished;
31 
32 #if FOUNDATION_SWIFT_SDK_EPOCH_AT_LEAST(8)
33 @property (class, readonly, strong, nullable) NSOperationQueue *currentQueue NS_AVAILABLE(10_6, 4_0);
34 @property (class, readonly, strong) NSOperationQueue *mainQueue NS_AVAILABLE(10_6, 4_0);
35 #endif
36 
37 @end

  NSOperationQueue 就是执行 NSOperation 的队列,我们可以将一个或多个 NSOperation 对象放到队列中去执行。

  比如 NSInvocationOperation,我们来将它放到队列中:

1     // 创建 NSInvocationOperation
2     NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testNSInvocationOperation) object:nil];
3     // 创建 NSOperationQueue
4     NSOperationQueue *queue = [[NSOperationQueue alloc] init];
5     [queue addOperation:invocationOperation];
6     
7     NSLog(@"主线程 %@", [NSThread mainThread]);
1 - (void)testNSInvocationOperation {
2     NSLog(@"Invo 内部打印 当前线程 %@", [NSThread currentThread]);
3     NSLog(@"Invo 内部打印 主线程 %@", [NSThread mainThread]);
4 }

  打印结果: 

1 2017-05-31 08:40:41.078 NSOperation_DEMO[54743:3290148] 主线程 <NSThread: 0x600000077f00>{number = 1, name = main}
2 2017-05-31 08:40:41.078 NSOperation_DEMO[54743:3290190] Invo 内部打印 当前线程 <NSThread: 0x608000274580>{number = 4, name = (null)}
3 2017-05-31 08:40:41.080 NSOperation_DEMO[54743:3290190] Invo 内部打印 主线程 <NSThread: 0x600000077f00>{number = 1, name = (null)}

  可以看到,现在 NSInvocationOperation 的执行已经被放到子线程中了。

  把 NSBlockOperation 也添加到这个 NSOperationQueue 里面:

1 //    [blockOperation start];
2     [queue addOperation:blockOperation];

  执行结果:

 1 2017-05-31 08:45:40.363 NSOperation_DEMO[54756:3295522] 主线程 <NSThread: 0x608000072f40>{number = 1, name = main}
 2 2017-05-31 08:45:40.363 NSOperation_DEMO[54756:3295632] 1 在第<NSThread: 0x600000277640>{number = 4, name = (null)}个线程
 3 2017-05-31 08:45:40.363 NSOperation_DEMO[54756:3295605] 2 在第<NSThread: 0x60000027f5c0>{number = 5, name = (null)}个线程
 4 2017-05-31 08:45:40.363 NSOperation_DEMO[54756:3295628] Invo 内部打印 当前线程 <NSThread: 0x600000266440>{number = 3, name = (null)}
 5 2017-05-31 08:45:40.363 NSOperation_DEMO[54756:3295606] 3 在第<NSThread: 0x60000027f580>{number = 6, name = (null)}个线程
 6 2017-05-31 08:45:40.365 NSOperation_DEMO[54756:3295632] 1 HML
 7 2017-05-31 08:45:40.365 NSOperation_DEMO[54756:3295608] 4 在第<NSThread: 0x608000262b80>{number = 7, name = (null)}个线程
 8 2017-05-31 08:45:40.366 NSOperation_DEMO[54756:3295605] 2 HML
 9 2017-05-31 08:45:40.368 NSOperation_DEMO[54756:3295628] Invo 内部打印 主线程 <NSThread: 0x608000072f40>{number = 1, name = (null)}
10 2017-05-31 08:45:40.368 NSOperation_DEMO[54756:3295606] 3 HML
11 2017-05-31 08:45:40.369 NSOperation_DEMO[54756:3295632] 5 在第<NSThread: 0x600000277640>{number = 4, name = (null)}个线程
12 2017-05-31 08:45:40.370 NSOperation_DEMO[54756:3295605] 6 在第<NSThread: 0x60000027f5c0>{number = 5, name = (null)}个线程
13 2017-05-31 08:45:40.371 NSOperation_DEMO[54756:3295606] 7 在第<NSThread: 0x60000027f580>{number = 6, name = (null)}个线程
14 2017-05-31 08:45:40.369 NSOperation_DEMO[54756:3295608] 4 HML
15 2017-05-31 08:45:40.374 NSOperation_DEMO[54756:3295605] 6 HML
16 2017-05-31 08:45:40.372 NSOperation_DEMO[54756:3295632] 5 HML
17 2017-05-31 08:45:40.376 NSOperation_DEMO[54756:3295606] 7 HML
18 2017-05-31 08:45:40.376 NSOperation_DEMO[54756:3295608] 8 在第<NSThread: 0x608000262b80>{number = 7, name = (null)}个线程
19 2017-05-31 08:45:40.378 NSOperation_DEMO[54756:3295605] 9 在第<NSThread: 0x60000027f5c0>{number = 5, name = (null)}个线程
20 2017-05-31 08:45:40.379 NSOperation_DEMO[54756:3295632] 10 在第<NSThread: 0x600000277640>{number = 4, name = (null)}个线程
21 2017-05-31 08:45:40.381 NSOperation_DEMO[54756:3295632] 10 HML
22 2017-05-31 08:45:40.380 NSOperation_DEMO[54756:3295605] 9 HML
23 2017-05-31 08:45:40.380 NSOperation_DEMO[54756:3295608] 8 HML

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

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

  更简单的使用方式 

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

1     NSLog(@"主线程 %@", [NSThread mainThread]);
2     NSOperationQueue *testOperationQueue = [[NSOperationQueue alloc] init];
3     [testOperationQueue addOperationWithBlock:^{
4         NSLog(@"operationQueue blok 执行 %@", [NSThread mainThread]);
5     }];

  打印结果:

1 2017-05-31 10:19:14.137 NSOperation_DEMO[74900:3375884] 主线程 <NSThread: 0x600000067440>{number = 1, name = main}
2 2017-05-31 10:19:14.140 NSOperation_DEMO[74900:3375966] operationQueue blok 执行 <NSThread: 0x600000067440>{number = 1, name = (null)}
 1     NSLog(@"主线程 %@", [NSThread mainThread]);
 2     
 3     NSOperationQueue *testOperationQueue = [[NSOperationQueue alloc] init];
 4     
 5     [testOperationQueue addOperationWithBlock:^{
 6         NSLog(@"1 operationQueue blok 执行 %@", [NSThread mainThread]);
 7         NSLog(@"1 HML");
 8     }];
 9     
10     [testOperationQueue addOperationWithBlock:^{
11         NSLog(@"2 operationQueue blok 执行 %@", [NSThread mainThread]);
12         NSLog(@"2 HML");
13     }];

  打印结果:

1 2017-05-31 10:22:44.435 NSOperation_DEMO[74924:3379628] 主线程 <NSThread: 0x60000007ad80>{number = 1, name = main}
2 2017-05-31 10:22:44.436 NSOperation_DEMO[74924:3379712] 1 operationQueue blok 执行 <NSThread: 0x60000007ad80>{number = 1, name = (null)}
3 2017-05-31 10:22:44.436 NSOperation_DEMO[74924:3379674] 2 operationQueue blok 执行 <NSThread: 0x60000007ad80>{number = 1, name = (null)}
4 2017-05-31 10:22:44.436 NSOperation_DEMO[74924:3379712] 1 HML
5 2017-05-31 10:22:44.436 NSOperation_DEMO[74924:3379674] 2 HML

  可以同时添加一个或多个 Block 来实现操作,但是它们都是在当前线程同步执行的。

  添加依赖关系

  NSOperationQueue 最吸引人的无疑是它的添加依赖的功能。 

  假如 opA 依赖于 opB,那么在 opB执行结束之前,opA将永远不会执行。

1     NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testNSInvocationOperation1) object:nil];
2     NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testNSInvocationOperation2) object:nil];
3     
4     NSOperationQueue *queue = [[NSOperationQueue alloc] init];
5     [op2 addDependency:op1];
6     
7     [queue addOperation:op1];
8     [queue addOperation:op2];
1 - (void)testNSInvocationOperation1 {
2     NSLog(@"op 1 第 %@ 个线程", [NSThread currentThread]);
3     NSLog(@"1 HML");
4 }
5 
6 - (void)testNSInvocationOperation2 {
7     NSLog(@"op 2 第 %@ 个线程", [NSThread currentThread]);
8     NSLog(@"2 HML");
9 }

  打印结果:

1 2017-05-31 11:36:03.698 NSOperation_DEMO[95103:3445337] op 1 第 <NSThread: 0x60800007d680>{number = 4, name = (null)} 个线程
2 2017-05-31 11:36:03.699 NSOperation_DEMO[95103:3445337] 1 HML
3 2017-05-31 11:36:03.699 NSOperation_DEMO[95103:3445358] op 2 第 <NSThread: 0x60000007c680>{number = 3, name = (null)} 个线程
4 2017-05-31 11:36:03.700 NSOperation_DEMO[95103:3445358] 2 HML

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

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

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

  设置优先级

1 typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
2     NSOperationQueuePriorityVeryLow = -8L,
3     NSOperationQueuePriorityLow = -4L,
4     NSOperationQueuePriorityNormal = 0,
5     NSOperationQueuePriorityHigh = 4,
6     NSOperationQueuePriorityVeryHigh = 8
7 };
8 
9 @property NSOperationQueuePriority queuePriority;

  NSOperation 对象都有一个 queuePriority 属性,表示当前的队列的优先级。

  其它操作及注意事项:

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

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

 

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

 

知其然亦知其所以然-NSOperation并发编程

  这篇强烈推荐,很有深度的文章。

END

参考链接:http://www.jianshu.com/p/a044cd145a3d

http://www.jianshu.com/p/0c241a4918bf

http://www.jianshu.com/p/51df21dbcdec

http://www.jianshu.com/p/f9e01c69a46f

http://www.jianshu.com/p/ebb3e42049fd

posted @ 2017-05-29 06:15  鳄鱼不怕牙医不怕  阅读(317)  评论(0编辑  收藏  举报