14

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

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

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

我使用了几个 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

4 回答 4

16

您可以使用ReactiveCocoa轻松完成此任务。它的一大目标是使这种组合变得微不足道。

如果您以前没有听说过 ReactiveCocoa,或者不熟悉它,请查看简介 以获得快速解释。

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

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

例如,让我们创建一对对应于processand 的信号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 是一个庞大的框架,不幸的是,很难将其优势浓缩到一个小的代码示例中。我强烈建议查看何时使用 ReactiveCocoa的示例, 以了解有关它如何提供帮助的更多信息。

于 2013-09-19T17:43:09.343 回答
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
    

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

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

    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方法的工作方式的技术,在该技术中,基于 KVO 在其他操作上NSOperation设置状态。这不仅使您不仅可以在操作之间建立更复杂的依赖关系,还可以在它们之间传递数据库。这可能超出了这个问题的范围(而且我已经患有 tl:dr),但如果您需要更多信息,请告诉我。isReadyisFinished

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

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

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

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

于 2013-09-18T16:41:50.663 回答
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 中还没有属性,但它基本上实现起来非常简单)。

于 2013-09-18T22:21:13.547 回答
1

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

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

于 2016-12-13T13:39:31.570 回答