6

从文档:

当 isFinished 方法返回的值变为 YES 时,就会执行您提供的完成块。因此,该块由操作对象在操作的主要任务完成或取消后执行。

我正在使用RestKit/AFNetworking,如果这很重要的话。

我的 in 中有多个依赖NSOperationOperationQueue。我使用完成块来设置我的孩子需要的一些变量(将结果附加到数组中)。

(task1,...,taskN) -> 任务A

taskA 添加依赖:task1-taskN

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

参考

NSOperations 和它们的完成块是否同时运行?

我做了一个简单的测试,在我的完成块中添加了一个睡眠,我得到了不同的结果。完成块在主线程中运行。当所有完成块都处于休眠状态时,子任务运行。

4

2 回答 2

5

正如我在下面的“一些观察”中讨论的那样,您无法保证在其他各种 AFNetworking 完成块完成之前不会开始此最终依赖操作。令我震惊的是,如果这个最终操作确实需要等待这些完成块完成,那么您有几个选择:

  1. 在每个完成块中使用信号量在完成时发出信号,并让完成操作等待n信号;或者

  2. 不要把这个最后的操作预先排队,而是让你的完成块来跟踪单个上传有多少未完成的上传,当它下降到零时,然后启动最后的“发布”操作。

  3. 正如您在评论中指出的那样,您可以将 AFNetworking 操作及其完成处理程序的调用包装在您自己的操作中,此时您可以使用标准addDependency机制。

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

    @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");
    }
    

    然后,您的应用程序代码可以执行类似的操作,使用addKeyDependency而不是addDependency,并在完成块中显式使用removeKeyDependency或:cancel

    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];
    

    这是 using AFHTTPRequestOperation,您显然会用适当的 AFNetworking 操作替换所有这些逻辑以进行上传,但希望它说明了这个想法。


原答案:

几点观察:

  1. 正如我认为你得出的结论,当你的操作完成时,它(a)启动它的完成块;maxConcurrentOperationCount(b) 使队列可用于其他操作(由于或由于操作之间的依赖关系而尚未开始的操作)。我不相信你有任何保证完成块将在下一次操作开始之前完成。

    根据经验,直到完成块完成后,依赖操作才真正触发,但是(a)我在任何地方都没有看到记录,(b)这没有实际意义,因为如果你使用的是 AFNetworking's own setCompletionBlockWithSuccess,它会结束up 将块异步分派到主队列(或定义的successCallbackQueue),从而阻碍任何(未记录的)同步保证。

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

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

    但是,如果您谈论的是 AFNetworking 的自定义完成块之一,例如您可能使用AFHTTPRequestOperation's设置的那些setCompletionBlockWithSuccess,那么,是的,确实这些通常被分派回主队列。但是 AFNetworking 使用标准completionBlock机制执行此操作,因此上述问题仍然适用。

于 2013-09-11T17:47:49.040 回答
2

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

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

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

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


编辑:

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

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

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

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

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

-(Promise*) fetchThingsWithURL:(NSURL*)url;

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

Promise* thingsPromise = [self fetchThingsWithURL:url];
thingsPromise.then(successHandlerBlock, failureHandlerBlock);

或者,内联块:

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>
});

更短:

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

这里,parseAsync:是一个返回 Promise 的异步方法。(是的,一个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);

这实际上启动了异步任务fetchThingsWithURL:。然后成功完成后,它会启动 async task 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
});

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

处理程序可能会返回一个 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;
});

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


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

array = @[
    [self task1],
    [self task2],
    [self task3]
];

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

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

设置一个 Promise 的值将被称为:“resolving”。你只能一次性解决一个承诺。

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

- (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;
}

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

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

self.fetchUserPromise = [self fetchUsersWithURL:url];

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

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

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

- (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;
}

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


所以,你可以用 Promise 做很多事情——甚至比这个简短的介绍还要多。如果你读到这里,你可能会知道如何解决你的实际问题。它就在那儿——只有几行代码。

我承认,对 Promise 的简短介绍非常粗略,而且对 Objective-C 开发人员来说也很新,而且听起来可能并不常见。

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

RX 承诺

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

于 2013-09-11T17:23:51.820 回答