52

我找不到关于如何将子类化为NSOperation并发以及支持取消的好的文档。我阅读了 Apple 文档,但找不到“官方”示例。

这是我的源代码:

@synthesize isExecuting = _isExecuting;
@synthesize isFinished = _isFinished;
@synthesize isCancelled = _isCancelled;

- (BOOL)isConcurrent
{
    return YES;
}

- (void)start
{
/* WHY SHOULD I PUT THIS ?
    if (![NSThread isMainThread])
    {
        [self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
        return;
    }
*/

    [self willChangeValueForKey:@"isExecuting"];
    _isExecuting = YES;
    [self didChangeValueForKey:@"isExecuting"];


    if (_isCancelled == YES)
    {
        NSLog(@"** OPERATION CANCELED **");
    }
    else
    {
        NSLog(@"Operation started.");
        sleep(1);
        [self finish];
    }
}

- (void)finish
{
    NSLog(@"operationfinished.");

    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];

    _isExecuting = NO;
    _isFinished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];

    if (_isCancelled == YES)
    {
        NSLog(@"** OPERATION CANCELED **");
    }
}

在我找到的示例中,我不明白为什么使用 performSelectorOnMainThread:。它会阻止我的操作同时运行。

此外,当我注释掉该行时,我的操作会同时运行。但是,isCancelled即使我调用了cancelAllOperations.

4

6 回答 6

111

好的,据我了解,您有两个问题:

  1. 您是否需要performSelectorOnMainThread:出现在代码注释中的段?那个代码有什么作用?

  2. 为什么调用包含该操作的那个_isCancelled标志没有被修改?cancelAllOperationsNSOperationQueue

让我们按顺序处理这些。我将假设您的子类NSOperation被称为MyOperation,只是为了便于解释。我会解释你的误解,然后给出一个更正的例子。

1.同时运行NSOperations

大多数时候,您将NSOperations 与 一起使用NSOperationQueue,并且从您的代码中,听起来这就是您正在做的事情。在这种情况下MyOperation,无论-(BOOL)isConcurrent方法返回什么,您都将始终在后台线程上运行,因为NSOperationQueues 被明确设计为在后台运行操作。

因此,您通常不需要重写该-[NSOperation start]方法,因为默认情况下它只是调用该-main方法。那是您应该覆盖的方法。默认-start方法已经在适当的时候为您isExecuting处理设置。isFinished

因此,如果您希望 anNSOperation在后台运行,只需覆盖该-main方法并将其放在NSOperationQueue.

您的performSelectorOnMainThread:代码中的 将导致 的每个实例MyOperation始终在主线程上执行其任务。由于一次只能在一个线程上运行一段代码,这意味着没有其他MyOperations 可以运行。NSOperationand的全部目的NSOperationQueue是在后台做一些事情。

唯一一次你想把事情强加到主线程上是在你更新用户界面的时候。如果您需要在MyOperation完成后更新 UI,那么您应该使用performSelectorOnMainThread:. 我将在下面的示例中展示如何做到这一点。

2. 取消一个 NSOperation

-[NSOperationQueue cancelAllOperations]调用该-[NSOperation cancel]方法,这会导致后续调用-[NSOperation isCancelled]返回YES但是,您已经做了两件事使这无效。

  1. 您正在使用@synthesize isCancelled覆盖 NSOperation 的-isCancelled方法。没有理由这样做。NSOperation已经-isCancelled以完全可接受的方式实施。

  2. 您正在检查自己的_isCancelled实例变量以确定操作是否已被取消。NSOperation保证如果操作被取消,[self isCancelled]它将返回。YES它不保证将调用您的自定义 setter 方法,也不保证您自己的实例变量是最新的。你应该检查[self isCancelled]

你应该做什么

标题:

// MyOperation.h
@interface MyOperation : NSOperation {
}
@end

和实施:

// MyOperation.m
@implementation MyOperation

- (void)main {
    if ([self isCancelled]) {
        NSLog(@"** operation cancelled **");
    }

    // Do some work here
    NSLog(@"Working... working....")

    if ([self isCancelled]) {
        NSLog(@"** operation cancelled **");
    }
    // Do any clean-up work here...

    // If you need to update some UI when the operation is complete, do this:
    [self performSelectorOnMainThread:@selector(updateButton) withObject:nil waitUntilDone:NO];

    NSLog(@"Operation finished");
}

- (void)updateButton {
    // Update the button here
}
@end

请注意,您不需要对 、 或 执行isExecuting任何isCancelled操作isFinished。这些都是自动为您处理的。只需覆盖该-main方法。就这么容易。

(注意:从技术上讲,这不是“并发” NSOperation,在某种意义上,它会按照上面的实现-[MyOperation isConcurrent]返回NO。但是,它将后台线程上运行。该isConcurrent方法确实应该命名为-willCreateOwnThread,因为这是更准确的描述方法的意图。)

于 2010-11-29T04:24:44.110 回答
5

@BJHomer 的出色回答值得更新。

并发操作应该覆盖start方法而不是main.

苹果文档中所述:

如果您正在创建并发操作,则至少需要覆盖以下方法和属性:

  • start
  • asynchronous
  • executing
  • finished

正确的实现也需要覆盖cancel。使子类线程安全并正确获得所需的语义也非常棘手。

因此,我在代码审查中提出了一个完整且有效的子类作为在 Swift 中实现的提案。欢迎提出意见和建议。

这个类可以很容易地用作自定义操作类的基类。

于 2015-10-15T11:42:07.137 回答
2

我知道这是一个老问题,但我最近一直在调查这个问题,遇到了同样的例子,也有同样的疑问。

如果您的所有工作都可以在 main 方法中同步运行,那么您不需要并发操作,也不需要覆盖 start,只需完成您的工作并在完成后从 main 返回。

然而,如果你的工作负载本质上是异步的——即加载一个 NSURLConnection,你必须从子类开始。当您的 start 方法返回时,操作尚未完成。只有当您手动将 KVO 通知发送到 isFinished 和 isExecuting 标志时(例如,一旦异步 URL 加载完成或失败),它才会被 NSOperationQueue 视为完成。

最后,当您要启动的异步工作负载需要在主线程上侦听运行循环时,您可能希望将启动分派到主线程。由于工作本身是异步的,它不会限制您的并发性,但在工作线程中启动工作可能没有准备好适当的运行循环。

于 2013-10-18T14:06:37.287 回答
0

看看ASIHTTPRequest。它是一个建立在子类之上的 HTTP 包装类,NSOperation并且似乎实现了这些。请注意,截至 2011 年年中,开发人员建议不要将 ASI 用于新项目。

于 2010-11-28T21:23:26.050 回答
0

关于在 NSOperation 子类中定义“取消”属性(或定义“ _cancelled ”iVAR),通常这不是必需的。仅仅因为当 USER 触发取消时,自定义代码应该总是通知 KVO 观察者你的操作现在已经完成了它的工作。换句话说,isCancelled => isFinished。

特别是,当 NSOperation 对象依赖于其他操作对象的完成时,它会监视那些对象的 isFinished 键路径。因此,未能生成完成通知(如果发生取消)可能会阻止在您的应用程序中执行其他操作。


顺便说一句,@BJ Homer 的回答:“isConcurrent 方法确实应该命名为 -willCreateOwnThread”,这很有意义

因为如果不覆盖 start-method,只需手动调用 NSOperation-Object 的 default-start-method,calling-thread 本身默认是同步的;所以,NSOperation-Object 只是一个非并发操作。

但是,如果您确实覆盖了 start-method,在 start-method 实现中,自定义代码应该产生一个单独的线程……等等,那么您成功地打破了“调用线程默认为同步”的限制,从而使 NSOperation-Object 成为一个并发操作,之后可以异步运行。

于 2014-07-13T00:59:49.267 回答
-2

这篇博文:

http://www.dribin.org/dave/blog/archives/2009/09/13/snowy_concurrent_operations/

解释了您可能需要的原因:

if (![NSThread isMainThread])
{
    [self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
    return;
}

在你的start方法中。

于 2012-10-01T22:15:52.420 回答