管理一堆具有依赖关系的NSOperation

时间:2013-09-18 15:50:57

标签: ios objective-c nsoperation nsoperationqueue

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

后端有点复杂,所以这就是我要做的事情:

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

我已经使用了几个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真的是要走的路,但我也觉得在某个地方有更好的实现。

谢谢!

4 个答案:

答案 0 :(得分:16)

您可以使用ReactiveCocoa 很容易实现这一点。其中一个重要目标就是实现这一目标 构成微不足道。

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

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

要开始使用此代码,我们可以做的第一件也是最简单的事情 将这些单独的操作转换为方法,并确保每个返回 一个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)非常相似。

一旦我们有了这些,就很容易将它们组合起来并表达它们 依赖(虽然注释使它看起来有点密集):

[[[[[[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的示例 了解更多有关它如何提供帮助的信息。

答案 1 :(得分:7)

有几点想法:

  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设置{{1}在另一个操作上基于isReady的KVO状态。这不仅允许您在操作之间建立更复杂的依赖关系,还允许您在它们之间传递数据库。这可能超出了这个问题的范围(我已经受到了tl:dr的影响),但是如果你需要更多的话,请告诉我。

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

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

  6. 如果您愿意,您可能需要查看couchdeveloper的RXPromise类,该类使用promises来控制单独操作之间的逻辑关系; (b)简化从一个到另一个的数据传递。迈克阿什有一个旧的MAFuture课,它做同样的事情。

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

答案 2 :(得分: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中提供,但它的实现基本上非常简单。)

答案 3 :(得分:1)

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

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