取消后,NSOperation仍然在NSOperationQueue

时间:2014-03-21 10:29:27

标签: ios nsurlconnection nsoperation nsoperationqueue nsrunloop

我使用NSOperation和NSOperationQueue执行下载图像。每个操作都保留一个StoreThumbRequest对象,该对象封装所有特定于请求的数据,包括目标视图,等待图像。同时,此视图保留NSOperation对象以在重用或取消分配期间取消操作。这个保留循环在'取消'和'主要'方法在适当的时候。

我在为maxConcurrentOperationsCount设置限制时发现NSOperation保留在NSOperationQueue中。达到这个限制阻止了主要的'被称为新NSOperations的方法。 NSOperation只有在取消时才会保留在队列中。如果它设法完成它的任务,它将从NSOperationQueue中正常删除。

我的NSOperations是非并发的,没有依赖关系。

有什么建议吗? Thanx提前

#import "StoreThumbLoadOperation.h"
#import "StoreThumbCache.h"

@interface StoreThumbLoadOperation () <NSURLConnectionDelegate> {
    StoreThumbRequest *_request;
    NSMutableData *_downloadedData;
    NSURLConnection *_connection;
    NSPort *_port;
}

@end

@implementation StoreThumbLoadOperation

-(id)initWithRequest: (StoreThumbRequest *)request
{
    NSParameterAssert(request);
    self = [super init];
    if (self) {
        _request = request;
    }
    return self;
}

-(void)main
{
    NSURL *url = ...;

    NSURLRequest *request = [NSURLRequest requestWithURL: url];
    _connection = [[NSURLConnection alloc] initWithRequest: request delegate: self startImmediately: NO];

    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    _port = [NSMachPort port];
    [runLoop addPort: _port forMode: NSDefaultRunLoopMode];
    [_connection scheduleInRunLoop: runLoop forMode: NSDefaultRunLoopMode];
    [_connection start];
    [runLoop run];
}

-(void)cancel
{
    [super cancel];

    [_connection unscheduleFromRunLoop: [NSRunLoop currentRunLoop] forMode: NSDefaultRunLoopMode];
    [_connection cancel];
    _request.thumbView.operation = nil; //break retain loop
    [[NSRunLoop currentRunLoop] removePort: _port forMode: NSDefaultRunLoopMode];
}

#pragma mark - NSURLConnectionDelegate

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    _downloadedData = [NSMutableData new];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    [_downloadedData appendData: data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    if (_connection == connection) {
        NSFileManager *fileManager = [NSFileManager new];
        NSString *pathForDownloadedThumb = [[StoreThumbCache sharedCache] thumbPathOnRequest: _request];
        NSString *pathForContainigDirectory = [pathForDownloadedThumb stringByDeletingLastPathComponent];
        if (! [fileManager fileExistsAtPath: pathForContainigDirectory]) {
            //create a directory if required
            [fileManager createDirectoryAtPath: pathForContainigDirectory withIntermediateDirectories: YES attributes: nil error: nil];
        }

        if (! self.isCancelled) {
            [_downloadedData writeToFile: pathForDownloadedThumb atomically: YES];
            UIImage *image = [UIImage imageWithContentsOfFile: pathForDownloadedThumb];

            //an image may be empty
            if (image) {
                if (! self.isCancelled) {
                    [[StoreThumbCache sharedCache] setThumbImage: image forKey: _request.cacheKey];

                    if (_request.targetTag == _request.thumbView.tag) {
                        dispatch_async(dispatch_get_main_queue(), ^{
                            _request.thumbView.image = image;
                        });
                    }
                }
            }
             _request.thumbView.operation = nil; //break retain loop
            [[NSRunLoop currentRunLoop] removePort: _port forMode: NSDefaultRunLoopMode];
        }
    }
}


- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    if (error.code != NSURLErrorCancelled) {
        #ifdef DEBUG
        NSLog(@"failed downloading thumb for name: %@ with error %@", _request.thumbName, error.localizedDescription);
        #endif

        _request.thumbView.operation = nil;
        [[NSRunLoop currentRunLoop] removePort: _port forMode: NSDefaultRunLoopMode];
    }
}

@end

据我所知,预计NSOperation不会立即从其NSOperationQueue中删除。但它仍然存在无限期的时间

来自NSOperation取消方法文档

  

此方法不会强制您的操作代码停止。相反,它会更新对象的内部标志以反映状态的变化。

2 个答案:

答案 0 :(得分:1)

我相信这种情况正在发生,因为runloop(当您打算结束操作时没有删除)会使操作保持活动状态。

替换以下行的所有实例:

[[NSRunLoop currentRunLoop] removePort: _port forMode: NSDefaultRunLoopMode];

这一个:

[[NSRunLoop currentRunLoop] removePort: _port forMode: NSDefaultRunLoopMode];
CFRunLoopStop(CFRunLoopGetCurrent());

答案 1 :(得分:1)

事实证明,在'main'方法中,我没有检查操作是否已被取消。我也没有特别考虑从主线程中调用'cancel'方法

我也没有检查'runLoop'是否曾停止过。如果没有附加输入源,它通常会停止,所以我从循环中删除了端口。 我现在改变了使用'runMode:beforeDate:'运行NSRunLoop的方式。

这是更新后的工作代码

#define STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED() \
if (self.cancelled) {\
    [self internalComplete]; \
    return; \
}

@interface StoreThumbLoadOperation () <NSURLConnectionDelegate>
@property (strong, nonatomic) StoreThumbRequest *request;
@property (strong, nonatomic) NSMutableData *downloadedData;
@property (strong, nonatomic) NSURLConnection *connection;
@property (strong, nonatomic) NSPort *port;
@property (strong, nonatomic) NSRunLoop *runLoop;
@property (strong, nonatomic) NSThread *thread;
@property (assign, nonatomic) unsigned long long existingOnDiskThumbWeightInBytes;
@property (assign, nonatomic) BOOL isCompleted;
@property (assign, nonatomic) BOOL needsStop;
@end

@implementation StoreThumbLoadOperation

-(id)initWithRequest: (StoreThumbRequest *)request existingCachedImageSize:(unsigned long long)bytes
{
    NSParameterAssert(request);
    self = [super init];
    if (self) {
        self.request = request;
        self.existingOnDiskThumbWeightInBytes = bytes;
    }
    return self;
}

-(void)main
{
    // do not call super for optimizations
    //[super main];

    STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED();

    @autoreleasepool {
        NSURL *url = ...;

        NSURLRequest *request = [NSURLRequest requestWithCredentialsFromUrl:url];
        self.connection = [[NSURLConnection alloc] initWithRequest: request delegate: self startImmediately: NO];

        self.runLoop = [NSRunLoop currentRunLoop];
        self.port = [NSMachPort port];
        self.thread = [NSThread currentThread];  // <- retain the thread

        [self.runLoop addPort: self.port forMode: NSDefaultRunLoopMode];
        [self.connection scheduleInRunLoop: self.runLoop forMode: NSDefaultRunLoopMode];

        [self.connection start];

        // will run the loop until the operation is not cancelled or the image is not downloaded
        NSTimeInterval stepLength = 0.1;
        NSDate *future = [NSDate dateWithTimeIntervalSinceNow:stepLength];
        while (! self.needsStop && [self.runLoop runMode:NSDefaultRunLoopMode beforeDate:future]) {
            future = [future dateByAddingTimeInterval:stepLength];
        }

        // operation is cancelled, or the image is downloaded
        self.isCompleted = YES;
        [self internalComplete];
    }
}

-(void)cancel {
    [super cancel];
    STORE_THUMB_LOAD_OPERATION_LOG_STATUS(@"cancelled");
    [self setNeedsStopOnPrivateThread];
}

- (BOOL)isFinished {
    // the operation must become completed to be removed from the queue
    return self.isCompleted || self.isCancelled;
}

#pragma mark - privates

- (void)setNeedsStopOnPrivateThread {
  // if self.thread is not nil, that the 'main' method was already called. if not, than the main thread cancelled the operation before it started
  if (! self.thread || [NSThread currentThread] == self.thread) {
     [self internalComplete];
  } else {
     [self performSelector:@selector(internalComplete) onThread:self.thread withObject:nil waitUntilDone:NO];
  }
}

- (void)internalComplete {
    [self cleanUp];
    self.needsStop = YES; // <-- will break the 'while' loop
}

- (void)cleanUp {
    [self.connection unscheduleFromRunLoop: self.runLoop forMode: NSDefaultRunLoopMode];
    [self.connection cancel];
    self.connection = nil;

    //break retain loop
    self.request.thumbView.operation = nil;

    [self.runLoop removePort: self.port forMode: NSDefaultRunLoopMode];
    self.port = nil;
    self.request = nil;
    self.downloadedData = nil;
}

#pragma mark - NSURLConnectionDelegate

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    self.downloadedData = [NSMutableData new];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.downloadedData appendData: data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    if (self.connection != connection)
        return;

    STORE_THUMB_LOAD_OPERATION_LOG_STATUS(@"entered connection finished");

    //create a directory if required

    STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED();

    // wright the image to the file

    STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED();

    // create the image from the file
    UIImage *image = [UIImage imageWithContentsOfFile:...];

    STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED();

    if (image) {
        // cache the image

        STORE_THUMB_LOAD_OPERATION_RETURN_IF_CANCELLED();

        // update UI on the main thread
        if (self.request.targetTag == self.request.thumbView.tag) {
            StoreThumbView *targetView = self.request.thumbView;
            dispatch_async(dispatch_get_main_queue(), ^{
                targetView.image = image;
            });
        }
    }

    [self setNeedsStopOnPrivateThread];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    if (error.code != NSURLErrorCancelled) {            
        [self cancel];
    }
}

@end