NSOperation-简介.md

NSOperation-简介

常用子类:

NSOperation是一个基类,不应该直接生成NSOperation对象,而是应该用它的子类。

  • NSInvocationOperation
    • 将特定对象的特定方法封装成NSOperation
  • NSBlockOperation
    • 将代码块封装成NSOpreation

示例:

创建NSInvocationOperation

1
2
3
4

NSString* url = @"http://www.xxx.com";
//以self的downloadImageFromURL:方法作为执行体,创建NSOperation
NSInvocationOperation* operation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(downloadImageFromURL:) object:url];

创建NSBlockOperation

1
2
3
4
5

NSBlockOperation* operation = [NSBlockOperation blockOperationWithBlock:^{
//执行体
...
}];

常用状态:

可以通过KVO监听NSOperation的状态

  • isCancelled
  • isAsynchronous
  • isExecuting
  • isFinished
  • isReady

执行任务

创建了一个NSBlockOperation,并且设置好它的block,也就是将要执行的任务。这个任务会在主线程中执行。

调用start方法让NSOperation方法运行起来。start是一个同步方法。

在当前任务状态和依赖关系合适的情况下,启动NSOperation的main方法任务,需要注意缺省实现只是在当前线程运行。如果需要并发执行,子类必须重写这个方法,并且使 - (BOOL)isConcurrent 方法返回YES

1
2
3
4
5
6
7
8
	
let operation = NSBlockOperation { () -> Void in
print(NSThread.currentThread())
}
operation.addExecutionBlock { () -> Void in
print("execution block1 --(NSThread.currentThread())")
}
operation.start()

默认的NSOperation是同步执行的。简单的看一下NSOperation类的定义会发现它有一个只读属性asynchronous

这意味着如果想要异步执行,就需要自定义NSOperation的子类。或者使用NSOperationQueue

取消任务

如果我们有两次网络请求,第二次请求会用到第一次的数据。如果此时网络情况不好,第一次请求超时了,那么第二次请求也没有必要发送了。当然,用户也有可能人为地取消某个NSOperation。

当某个NSOperation被取消时,我们应该尽可能的清除NSOperation内部的数据并且把cancelled和finished设为true,把executing设为false。

1
2
3
4
5
6

//取消某个NSOperation
operation1.cancel()

//取消某个NSOperationQueue剩余的NSOperation
queue.cencelAllOperations()

获取状态

  • @property(readonly, getter=isCancelled) BOOL cancelled
    • 当前任务状态是否已标记为取消
  • @property(readonly, getter=isExecuting) BOOL executing
    • 当前任务状态是否已标记为取消
  • @property(readonly, getter=isFinished) BOOL finished
    • NSOperation任务是否已结束
  • @property(readonly, getter=isConcurrent) BOOL concurrent
  • @property(readonly, getter=isAsynchronous) BOOL asynchronous
  • @property(readonly, getter=isReady) BOOL ready
    • NSOperation任务是否已结束
  • @property(copy) NSString *name

等待

  • -(void)waitUntilFinished

    1
    2
    3
    4
    5
    	
    NSBlockOperation *opB = [NSBlockOperation blockOperationWithBlock:^{
    [opA waitUntilFinished]; //opB线程等待直到opA执行结束(正常结束或被取消)
    [self operate];
    }];

设置依赖

依然考虑刚刚所说的两次网络请求的例子。因为第二次请求会用到第一次的数据,所以我们要保证发出第二次请求的时候第一个请求已经执行完。但是我们同时还希望利用到NSOperationQueue的并发特性(因为可能不止这两个任务)。

这时候我们可以设置NSOperation之间的依赖关系。语法非常简洁:

1
operation2.addDependency(operation1)

需要注意的是NSOperation之间的相互依赖会导致死锁

移除依赖:

1
- (void)removeDependency:(NSOperation *)operation

优先级

GCD中,任务(block)是没有优先级的,而队列具有优先级。和GCD相反,我们一般考虑NSOperation的优先级

NSOperation有一个NSOperationQueuePriority枚举类型的属性queuePriority

1
2
3
4
5
6
7
typedef enum : NSInteger {
NSOperationQueuePriorityVeryLow = -8,
NSOperationQueuePriorityLow = -4,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
} NSOperationQueuePriority;

如果NSInvocationOperation和NSBlockOperation对象不能满足需求, 我们可以直接继承NSOperation, 并添加额外的功能。

继承所需的工作量主要取决于你要实现非并发还是并发的NSOperation。定义同步的NSOperation要简单许多,只需要重载-main这个方法,在这个方法里面执行主任务,并正确地响应取消事件; 对于异步NSOperation, 必须重写NSOperation的多个基本方法进行实现(main、start)。

自定义非并发NSOperation

  1. 重写NSOperation的main方法,该方法会作为NSOperation所启动线程的执行体
    • 在 operation 的 main 方法里面,必须提供 autorelease pool,因为你的 operation 完成后需要销毁。
  2. 实现KVO机制,一旦你的 operation 开始了,必须通过 KVO,告诉所有的监听者,现在该operation的执行状态。(即修改isFinished,isExecuting的值)
  3. 重载 isExecuting方法和isFinished方法。在这两个方法中,必须返回一个线程安全值(通常是个BOOL值),这个值可以在 operation 中进行操作。

示例代码:

.h文件:

1
2
3
4
5
6
7
8
9
10
11

typedef void(^HYBDownloadReponse)(UIImage *image);

@interface DownloadOperation : NSOperation

@property (nonatomic, copy) NSString *url;
@property (nonatomic, copy) HYBDownloadReponse responseBlock;

- (instancetype)initWithUrl:(NSString *)url completion:(HYBDownloadReponse)completion;

@end

.m文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

#import "DownloadOperation.h"

@interface DownloadOperation () {
BOOL _isFinished;
BOOL _isExecuting;
}

@end

@implementation DownloadOperation

- (instancetype)initWithUrl:(NSString *)url completion:(HYBDownloadReponse)completion {
if (self = [super init]) {
self.url = url;
self.responseBlock = completion;
}

return self;
}

// 必须重写这个主方法
- (void)main {
// 新建一个自动释放池,如果是异步执行操作,那么将无法访问到主线程的自动释放池
@autoreleasepool {
// 提供一个变量标识,来表示我们需要执行的操作是否完成了,当然,没开始执行之前,为NO
BOOL taskIsFinished = NO;
UIImage *image = nil;

// while 保证:只有当没有执行完成和没有被取消,才执行我们自定义的相应操作
while (taskIsFinished == NO && [self isCancelled] == NO){
// 获取图片数据
NSURL *url = [NSURL URLWithString:self.url];
NSData *imageData = [NSData dataWithContentsOfURL:url];
image = [UIImage imageWithData:imageData];
NSLog(@"Current Thread = %@", [NSThread currentThread]);

// 这里,设置我们相应的操作都已经完成,后面就是要通知KVO我们的操作完成了。

taskIsFinished = YES;
}

// KVO 生成通知,告诉其他线程,该operation 执行完了
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_isFinished = YES;
_isExecuting = NO;
[self didChangeValueForKey:@"isFinished"];
[self didChangeValueForKey:@"isExecuting"];

if (self.responseBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
self.responseBlock(image);
});
}
}
}

- (void)start {
// 如果我们取消了在开始之前,我们就立即返回并生成所需的KVO通知
if ([self isCancelled]){
// 我们取消了该 operation,那么就要告诉KVO,该operation已经执行完成(isFinished)
// 这样,调用的队列(或者线程)会继续执行。
[self willChangeValueForKey:@"isFinished"];
_isFinished = NO;
[self didChangeValueForKey:@"isFinished"];
} else {
// 没有取消,那就要告诉KVO,该队列开始执行了(isExecuting)!那么,就会调用main方法,进行同步执行。
[self willChangeValueForKey:@"isExecuting"];
_isExecuting = YES;
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
[self didChangeValueForKey:@"isExecuting"];
}
}

- (BOOL)isExecuting {
return _isExecuting;
}

- (BOOL)isFinished {
return _isFinished;
}

@end

调用

queue方式:

1
2
3
4
5
6
7
8
9
10
11
12
13

DownloadOperation *operation = [[DownloadOperation alloc] initWithUrl:@"https://mmbiz.qlogo.cn/mmbiz/sia5QxFVcFD0wkCgnmf6DVxI6fVewNS8rhtZb71v2DMpDy8jIdtviaetzicwQzTEoKKyHAN96Beibk2G61tZpezQ0Q/0?wx_fmt=png" completion:^(UIImage *image) {
// self.imageView.image = image;
}];

DownloadOperation *operation1 = [[DownloadOperation alloc] initWithUrl:@"https://mmbiz.qlogo.cn/mmbiz/sia5QxFVcFD0wkCgnmf6DVxI6fVewNS8rhtZb71v2DMpDy8jIdtviaetzicwQzTEoKKyHAN96Beibk2G61tZpezQ0Q/0?wx_fmt=png" completion:^(UIImage *image) {
// self.imageView1.image = image;
}];

NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:operation];
[queue addOperation:operation1];
[queue setMaxConcurrentOperationCount:2];

手动方式:

DownloadOperation *operation = [[DownloadOperation alloc] initWithUrl:@"https://mmbiz.qlogo.cn/mmbiz/sia5QxFVcFD0wkCgnmf6DVxI6fVewNS8rhtZb71v2DMpDy8jIdtviaetzicwQzTEoKKyHAN96Beibk2G61tZpezQ0Q/0?wx_fmt=png" completion:^(UIImage *image) {
//    self.imageView.image = image;
}];
[operation start];        

自定义并发NSOperation

  1. 和同步一样需要 重写mian start
  2. 和同步一样需要 使用KVO通知状态改变
  3. 和同步一样需要 重写isExecuting isFinishing
  4. 比同步多的是 重写isAsynchronous(或者isConcurrent)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

#import "DownloadOperation.h"

@interface DownloadOperation () {
@private BOOL _isFinished;
@private BOOL _isExecuting;
}

@end

@implementation DownloadOperation

- (instancetype)initWithUrl:(NSString *)url completion:(HYBDownloadReponse)completion {
if (self = [super init]) {
self.url = url;
self.responseBlock = completion;
}

return self;
}

// 必须重写这个主方法
- (void)main {
// 新建一个自动释放池,如果是异步执行操作,那么将无法访问到主线程的自动释放池
@autoreleasepool {
UIImage *image = nil;
if (!self.isCancelled) {
// 获取图片数据
NSURL *url = [NSURL URLWithString:self.url];
NSData *imageData = [NSData dataWithContentsOfURL:url];
image = [UIImage imageWithData:imageData];
}

NSLog(@"currentThread: %@", [NSThread currentThread]);

// 被取消,也可能发生在转换的地方
if (self.isCancelled) {
image = nil;
}


if (![self isCancelled] && self.responseBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
self.responseBlock(image);
});
}
}
}

// 与自定义同步NSOperation不同的是,必须要实现下面的方法
#if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_7_0
- (BOOL)isConcurrent {
return YES;
}

#else

- (BOOL)isAsynchronous {
return YES;
}
#endif

@end

对于非主队列来说,一旦一个NSOperation被放入其中,那这个NSOperation一定是并发执行的。 NSOperationQueue会为每一个NSOperation创建线程并调用它的start方法。

KVO键值观察

  • operations - read-only
  • operationCount - read-only
  • maxConcurrentOperationCount - readable and writable
  • suspended - readable and writable
  • name - readable and writable

获取特殊NSOperationQueue

  • +(NSOperationQueue *)currentQueue
    • 返回当前NSOperationQueue,如果当前线程不是在NSOperationQueue上运行则返回nil
  • +(NSOperationQueue *)mainQueue
    • 返回主线程的NSOperationQueue

管理队列中的Operation

  • -(void)addOperation:(NSOperation *)operation
    • 加入到执行队列中,如果isReady则开始执行
  • -(void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait
    • 批量加入执行operation,wait标志是否当前线程等到所有operation结束
  • -(void)addOperationWithBlock:(void (^)(void))block
    • 相当于加入一个NSBlockOperation执行任务
  • -(void)cancelAllOperations
    • 相当于加入一个NSBlockOperation执行任务
  • -(void)waitUntilAllOperationsAreFinished
    • 当前线程等待,直到所有operation都执行结束
  • @property(readonly, copy) NSArray <__kindof NSOperation > operations
    • 返回已加入执行operation的数组,当某个operation结束后会自动从这个数组清除
  • @property(readonly) NSUInteger operationCount
    • 返回已加入执行operation的数目
  • @property NSInteger maxConcurrentOperationCount
    • 设置最大并发执行数,如果为1则同时只有一个并发任务在运行,可控制顺序执行关系
  • @property(getter=isSuspended) BOOL suspended

    1
    2
    queue.suspended = true //暂停queue中所有operation  
    queue.suspended = false //恢复queue中所有operation

简单使用:

1
2
3
4
5
6
7
8
9
10

let operationQueue = NSOperationQueue()
let operation = NSBlockOperation { () -> Void in
print(NSThread.currentThread())
}
operation.addExecutionBlock { () -> Void in
print("execution block1 -- (NSThread.currentThread())")
}
operationQueue.addOperation(operation)
print("操作结束")