Chaining `NSOperation` : Pass result from an opera

2019-06-28 07:00发布

问题:

I've been looking for a way to pass results for chained NSOperation. For example, lets assume we have 3 operations chained:

  1. Operation1 to download JSON data from server
  2. Operation2 to parse & model JSON received
  3. Operation3 to download user images

So Op3 would be dependent on Op2, which is dependent on Op1. But I'm looking for way to pass results from Op1 -> Op2, then from Op2 -> Op3 as:

[operation1 startWithURL:url];
[operation2 parseJSONfromOp1IntoModel:JSONData];
[operation3 downloadUserImagesForUser: UserModelObject];

and nesting blocks doesn't seem to be a clean readable solution, any idea?

回答1:

If you want to chain operations, but don't like the nesting, you can use NSOperation subclasses, and then define your own completion handlers:

DownloadOperation *downloadOperation = [[DownloadOperation alloc] initWithURL:url];
ParseOperation *parseOperation = [[ParseOperation alloc] init];
DownloadImagesOperation *downloadImagesOperation = [[DownloadImagesOperation alloc] init];

downloadOperation.downloadCompletionHandler = ^(NSData *data, NSError *error) {
    if (error != nil) {
        NSLog(@"%@", error);
        return;
    }

    parseOperation.data = data;
    [queue addOperation:parseOperation];
};

parseOperation.parseCompletionHandler = ^(NSDictionary *dictionary, NSError *error) {
    if (error != nil) {
        NSLog(@"%@", error);
        return;
    }

    NSArray *images = ...;

    downloadImagesOperation.images = images;
    [queue addOperation:downloadImagesOperation];
};

[queue addOperation:downloadOperation];

Frankly, though, I'm not sure that's any more intuitive than the nested approach:

DownloadOperation *downloadOperation = [[DownloadOperation alloc] initWithURL:url downloadCompletionHandler:^(NSData *data, NSError *error) {
    if (error != nil) {
        NSLog(@"%@", error);
        return;
    }

    ParseOperation *parseOperation = [[ParseOperation alloc] initWithURL:data parseCompletionHandler:^(NSDictionary *dictionary, NSError *error) {
        if (error != nil) {
            NSLog(@"%@", error);
            return;
        }

        NSArray *images = ...

        DownloadImagesOperation *downloadImagesOperation = [[DownloadImagesOperation alloc] initWithImages:images imageDownloadCompletionHandler:^(NSError *error) {
            if (error != nil) {
                NSLog(@"%@", error);
                return;
            }

            // everything OK
        }];
        [queue addOperation:downloadImagesOperation];
    }];
    [queue addOperation:parseOperation];
}];
[queue addOperation:downloadOperation];

By the way, the above assumes that you're familiar with subclassing NSOperation, especially the subtleties of creating an asynchronous NSOperation subclass (and doing all of the necessary KVO). If you need examples of how that's done, let me know.



回答2:

Creating chained operations:

Create the Op2 from within the completion block of Op1, then use delegation or something similar to set the dependency on the newly created operation. You can use this pattern to chain as many as you want. To pass the result in the completion block, you cannot use completionBlock that is on NSOperation. You will need to define your own (like I did with almostFinished) in order to pass the result through.

- (void)someMethod {
    Operation1 *operation1 = [[Operation1 alloc] init];
    operation1.almostFinished = ^(id op1Result) {

        Operation2 *operation2 = [[Operation2 alloc] initWithResultFromOp1: op1Result];
        operation2.almostFinished = ^(id op2Result) {

            Operation3 *operation3 = [[Operation3 alloc] initWithResultFromOp2:op2Result];
            operation3.completionBlock = ^{
                NSLog(@"Operations 1 and 2 waited on me, but now we're all finished!!!);
            };

            [operation2 addDependency:operation3];
            [queue addOperation:operation3];
        };

        [operation1 addDependency:operation2];
        [queue addOperation:operation2];
    };

    [queue addOperation:operation1];
}

Custom Subclass

You will need to subclass NSOperation for this to work. As I mentioned, you need to define your own completion block AND make sure that completion block is called before the operation is truly finished so that you can add the dependency. Instead of adding the dependency in the new completion block, you could add it in a different block or delegate method. This way kept my example concise.

@interface Operation: NSOperation {
@property (nonatomic, copy) void (^almostFinished)(id result);
@end

@implementation Operation {
    //...

- (void)main {
    //...
    // Call here to allow to add dependencies and new ops
    self.almostFinished(result);  

    // Finish the op
    [self willChangeValueForKey:@"isFinished"];
    // repeat for isExecuting and do whatever else
    [self didChangeValueForKey:@"isFinished"];
}
@end

EDIT: This isn't the most readable thing, but it contains all the code in one method. If you want to get fancy, then place things out in delegate methods or get creative with how you define these things.