NSOperation + setCompletionBlock

时间:2014-04-12 17:22:05

标签: ios objective-c nsurlconnection nsoperation nsoperationqueue

关于NSOperationNSOperationQueue,我有几个不同的问题,我知道你的回答会帮助我;

我必须加载大量图片,并根据NSOperationNSOperationQueueNSURLConnection(异步加载)创建了自己的加载器;

问题:

  1. 如果我为队列(maxConcurrentOperationCount)设置NSOperationQueue(例如3),是否意味着在同一时间内只执行3次操作甚至队列有100次操作?

  2. 我有时为队列设置属性maxConcurrentOperationCount" setCompletionBlock"没有工作和计数(operationCount)只会增加;为什么呢?

  3. MyLoader:

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

    MyOperation:

    @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 个答案:

答案 0 :(得分:1)

回答你的问题:

  1. 是的,三个maxConcurrentOperationCount表示一次只能运行三个。像这样做网络请求是你想要使用maxConcurrentOperationCount时的完美示例,因为如果不这样做会导致尝试运行的网络请求太多,很可能导致某些连接在使用时失败网络连接速度较慢。

  2. 但是,这里的主要问题是您从fileServerUrl调用操作的MyLoader方法(即开始连接)。您已将请求与操作start断开连接(违反了maxConcurrentCount 3的目的,可能会混淆操作状态。

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

  3. 我们可能会对您的操作提出其他一些小修改(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];
    }
    

答案 1 :(得分:1)

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

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

此外,您还不清楚覆盖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];
}

答案 2 :(得分:0)

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