【问题标题】:Managing a bunch of NSOperation with dependencies管理一堆具有依赖关系的 NSOperation
【发布时间】:2013-09-23 11:40:24
【问题描述】:

我正在开发一个创建内容并将其发送到现有后端的应用程序。内容是标题、图片和位置。没什么特别的。

后端有点复杂,所以这是我必须做的:

  • 让用户拍照,输入标题并授权地图使用其位置
  • 为帖子生成唯一标识符
  • 在后端创建帖子
  • 上传图片
  • 刷新用户界面

我使用了几个 NSOperation 子类来完成这项工作,但我并不为我的代码感到自豪,这里有一个示例。

NSOperation *process = [NSBlockOperation blockOperationWithBlock:^{
    // Process image before upload
}];

NSOperation *filename = [[NSInvocationOperation alloc] initWithTarget: self selector: @selector(generateFilename) object: nil];

NSOperation *generateEntry = [[NSInvocationOperation alloc] initWithTarget: self selector: @selector(createEntry) object: nil];

NSOperation *uploadImage = [[NSInvocationOperation alloc] initWithTarget: self selector: @selector(uploadImageToCreatedEntry) object: nil];

NSOperation *refresh = [NSBlockOperation blockOperationWithBlock:^{
    // Update UI
    [SVProgressHUD showSuccessWithStatus: NSLocalizedString(@"Success!", @"Success HUD message")];
}];

[refresh addDependency: uploadImage];

[uploadImage addDependency: generateEntry];
[generateEntry addDependency: filename];
[generateEntry addDependency: process];

[[NSOperationQueue mainQueue] addOperation: refresh];
[_queue addOperations: @[uploadImage, generateEntry, filename, process] waitUntilFinished: NO];

以下是我不喜欢的东西:

  • 在我的 createEntry 中:例如,我将生成的文件名存储在一个属性中,该属性符合我的类的全局范围
  • 在 uploadImageToCreatedEntry: 方法中,我使用 dispatch_async + dispatch_get_main_queue() 来更新我的 HUD 中的消息

您将如何管理这样的工作流程?我想避免嵌入多个完成块,我觉得 NSOperation 确实是要走的路,但我也觉得在某个地方有更好的实现。

谢谢!

【问题讨论】:

    标签: ios objective-c nsoperation nsoperationqueue


    【解决方案1】:

    您可以使用ReactiveCocoa 很容易做到这一点。它的一大目标是制作这种 组成微不足道。

    如果您之前没有听说过 ReactiveCocoa,或者不熟悉它,请查看 出Introduction 快速解释。

    我将避免在这里重复整个框架概述,但我只想说 RAC 实际上提供了一个承诺/未来的超集。它允许您撰写和 转换完全不同来源的事件(UI、网络、数据库、KVO、 通知等),非常强大。

    要开始 RAC 化此代码,我们可以做的第一件事也是最简单的事情是 将这些单独的操作放入方法中,并确保每一个都返回 RACSignal。这不是绝对必要的(它们都可以在 一个作用域),但它使代码更具模块化和可读性。

    例如,让我们创建一对对应于process 的信号和 generateFilename:

    - (RACSignal *)processImage:(UIImage *)image {
        return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
            // Process image before upload
    
            UIImage *processedImage = …;
            [subscriber sendNext:processedImage];
            [subscriber sendCompleted];
        }];
    }
    
    - (RACSignal *)generateFilename {
        return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
            NSString *filename = [self generateFilename];
            [subscriber sendNext:filename];
            [subscriber sendCompleted];
        }];
    }
    

    其他操作(createEntryuploadImageToCreatedEntry)将非常相似。

    一旦我们有了这些,就很容易组合它们并表达它们 依赖关系(尽管 cmets 让它看起来有点密集):

    [[[[[[self
        generateFilename]
        flattenMap:^(NSString *filename) {
            // Returns a signal representing the entry creation.
            // We assume that this will eventually send an `Entry` object.
            return [self createEntryWithFilename:filename];
        }]
        // Combine the value with that returned by `-processImage:`.
        zipWith:[self processImage:startingImage]]
        flattenMap:^(RACTuple *entryAndImage) {
            // Here, we unpack the zipped values then return a single object,
            // which is just a signal representing the upload.
            return [self uploadImage:entryAndImage[1] toCreatedEntry:entryAndImage[0]];
        }]
        // Make sure that the next code runs on the main thread.
        deliverOn:RACScheduler.mainThreadScheduler]
        subscribeError:^(NSError *error) {
            // Any errors will trickle down into this block, where we can
            // display them.
            [self presentError:error];
        } completed:^{
            // Update UI
            [SVProgressHUD showSuccessWithStatus: NSLocalizedString(@"Success!", @"Success HUD message")];
        }];
    

    请注意,我重命名了您的一些方法,以便它们可以接受来自 它们的依赖关系,为我们提供了一种更自然的方式来从一个提供值 操作到下一个。

    这里有很大的优势:

    • 您可以从上到下阅读,因此很容易理解 事情发生在哪里,以及依赖关系在哪里。
    • 在不同线程之间移动工作非常容易,如下所示 使用-deliverOn:
    • 这些方法中任何发送的任何错误都将自动取消所有 剩下的工作,最终到达subscribeError:块很容易 处理。
    • 您还可以将其与其他事件流(即,不只是 操作)。例如,您可以将其设置为仅在 UI 信号(如按钮点击)触发。

    ReactiveCocoa 是一个庞大的框架,不幸的是,它很难提炼出 优势归结为一个小代码示例。我强烈建议您查看 when to use ReactiveCocoa 的示例 了解有关它如何提供帮助的更多信息。

    【讨论】:

    • RAC 给我留下了深刻的印象。实际上,我想要它。然而,它相当复杂。文件数量巨大(大约 80 个源模块)。另一方面,Promise 库只是一个类和一个源模块。您的 RAC 示例解决方案的巨大优势也适用于 Promise 解决方案。尽管它的 API 极简,Promises 却异常强大。不过,我不认为 OPs 问题足以给 RAC 带来压力。它也完全在 Promises 可以做的“范围内”。这可能是这类问题的 95%。
    • @CouchDeveloper 问题是期货不与其他任何东西组成。假设您将它们用于后台操作——然后呢?当您需要来自 KVO、NSNotificationCenter 或 UI 的信息时会发生什么?我不会否认 ReactiveCocoa 很大(事实上,我在回答中直言不讳),但大小与所有这些乍一看似乎非常不同的模式的统一有关,所以我绝对认为这是值得的。
    • 嗨@JustinSpahr-Summers,您能帮我解释一下startEagerlyWithSchedulerdeliverOn: 消息之间的区别吗?
    • @TruongThanhDung +startEagerlyWithScheduler:block: 用于当您想要立即开始执行某些操作时(即,一旦调用该方法)。 -deliverOn: 只是转换一个现有信号,这样它的所有事件回调都发生在不同的线程上。
    • @febeling 这将去任何 -addOperation: 代码(如 OP 中所示)将去的地方。 startingImage 是为了演示而编造的。
    【解决方案2】:

    一些想法:

    1. 我倾向于利用完成块,因为您可能只想在前一个操作成功的情况下启动下一个操作。您要确保正确处理错误,并在失败时轻松脱离操作链。

    2. 如果我想将数据从操作传递到另一个操作并且不想使用调用者类的某些属性,我可能会将自己的完成块定义为我的自定义操作的属性,该操作具有包含该字段的参数我想从一个操作传递到另一个操作。不过,这假设您正在进行NSOperation 子类化。

      例如,我可能有一个FilenameOperation.h,它为我的操作子类定义了一个接口:

      #import <Foundation/Foundation.h>
      
      typedef void (^FilenameOperationSuccessFailureBlock)(NSString *filename, NSError *error);
      
      @interface FilenameOperation : NSOperation
      
      @property (nonatomic, copy) FilenameOperationSuccessFailureBlock successFailureBlock;
      
      @end
      

      如果不是并发操作,实现可能如下所示:

      #import "FilenameOperation.h"
      
      @implementation FilenameOperation
      
      - (void)main
      {
          if (self.isCancelled)
              return;
      
          NSString *filename = ...;
          BOOL failure = ...
      
          if (failure)
          {
              NSError *error = [NSError errorWithDomain:... code:... userInfo:...];
              if (self.successFailureBlock)
                  self.successFailureBlock(nil, error);                                                    
          }
          else
          {
              if (self.successFailureBlock)
                  self.successFailureBlock(filename, nil);
          }
      }
      
      @end
      

      显然,如果您有一个并发操作,您将实现所有标准的isConcurrentisFinishedisExecuting 逻辑,但想法是相同的。顺便说一句,有时人们会将这些成功或失败发送回主队列,因此您也可以根据需要这样做。

      无论如何,这说明了使用我自己的完成块传递适当数据的自定义属性的想法。您可以对每种相关类型的操作重复此过程,然后可以将它们全部链接在一起,例如:

      FilenameOperation *filenameOperation = [[FilenameOperation alloc] init];
      GenerateOperation *generateOperation = [[GenerateOperation alloc] init];
      UploadOperation   *uploadOperation   = [[UploadOperation alloc] init];
      
      filenameOperation.successFailureBlock = ^(NSString *filename, NSError *error) {
          if (error)
          {
              // handle error
              NSLog(@"%s: error: %@", __FUNCTION__, error);
          }
          else
          {
              generateOperation.filename = filename;
              [queue addOperation:generateOperation];
          }
      };
      
      generateOperation.successFailureBlock = ^(NSString *filename, NSData *data, NSError *error) {
          if (error)
          {
              // handle error
              NSLog(@"%s: error: %@", __FUNCTION__, error);
          }
          else
          {
              uploadOperation.filename = filename;
              uploadOperation.data     = data;
              [queue addOperation:uploadOperation];
          }
      };
      
      uploadOperation.successFailureBlock = ^(NSString *result, NSError *error) {
          if (error)
          {
              // handle error
              NSLog(@"%s: error: %@", __FUNCTION__, error);
          }
          else
          {
              [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                  // update UI here
                  NSLog(@"%@", result);
              }];
          }
      };
      
      [queue addOperation:filenameOperation];
      
    3. 在更复杂的场景中的另一种方法是让您的 NSOperation 子类采用类似于标准 addDependency 方法的工作方式的技术,其中 NSOperation 基于 @ 上的 KVO 设置 isReady 状态987654337@ 其他操作。这不仅使您不仅可以在操作之间建立更复杂的依赖关系,还可以在它们之间传递数据库。这可能超出了这个问题的范围(我已经患上了 tl:dr),但如果您需要更多信息,请告诉我。

    4. 我不会太担心uploadImageToCreatedEntry 正在分派回主线程。在复杂的设计中,您可能有各种不同的队列专门用于特定类型的操作,而将 UI 更新添加到主队列这一事实与这种模式完全一致。但我可能倾向于使用NSOperationQueue 等价物,而不是dispatch_async

      [[NSOperationQueue mainQueue] addOperationWithBlock:^{
          // do my UI update here
      }];
      
    5. 我想知道您是否需要所有这些操作。例如,我很难想象filename 足够复杂以证明它自己的操作是合理的(但如果您从某个远程源获取文件名,那么单独的操作非常有意义)。我会假设您正在做一些足够复杂的事情来证明它是合理的,但是这些操作的名称让我感到疑惑。

    6. 如果你愿意,你可能想看看 couchdeveloper 的 RXPromise 类,它使用 promises 来(a)控制单独操作之间的逻辑关系; (b) 简化数据从一个到另一个的传递。 Mike Ash 有一个旧的 MAFuture 类,它做同样的事情。

      我不确定其中任何一个是否成熟到可以考虑在我自己的代码中使用它们,但这是一个有趣的想法。

    【讨论】:

    • 我想我真的很喜欢方法#2,即使它感觉像是自制的依赖管理,我不确定我是否对此感到满意。它看起来确实更漂亮,所以谢谢你。关于#3,我不介意更多信息,即使我的工作流程并不复杂。我也许可以减少操作次数并修复我的方法名称,它们确实令人困惑。非常感谢您的详细回答!
    • @Palleas 请参阅this answer 以获取第三种方法的示例。但是您没有任何迫切需要做任何复杂的事情(在这种情况下我们需要它,因为我们希望在操作的完成块中触发下一个操作而不是操作本身时手动触发)。
    【解决方案3】:

    我可能完全有偏见 - 但出于特殊原因 - 我喜欢 @Rob 的方法 #6 ;)

    假设您为异步方法和操作创建了适当的包装器,这些包装器返回 Promise 而不是使用完成块发出完成信号,解决方案如下所示:

    RXPromise* finalResult = [RXPromise all:@[[self filename], [self process]]]
    .then(^id(id filenameAndProcessResult){
        return [self generateEntry];
    }, nil)
    .then(^id(id generateEntryResult){
        return [self uploadImage];
    }, nil)
    .thenOn(dispatch_get_main_queue() , ^id(id uploadImageResult){
        [self refreshWithResult:uploadImageResult];
        return nil;
    }, nil)
    .then(nil, ^id(NSError*error){
        // Something went wrong in any of the operations. Log the error:
        NSLog(@"Error: %@", error);
    });
    

    而且,如果您想在任何时间、任何地点、无论执行多远都取消整个异步序列:

    [finalResult.root cancel];
    

    (一个小提示:属性root在当前版本的RXPromise中尚不可用,但它的实现基本上非常简单)。

    【讨论】:

      【解决方案4】:

      如果还想使用 NSOperation,可以依赖ProcedureKit,使用Procedure类的注入属性。

      对于每个操作,指定它生成的类型并将其注入下一个依赖操作。最后,您还可以将整个过程封装在 GroupProcedure 类中。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-06-06
        • 1970-01-01
        • 2019-09-03
        • 2015-10-17
        • 1970-01-01
        • 2014-08-19
        • 2013-11-05
        • 1970-01-01
        相关资源
        最近更新 更多