简单实现一个并发的NSOperation

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,我看了下最新的源码已修改并做了特殊注释


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容