何时将在NSOperation中为completionBlock调用依赖项

Nor*_*sen 6 objective-c nsoperationqueue objective-c-blocks restkit afnetworking

来自文档:

当isFinished方法返回的值更改为YES时,将执行您提供的完成块.因此,在操作的主要任务完成或取消之后,操作对象执行该块.

我正在使用RestKit/AFNetworking,如果重要的话.

NSOperation在a中有多个依赖项OperationQueue.我使用完成块来设置我的孩子需要的一些变量(将结果附加到数组).

(task1,...,taskN) - > taskA

taskA addDependency:task1-taskN

taskA是否会收到不完整的数据,因为孩子可以在完成块被触发之前执行?

参考

NSOperations及其completionBlocks同时运行吗?

我通过在完成块中添加一个睡眠进行了简单的测试,结果不一样.完成块在主线程中运行.当所有完成块都处于休眠状态时,子任务就会运行.

Rob*_*Rob 5

正如我在下面的"一些观察"中所讨论的那样,您无法保证在您的其他各种AFNetworking完成块完成之前,此最终相关操作不会启动.令我感到震惊的是,如果这个最终操作真的需要等待这些完成块完成,那么你有几个选择:

  1. 在每个的使用旗语ň当他们完成竣工块信号,并具有完成操作等待ñ信号; 要么

  2. 不要预先排队这个最终操作,而是让你的个人上传的完成块跟踪仍有多少未完成的上传,以及当它下降到零时,然后启动最后的"post"操作.

  3. 正如您在注释中指出的那样,您可以在自己的操作中包装AFNetworking操作及其完成处理程序的调用,然后您可以使用标准addDependency机制.

  4. 您可以放弃该addDependency方法(isFinished在此操作所依赖的操作的键上添加观察者,并且一旦解决了所有这些依赖关系,就执行isReadyKVN;问题是理论上这可能完成块完成之前发生)并用你自己的isReady逻辑替换它.例如,假设您有一个post操作,您可以添加自己的密钥依赖项并在完成块中手动删除它们,而不是自动删除它们isFinished.这样,你自定义操作

    @interface PostOperation ()
    @property (nonatomic, getter = isReady) BOOL ready;
    @property (nonatomic, strong) NSMutableArray *keys;
    @end
    
    @implementation PostOperation
    
    @synthesize ready = _ready;
    
    - (void)addKeyDependency:(id)key {
        if (!self.keys)
            self.keys = [NSMutableArray arrayWithObject:key];
        else
            [self.keys addObject:key];
    
        self.ready = NO;
    }
    
    - (void)removeKeyDependency:(id)key {
        [self.keys removeObject:key];
    
        if ([self.keys count] == 0)
            self.ready = YES;
    }
    
    - (void)setReady:(BOOL)ready {
        if (ready != _ready) {
            [self willChangeValueForKey:@"isReady"];
            _ready = ready;
            [self didChangeValueForKey:@"isReady"];
        }
    }
    
    - (void)addDependency:(NSOperation *)operation{
        NSAssert(FALSE, @"You should not use addDependency with this custom operation");
    }
    
    Run Code Online (Sandbox Code Playgroud)

    然后,您的应用程序代码可以执行类似,使用addKeyDependency而不是addDependency明确地removeKeyDependencycancel在完成块中:

    PostOperation *postOperation = [[PostOperation alloc] init];
    
    for (NSInteger i = 0; i < numberOfImages; i++) {
        NSURL *url = ...
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        NSString *key = [url absoluteString]; // or you could use whatever unique value you want
    
        AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
        [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
            // update your model or do whatever
    
            // now inform the post operation that this operation is done
    
            [postOperation removeKeyDependency:key];
        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            // handle the error any way you want
    
            // perhaps you want to cancel the postOperation; you'd either cancel it or remove the dependency
    
            [postOperation cancel];
        }];
        [postOperation addKeyDependency:key];
        [queue addOperation:operation];
    }
    
    [queue addOperation:postOperation];
    
    Run Code Online (Sandbox Code Playgroud)

    这是使用AFHTTPRequestOperation,你显然用适当的AFNetworking操作替换所有这些逻辑,但希望它说明了这个想法.


原始答案:

一些观察:

  1. 我认为你的结论是,当你的操作完成时,它(a)启动它的完成块; (b)使队列可用于其他操作(maxConcurrentOperationCount由于操作之间或因操作之间的依赖性而导致尚未启动的操作).我不相信您确信在下一次操作开始之前完成块将完成.

    根据经验,看起来依赖操作直到完成块完成后才会实际触发,但是(a)我没有看到任何记录在任何地方和(b)这没有实际意义,因为如果你使用AFNetworking自己的setCompletionBlockWithSuccess,它结束up将块异步调度到主队列(或定义的successCallbackQueue),从而阻止任何(未记录的)同步保证.

  2. 此外,您说完成块在主线程中运行.如果你在谈论内置NSOperation完成块,你没有这样的保证.事实上,setCompletionBlock 文档说:

    您的完成块的确切执行上下文无法保证,但通常是辅助线程.因此,您不应该使用此块来执行任何需要非常特定的执行上下文的工作.相反,您应该将该工作分流到应用程序的主线程或能够执行此操作的特定线程.例如,如果您有一个用于协调操作完成的自定义线程,则可以使用完成块来ping该线程.

    但是,如果你在谈论AFNetworking的定制完成区块之一,例如那些你可能会设置AFHTTPRequestOperationsetCompletionBlockWithSuccess,那么,是的,这是真的,那些通常被派遣回主队列.但AFNetworking使用标准completionBlock机制来做到这一点,因此上述问题仍然适用.


Cou*_*per 2

如果您NSOperation是 AFHTTPRequestOperation 的子类,这很重要。AFHTTPRequestOperation在方法中将NSOperations 属性用于completionBlock其自身目的setCompletionBlockWithSuccess:failure。在这种情况下,请勿completionBlock自行设置该属性!

看来,AFHTTPRequestOperation 的成功和失败处理程序将在主线程上运行。

否则, 的完成块的执行上下文NSOperation是“未定义的”。这意味着完成块可以在任何线程/队列上执行。事实上它在一些私有队列上执行。

IMO,这是首选方法,除非调用站点应明确指定执行上下文。在可访问实例的线程或队列(例如主线程)上执行完成处理程序很容易导致粗心的开发人员死锁。


编辑:

如果您想在父操作的完成块完成后启动依赖操作,可以通过将完成块内容本身设置为NSBlockOperation(新的父操作)并将此操作作为依赖项添加到子操作并启动来解决此问题它在队列中。不过,您可能会意识到,这很快就会变得笨拙。

另一种方法需要一个实用程序类或类库,它特别适合以更简洁和简单的方式解决异步问题。ReactiveCocoa将能够解决这样的(简单的)问题。然而,它过于复杂,而且实际上有一个“学习曲线”——而且是陡峭的。我不会推荐它,除非您同意花几周的时间来学习它并且有很多其他异步用例甚至更复杂的用例。

一种更简单的方法是利用“Promises”,这在 JavaScript、Python、Scala 和其他一些语言中非常常见。

现在,请仔细阅读,(简单)解决方案实际上如下:

“Promises”(有时称为 Futures 或 Deferred)代表异步任务的最终结果。您的获取请求就是这样的异步任务。但异步方法/任务返回一个 Promise,而不是指定完成处理程序:

-(Promise*) fetchThingsWithURL:(NSURL*)url;
Run Code Online (Sandbox Code Playgroud)

您可以通过注册成功处理程序块或失败处理程序块来获取结果或错误,如下所示:

Promise* thingsPromise = [self fetchThingsWithURL:url];
thingsPromise.then(successHandlerBlock, failureHandlerBlock);
Run Code Online (Sandbox Code Playgroud)

或者,内联块:

thingsPromise.then(^id(id things){
   // do something with things
   return <result of success handler>
}, ^id(NSError* error){
   // Ohps, error occurred
   return <result of failure handler>
});
Run Code Online (Sandbox Code Playgroud)

更短:

[self fetchThingsWithURL:url]
.then(^id(id result){
     return [self.parser parseAsync:result];
}, nil);
Run Code Online (Sandbox Code Playgroud)

这里,parseAsync:是一个返回 Promise 的异步方法。(是的,一个承诺)。


您可能想知道如何从解析器获取结果?

[self fetchThingsWithURL:url]
.then(^id(id result){
     return [self.parser parseAsync:result];
}, nil)
.then(^id(id parserResult){
    NSLog(@"Parser returned: %@", parserResult);
    return nil;  // result not used
}, nil);
Run Code Online (Sandbox Code Playgroud)

这实际上启动了异步任务fetchThingsWithURL:。然后,当成功完成时,它启动异步任务parseAsync:。然后,当成功完成时,它会打印结果,否则会打印错误。

依次调用多个异步任务,一个接一个,称为“连续”或“链接”。

请注意,上面的整个语句是异步的!也就是说,当你将上面的语句包装到一个方法中并执行它时,该方法立即返回。


您可能想知道如何捕获任何错误,例如fetchThingsWithURL:失败,或者parseAsync:

[self fetchThingsWithURL:url]
.then(^id(id result){
     return [self.parser parseAsync:result];
}, nil)
.then(^id(id parserResult){
    NSLog(@"Parser returned: %@", parserResult);
    return nil;  // result not used
}, nil)
.then(/*succes handler ignored*/, ^id (NSError* error){
    // catch any error
    NSLog(@"ERROR: %@", error);
    return nil; // result not used
});
Run Code Online (Sandbox Code Playgroud)

处理程序在相应的任务完成后执行(当然)。如果任务成功,将调用成功处理程序(如果有)。如果任务失败,将调用错误处理程序(如果有)。

处理程序可以返回 Promise(或任何其他对象)。例如,如果异步任务成功完成,则将调用其成功处理程序,该处理程序将启动另一个异步任务,并返回承诺。当这件事完成后,又可以开始另一件事,如此强制。这就是“继续”;)


您可以从处理程序返回任何内容:

Promise* finalResult = [self fetchThingsWithURL:url]
.then(^id(id result){
     return [self.parser parseAsync:result];
}, nil)
.then(^id(id parserResult){
    return @"OK";
}, ^id(NSError* error){
    return error;
});
Run Code Online (Sandbox Code Playgroud)

现在,finalResult最终将变成值@"OK" 或 NSError。


您可以将最终结果保存到数组中:

array = @[
    [self task1],
    [self task2],
    [self task3]
];
Run Code Online (Sandbox Code Playgroud)

然后在所有任务成功完成后继续:

[Promise all:array].then(^id(results){
    ...
}, ^id (NSError* error){
    ...
});
Run Code Online (Sandbox Code Playgroud)

设置承诺的值将被称为:“解决”。您只能解决一次承诺。

您可以将带有完成处理程序或完成委托的任何异步方法包装到返回 Promise 的方法中:

- (Promise*) fetchUserWithURL:(NSURL*)url 
{
    Promise* promise = [Promise new];

    HTTPOperation* op = [[HTTPOperation alloc] initWithRequest:request 
        success:^(NSData* data){
            [promise fulfillWithValue:data];
        } 
        failure:^(NSError* error){
            [promise rejectWithReason:error];
        }];

    [op start];

    return promise;
}
Run Code Online (Sandbox Code Playgroud)

任务完成后,可以通过传递结果值“履行”承诺,也可以通过传递原因(错误)“拒绝”承诺。

根据实际的实现,Promise 也可以被取消。假设您持有对请求操作的引用:

self.fetchUserPromise = [self fetchUsersWithURL:url];
Run Code Online (Sandbox Code Playgroud)

您可以按如下方式取消异步任务:

- (void) viewWillDisappear:(BOOL)animate {
    [super viewWillDisappear:animate];
    [self.fetchUserPromise cancel];
    self.fetchUserPromise = nil;
}
Run Code Online (Sandbox Code Playgroud)

为了取消关联的异步任务,请在包装器中注册一个失败处理程序:

- (Promise*) fetchUserWithURL:(NSURL*)url 
{
    Promise* promise = [Promise new];

    HTTPOperation* op = ... 
    [op start];

    promise.then(nil, ^id(NSError* error){
        if (promise.isCancelled) {
            [op cancel];
        }
        return nil; // result unused
    });

    return promise;
}
Run Code Online (Sandbox Code Playgroud)

注意:您可以根据需要注册成功或失败处理程序,时间、地点和数量。


因此,您可以使用 Promise 做很多事情 - 甚至比这个简短的介绍还要多。如果您读到这里,您可能会了解如何解决您的实际问题。它就在那里——而且只有几行代码。

我承认,这个对 Promise 的简短介绍相当粗糙,而且对于 Objective-C 开发人员来说也是相当新鲜的,并且可能听起来不常见。

您可以在 JS 社区阅读很多有关 Promise 的内容。Objective-C 中有一种或三种实现。实际实现不会超过几百行代码。碰巧我是其中一篇的作者:

RX 承诺

持保留态度,我可能完全有偏见,显然所有其他人也都处理过 Promise。;)