【问题标题】:NSOperation + setCompletionBlockNSOperation + setCompletionBlock
【发布时间】:2014-04-12 17:22:04
【问题描述】:

我对@9​​87654321@ 和NSOperationQueue 有几个不同的问题,我知道你们的回答会对我有所帮助;

我必须加载大量图像,并且我已经基于NSOperationNSOperationQueueNSURLConnection(异步加载)创建了自己的加载器;

问题:

  1. 如果我为队列(NSOperationQueue)设置maxConcurrentOperationCount(例如3),是否意味着即使队列有100个操作,同时也只有3个操作?

  2. 当我为队列设置属性maxConcurrentOperationCount 时,有时“setCompletionBlock”不起作用并且计数(operationCount)只会增加;为什么?

我的加载器:

- (id)init
{
    self = [super init];
    if (self) {
        _loadingFiles = [NSMutableDictionary new];
        _downloadQueue = [NSOperationQueue new];
        _downloadQueue.maxConcurrentOperationCount = 3;
        _downloadQueue.name = @"LOADER QUEUE";

    }
    return self;
}

- (void)loadFile:(NSString *)fileServerUrl handler:(GetFileDataHandler)handler {
    if (fileServerUrl.length == 0) {
        return;
    }

    if ([_loadingFiles objectForKey:fileServerUrl] == nil) {
        [_loadingFiles setObject:fileServerUrl forKey:fileServerUrl];

        __weak NSMutableDictionary *_loadingFiles_ = _loadingFiles;
        MyLoadOperation *operation = [MyLoadOperation new];
        [operation fileServerUrl:fileServerUrl handler:^(NSData *fileData) {
            [_loadingFiles_ removeObjectForKey:fileServerUrl];
            if (fileData != nil) {
                handler(fileData);
            }
        }];
        [operation setQueuePriority:NSOperationQueuePriorityLow];
        [_downloadQueue addOperation:operation];

        __weak NSOperationQueue *_downloadQueue_ = _downloadQueue;
        [operation setCompletionBlock:^{
            NSLog(@"completion block :%i", _downloadQueue_.operationCount);
        }];
    }
}

我的操作:

@interface MyLoadOperation()
@property (nonatomic, assign, getter=isOperationStarted) BOOL operationStarted;

@property(nonatomic, strong)NSString *fileServerUrl;

@property(nonatomic, copy)void (^OnFinishLoading)(NSData *);

@end
@implementation MyLoadOperation
- (id)init
{
    self = [super init];
    if (self) {
        _executing = NO;
        _finished = NO;
    }
    return self;
}
- (void)fileServerUrl:(NSString *)fileServerUrl
              handler:(void(^)(NSData *))handler {

    @autoreleasepool {

        self.fileServerUrl = fileServerUrl;

        [self setOnFinishLoading:^(NSData *loadData) {
            handler(loadData);
        }];

        [self setOnFailedLoading:^{
            handler(nil);
        }];
        self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc]
                                        initWithURL:self.url
                                        cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
                                        timeoutInterval:25];
        [request setValue:@"" forHTTPHeaderField:@"Accept-Encoding"];

        self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];

        [self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        [self.connection start];
        _data = [[NSMutableData alloc] init];

    }
}
- (void)main {
    @autoreleasepool {
        [self stop];
    }
}
- (void)start {
    [self setOperationStarted:YES];

    [self willChangeValueForKey:@"isFinished"];
    _finished = NO;
    [self didChangeValueForKey:@"isFinished"];
    if ([self isCancelled])
    {
        [self willChangeValueForKey:@"isFinished"];
        _finished = YES;
        _executing = NO;
        [self didChangeValueForKey:@"isFinished"];
    }
    else
    {
        [self willChangeValueForKey:@"isExecuting"];
        _finished = NO;
        _executing = YES;
        [self didChangeValueForKey:@"isExecuting"];
    }
}

- (BOOL)isConcurrent {

    return YES;
}

- (BOOL)isExecuting {
    return _executing;
}

- (BOOL)isFinished {

    return _finished;
}

- (void)cancel {
    [self.connection cancel];
    if ([self isExecuting])
    {
        [self stop];
    }
    [super cancel];
}
#pragma mark -NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [_data appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    if ([self OnFinishLoading]) {
        [self OnFinishLoading](_data);
    }
    if (![self isCancelled]) {
        [self stop];

    }
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
;
    if (![self isCancelled]) {
        [self stop];
    }
}
- (void)stop {
    @try {
        __weak MyLoadOperation *self_ = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [self_ completeOperation];
        });
    }
    @catch (NSException *exception) {
        NSLog(@"Exception! %@", exception);
        [self completeOperation];
    }
}
- (void)completeOperation {
    if (![self isOperationStarted]) return;

    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
    _executing = NO;
    _finished  = YES;

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

【问题讨论】:

  • 明确地说,您是说当使用 3 个 maxConcurrentOperationCount 时,您会看到所有 100 个操作都运行并完成(不仅仅是前三个),但看不到 operationCount一点都不改变? operationCount 方法并不完全可靠(请参阅该方法的文档),但您应该会看到返回值的一些变化。你能更详细地描述一下这种行为吗?
  • 感谢您的回答;是的,你是对的,我必须更好地描述这种行为;看,如果我删除了maxConcurrentOperationCount,那么上面显示的代码一切正常;很好 - 我的意思是 setCompletionBlock 每次调用和 operationCount 减少;如果我使用maxConcurrentOperationCount(数字无关紧要)setCompletionBlock 调用,但当我滚动tableview(显示加载图像的位置)时,它通常会停止工作;换句话说,我只更改了一行代码就改变了行为;

标签: ios objective-c nsurlconnection nsoperation nsoperationqueue


【解决方案1】:

您必须在操作的start 方法中启动连接,而不是在fileServerUrl:handler: 中。

我会完全删除这个方法,只提供一个包含所有必需参数的 init 方法,您可以在其中完全设置操作。然后,在方法@​​987654323@ 中启动连接。

此外,不清楚为什么要覆盖main

修改状态变量_executing_finished 可以更简洁明了(你不需要一开始就设置它们,因为它们已经初始化为NO)。仅在“final”方法completeOperation 中设置它们,包括 KVO 通知。

stop 中也不需要@try/@catch,因为函数dispatch_async() 不会抛出Objective-C 异常。

您的cancel 方法不是线程安全的,还有其他一些问题。我建议进行以下更改:

@implementation MyOperation {
    BOOL _executing;
    BOOL _finished;

    NSError* _error;  // remember the error
    id _result;       // the "result" of the connection, unless failed
    completion_block_t _completionHandler; //(your own completion handler)
    id _self; // strong reference to self
}

// Use the "main thread" as the "synchronization queue"

- (void) start
{
    // Ensure start will be called only *once*:
    dispatch_async(dispatch_get_main_queue(), ^{
        if (!self.isCancelled && !_finished && !_executing) {

            [self willChangeValueForKey:@"isExecuting"];
            _executing = YES;
            [self didChangeValueForKey:@"isExecuting"];
            _self = self; // keep a strong reference to self in order to make 
                          // the operation "immortal for the duration of the task

            // Setup connection:
            ...

            [self.connection start];
        }
    });
}

- (void) cancel 
{
    dispatch_async(dispatch_get_main_queue, ^{
        [super cancel];
        [self.connection cancel];
        if (!_finished && !_executing) {
            // if the op has been cancelled before we started the connection
            // ensure the op will be orderly terminated:
            self.error = [[NSError alloc] initWithDomain:@"MyOperation"
                                                    code:-1000
                                                userInfo:@{NSLocalizedDescriptionKey: @"cancelled"}];
            [self completeOperation];
        }
    });
}


- (void)completeOperation 
{
    [self willChangeValueForKey:@"isExecuting"];
    self.isExecuting = NO;
    [self didChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];
    self.isFinished = YES;
    [self didChangeValueForKey:@"isFinished"];

    completion_block_t completionHandler = _completionHandler;
    _completionHandler = nil;
    id result = self.result;
    NSError* error = self.error;
    _self = nil;
    if (completionHandler) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            completionHandler(result, error);
        });
    }
}


- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    if ([self onFinishLoading]) {
        [self onFinishLoading](self.result);
    }
    [self completeOperation];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    if (self.error == nil) {
        self.error = error;
    }
    [self completeOperation];
}

【讨论】:

  • 感谢您的回答;你给了我非常有用的建议;不幸的是,现在最大的问题是我无法停止执行操作。换句话说,方法cancel 从不调用;我不明白原因;
  • @O.Daniel 请在取消方法的第一行设置一个断点,在块中设置第二个 - 在主线程上执行。你打到第一个断点了吗?如果是这种情况,但你从来没有进入过阻塞,那么主线程可能被阻塞了。
  • 看,我有_downloadQueue,我在加载过程中添加了所有操作;有时我必须停止进程并从_downloadQueue 中删除所有操作。我接下来做 - [_downloadQueue cancelAllOperations];but 它不起作用,所有操作继续工作;你的问题的答案是否定的,两个断点都没有命中;
  • @O.Daniel cancelAllOperations 向当前队列中的所有操作发送取消消息。因此,您的代码有些奇怪。还要确保 _downloadQueue 不为零,并且您的 NSOperation 子类已正确子类化。
【解决方案2】:

回答您的问题:

  1. 是的,三个中的maxConcurrentOperationCount 意味着一次只能运行三个。像这样进行网络请求是您想要使用maxConcurrentOperationCount 的完美示例,因为不这样做会导致尝试运行的网络请求过多,很可能导致使用较慢的网络时某些连接失败连接。

  2. 不过,这里的主要问题是您正在从MyLoader 调用操作的fileServerUrl 方法(它正在启动连接)。您已将请求与操作的 start 断开连接(违背了 maxConcurrentCount 的目的 3 并可能混淆了操作的状态)。

    start 方法应该启动连接(即,在这三个可用并发操作之一可用之前不要启动请求)。此外,由于您无法将 URL 和 handler 传递给 start 方法,您应该将保存这些值的逻辑移动到您的 init 方法的自定义再现中。

我们可能会建议对您的操作进行其他小修改(main 不需要,operationStarted 有点多余,简化_executing/_finished 处理等),但连接的开始在fileServerUrl 中而不是由start 方法发起是关键问题。

因此:

- (id)initWithServerUrl:(NSString *)fileServerUrl
                handler:(void(^)(NSData *))handler
{
    self = [super init];
    if (self) {
        _executing = NO;
        _finished = NO;

        // do your saving of `fileServerURL` and `handler` here, e.g.

        self.fileServerUrl = fileServerUrl;

        self.OnFinishLoading:^(NSData *loadData) {
            handler(loadData);
        }];

        [self setOnFailedLoading:^{
            handler(nil);
        }];
    }
    return self;
}

- (void)startRequest {
    self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.url
                                                                cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
                                                            timeoutInterval:25];
    [request setValue:@"" forHTTPHeaderField:@"Accept-Encoding"];

    self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];

    [self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    [self.connection start];
    _data = [[NSMutableData alloc] init];
}

- (void)start {
    if ([self isCancelled])
    {
        [self willChangeValueForKey:@"isFinished"];
        _finished = YES;
        [self didChangeValueForKey:@"isFinished"];

        return;
    }

    [self setOperationStarted:YES];  // personally, I'd retire this and just reference your `executing` flag, but I'll keep it here for compatibility with the rest of your code

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

    [self startRequest];
}

【讨论】:

    【解决方案3】:

    对于第一个问题,答案是肯定的,如果将最大操作数设置为 3,则最多只能运行 3 个。
    第二个有点奇怪的问题,我不完全确定这个答案是否正确。
    当您将操作留给 NSOperationQueue 时,您无法确定它们将在哪个线程上执行,这会导致一个巨大的问题使用异步连接。
    当您像往常一样启动 NSURLConnection 时,您会毫无问题地收到委托回调,这是因为连接在具有活动运行循环的线程上运行。如果您在辅助线程上启动连接,则会在该线程上调用回调,但如果您不保持运行循环处于活动状态,它们将永远不会收到。
    这可能是我的答案不正确的地方, GCD 应该处理活运行循环,因为 GCD 队列在活线程上运行。
    但如果不是,问题可能是操作在不同的线程上启动,调用 start 方法,但是永远不会调用回调。尝试检查线程是否始终是主线程。

    【讨论】:

    • OP 在[NSRunLoop mainRunLoop] 上安排NSURLConnection 代表,这解决了您描述的问题。我认为问题出在其他地方。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-23
    相关资源
    最近更新 更多