I perform downloading images using NSOperation and an NSOperationQueue. Every Operation retains a StoreThumbRequest object encapsulating all request specific data including a target view, waiting for an image. At the same time this view retains an NSOperation object to cancel an operation during reusing or deallocations. This retain loop is carefully broken at 'cancel' and 'main' methods in appropriate times.
I found an NSOperation to remain in a NSOperationQueue when set a limit on its maxConcurrentOperationsCount. Reaching this limit prevented 'main' method of new NSOperations from being called. NSOperation remains in a queue only when it's cancelled. If it managed to finish its task, it is gracefully removed from its NSOperationQueue.
My NSOperations are non-concurrent and have no dependencies.
Any suggestions? Thanx in advance
#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
I understand, that an NSOperation is not expected to be immediately removed from its NSOperationQueue. But it remains there for an unlimited time period
From NSOperation cancel method documentation
This method does not force your operation code to stop. Instead, it updates the object’s internal flags to reflect the change in state.