NSOperation、NSOperationQueue
An operation object is a single-shot object—that is, it executes its task once and cannot be used to execute it again. You typically execute operations by adding them to an operation queue (an instance of the NSOperationQueue class). An operation queue executes its operations either directly, by running them on secondary threads, or indirectly using the libdispatch library (also known as Grand Central Dispatch). For more information about how queues execute operations, see NSOperationQueue.
If you do not want to use an operation queue, you can execute an operation yourself by calling its start method directly from your code. Executing operations manually does put more of a burden on your code, because starting an operation that is not in the ready state triggers an exception. The ready property reports on the operation’s readiness
我们来看下GNUstep Base相关源码猜测下它的内部关键实现
addOperation方法
- (void) addOperation: (NSOperation *)op
{
if (op == nil || NO == [op isKindOfClass: [NSOperation class]])
{
[NSException raise: NSInvalidArgumentException
format: @"[%@-%@] object is not an NSOperation",
NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
}
[internal->lock lock];
if (NSNotFound == [internal->operations indexOfObjectIdenticalTo: op]
&& NO == [op isFinished])
{
[op addObserver: self
forKeyPath: @"isReady"
options: NSKeyValueObservingOptionNew
context: NULL];
[self willChangeValueForKey: @"operations"];
[self willChangeValueForKey: @"operationCount"];
[internal->operations addObject: op];
[self didChangeValueForKey: @"operationCount"];
[self didChangeValueForKey: @"operations"];
if (YES == [op isReady])
{
[self observeValueForKeyPath: @"isReady"
ofObject: op
change: nil
context: nil];
}
}
[internal->lock unlock];
}
- (void) observeValueForKeyPath: (NSString *)keyPath
ofObject: (id)object
change: (NSDictionary *)change
context: (void *)context
{
[internal->lock lock];
if (YES == [object isFinished])
{
internal->executing--;
[object removeObserver: self
forKeyPath: @"isFinished"];
[internal->lock unlock];
[self willChangeValueForKey: @"operations"];
[self willChangeValueForKey: @"operationCount"];
[internal->lock lock];
[internal->operations removeObjectIdenticalTo: object];
[internal->lock unlock];
[self didChangeValueForKey: @"operationCount"];
[self didChangeValueForKey: @"operations"];
}
else if (YES == [object isReady])
{
[object removeObserver: self
forKeyPath: @"isReady"];
[internal->waiting addObject: object];
[internal->lock unlock];
}
[self _execute];
}
- (void) _execute
{
//do something。。。
if (YES == [op isConcurrent])
{
[op start];
}
else
{
NSUInteger pending;
[internal->cond lock];
pending = [internal->starting count];
[internal->starting addObject: op];
/* Create a new thread if all existing threads are busy and
* we haven't reached the pool limit.
*/
if (0 == internal->threadCount
|| (pending > 0 && internal->threadCount < POOL))
{
internal->threadCount++;
[NSThread detachNewThreadSelector: @selector(_thread)
toTarget: self
withObject: nil];
}
/* Tell the thread pool that there is an operation to start.
*/
[internal->cond unlockWithCondition: 1];
}
}
- (void) start
{
//do something。。。
if (NO == [self isCancelled])
{
[NSThread setThreadPriority: internal->threadPriority];
[self main];
}
[self _finish];
//do something
}
再看下苹果的官方文档NSOperation一些关键属性基于KVC\KVO
The NSOperation class is key-value coding (KVC) and key-value observing (KVO) compliant for several of its properties. As needed, you can observe these properties to control other parts of your application. To observe the properties, use the following key paths:
isCancelled - read-only
isAsynchronous - read-only
isExecuting - read-only
isFinished - read-only
isReady - read-only
dependencies - read-only
queuePriority - readable and writable
completionBlock - readable and writable
上面我们可以看到添加一个未完成的NSOperation,其实就是将NSOperation添加到一个动态数组当中,然后通过手动通知各个关键属性的变化最后执行_execute方法,_execute就是一个执行队列,依次将等待队列里面的所有operation进行start,可以看出非并非的时候会默认单独开启一个线程去执行这些operations--[op start],start方法会去执行我们所谓的main方法。
通过上面GNUstep源码可以大概猜测一部分OC相关实际实现,结合现有官方文档注释即可更好理解。
- NSOperation和NSOperationQueue的组合操作是基于GCD之上的更高一层封装,基于开头苹果文档所提NSOperationQueue执行这些operations在隔离的线程或者依靠GCD来实现
- 内部基于KVO监测isExecuted, isFinished, isCancelled等属性变化来动态执行operations
- 默认是非并发的,是抽象类
其它一些常见的就不说了,想必大家都用的已经很熟悉了
实现一个并发NSOperation
NSOperation 默认是非并发,也就是说所有的动态量由系统内部控制,在你的main方法结束也就意味着operation的结束;这样的话我们会经常有这样的需求,最常见的图片下载,需要通过NSOperation来实现,如果在main方法开启异步下载的话,那么main方法返回后就代表operation的结束,你的下载相关委托可能不会回调或者回调之前operation已经finished;所以我们来设计一个我们自己可控的并发的operation。
要想实现可控的并发的operation,那么我们就首先告诉系统,此operation需要并发,那么首先重写
- (BOOL)isConcurrent {
return YES;//告诉系统要并发,系统就把必要的isExecuted, isFinished, isCancelled等交给你控制
}
然后重写:
start()函数.
isExecuting和isFinished函数
具体看实现demo
创建两个NSOperation,TestOperation和TestOperationTwo
TestOperation.m如下:
@interface TestOperation () <NSURLSessionTaskDelegate>
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
@end
@implementation TestOperation
@synthesize executing = _executing;
@synthesize finished = _finished;
- (void)start{//
if (self.isCancelled) {
self.finished = YES;
return;
}
NSLog(@"TestOperation executing");
self.executing = YES;
NSString *imgurl = @"http://img3.baozhenart.com/images/201602/source_img/20205_P_1454380927950.jpg";
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:imgurl] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15];
request.HTTPShouldHandleCookies = YES;
request.HTTPShouldUsePipelining = YES;
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:nil
delegateQueue:nil];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"TestOperation finished...");
self.finished = YES;
self.executing = NO;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"DOWN_RESULT" object:data];
});
}];
[dataTask resume];
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (BOOL)isConcurrent {
return YES;
}
TestOperationTwo.m如下唯一不同的是start方法实现:
- (void)start{//
if (self.isCancelled) {
self.finished = YES;
return;
}
NSLog(@"TestOperationTwo executing");
self.executing = YES;
NSString *imgurl = @"http://img2.baozhenart.com/images/201703/source_img/54866_G_1489463826422376203.JPG";
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:imgurl] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15];
request.HTTPShouldHandleCookies = YES;
request.HTTPShouldUsePipelining = YES;
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:nil
delegateQueue:nil];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"TestOperationTwo finished...");
NSLog(@"TestOperationTwo sleep 4s...");
sleep(4);
self.finished = YES;
self.executing = NO;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"DOWN_RESULT1" object:data];
});
}];
[dataTask resume];
}
以上是创建两个operation,对应相应的下载任务,start方法里面很清晰的看到,我们告诉系统operation何时开始何时结束,由我们自己决定。
我们设置下op1作为op的依赖,op1执行完开始执行op
TestOperation *op = [[TestOperation alloc] init];
TestOperationTwo *op1 = [[TestOperationTwo alloc] init];
NSOperationQueue *que = NSOperationQueue.new;
[op addDependency:op1];
[que addOperation:op];
[que addOperation:op1];
执行结果很明显看出op1的异步下载任务结束之后开始执行op
2017-03-25 17:44:02.609 OperationQueueTT[73601:5333312] TestOperationTwo executing
2017-03-25 17:44:02.966 OperationQueueTT[73601:5333311] TestOperationTwo finished...
2017-03-25 17:44:02.966 OperationQueueTT[73601:5333311] TestOperationTwo sleep 4s...
2017-03-25 17:44:06.968 OperationQueueTT[73601:5333332] TestOperation executing
2017-03-25 17:44:07.628 OperationQueueTT[73601:5333311] TestOperation finished...
以上测试Demo地址
文末对SDWebImage中SDWebImageDownloader里面一段代码持有疑问
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
wself.lastAddedOperation的isExecuting已经是YES了,这样写貌似不起作用,暂时已经给作者提issue了,有好奇的一起研究下,也许是我理解不到位,欢迎指教,写这篇文章算是自我识记和分享给同样研究用到的,有误多多指出。
2022.1.4更新
关于文末提的给SDWebImage提的issue,我看了下最新的源码已修改并做了特殊注释