NSOperation Subclass performance or leak - ios

Following is the subclass implementation of NSOperation Subclass
The operation will be use to asynchronously download Image from server.
-(void) main{
#autoreleasepool {
//NSURLConnection
NSURLRequest *request=[NSURLRequest requestWithURL:[NSURL URLWithString:#"A URl"]];
_downloadConnection=[[NSURLConnection alloc] initWithRequest:request delegate:self];
}
}
-(BOOL)isConcurrent{
return YES;
}
-(BOOL)isExecuting{
return _isExecuting;
}
-(BOOL)isFinished{
return _isFinished;
}
-(void)finish{
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
_isExecuting = NO;
_isFinished =YES;
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
}
-(void)cancel{
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
_isExecuting = NO;
_isFinished =YES;
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
[connection cancel];
connection=nil;
[self finish];
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
//Other Code
[connection cancel];
connection=nil;
[self finish];
}
Please let me know if there might be anything that I might have missed out within the code, so as to avoid leaks and check all KVO are handled properly.

I see a couple of issues:
Your finish and cancel routines are calling willChangeValueForKey twice for each key. Obviously that second call should be didChangeValueForKey.
I would advise against implementing cancel method. The default implementation does some other stuff. Do not implement that method. If you really want to, there are changes I'd advise here (at very least, cancel connection, too; call super; some other things, too), but I'd simply advise against implementing it at all and detect cancellation in didReceiveData (see point #5).
This code does not appear to be setting _isExecuting (nor do appropriate KVO) when the operation starts. Maybe that's in a start method you neglected to share with us?
Likewise, that start method should check to see if the operation has been canceled already and if so, stop the operation immediately.
In didReceiveData, are you checking isCancelled, as well? Making operations cancelable is one of the main reasons you use operation queues.
You're starting a NSURLConnection within an operation (presumably with the intent to add this operation to some random queue). But NSURLConnection will not work correctly unless you either schedule it on the main run loop (the easy solution) or you create your own run loop alive (there are a variety of techniques of that).
For example, to tell the operation to schedule the connection in the main run loop, you could do something like:
_downloadConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:FALSE];
[_downloadConnection scheduleInRunLoop:[NSRunLoop mainRunLoop] runLoopModes:NSRunLoopCommonModes];
[_downloadConnection start];
I'm typing that without benefit of Xcode, so if I mistyped a method forgive me, but it illustrates the idea: Create connection with startImmediately of FALSE, schedule it on the main run loop, and only then should you start it.
If connectionDidFinishLoading is called, it is entirely unnecessary to call [connection cancel].
As of iOS 7, isConcurrent has been deprecated in favor of isAsynchronous. If you need to support earlier iOS versions, though, keep isConcurrent.
By the way, while I think it might work the way you've laid it out, it is generally advised to implement properties called executing and finished:
#property (nonatomic, readwrite, getter=isExecuting) BOOL executing;
#property (nonatomic, readwrite, getter=isFinished) BOOL finished;
I then manually synthesize:
#synthesize finished = _finished;
#synthesize executing = _executing;
And then I implement manual setters (but rely upon the synthesized getters and ivars):
- (void)setExecuting:(BOOL)executing
{
if (_executing != executing) {
[self willChangeValueForKey:#"isExecuting"];
_executing = executing;
[self didChangeValueForKey:#"isExecuting"];
}
}
- (void)setFinished:(BOOL)finished
{
if (_finished != finished) {
[self willChangeValueForKey:#"isFinished"];
_finished = finished;
[self didChangeValueForKey:#"isFinished"];
}
}
But if you do that, you can now just set self.executing = FALSE (or whatever) and it (a) does appropriate KVO, saving you from littering your code with all sorts of KVO calls; and (b) but saves you from having to implement properties and getters manually.

Related

setDelegate self and using delegates in ASINetworkQueue. Selector not found when request finished

I have the following class:
File_Downloadmanager.h:
#import "ASINetworkQueue.h"
#interface File_Downloadmanager : NSObject {
}
-(void)addRequestToDownloadQueue:(NSString*)objectID :(NSString*)userID :(NSString*)filename;
-(void)initDownloadQueue; // creates a new download queue and sets delegates
-(void)startDownload; // starts the download queue
-(void)requestFinished;
-(void)requestFailed;
-(void)queueFinished;
#property(retain) ASINetworkQueue *downloadQueue;
#end
File_Downloadmanager.m:
#implementation File_Downloadmanager
#synthesize downloadQueue;
-(void)initDownloadQueue{
NSLog(#"Init DownloadQueue");
// Stop anything already in the queue before removing it
[[self downloadQueue] cancelAllOperations];
[self setDownloadQueue:[ASINetworkQueue queue]];
[[self downloadQueue] setDelegate:self];
[[self downloadQueue] setRequestDidFinishSelector:#selector(requestFinished:)];
[[self downloadQueue] setRequestDidFailSelector:#selector(requestFailed:)];
[[self downloadQueue] setQueueDidFinishSelector:#selector(queueFinished:)];
[self downloadQueue].shouldCancelAllRequestsOnFailure = NO;
}
-(void)startDownload{
NSLog(#"DownloadQueue Go");
[downloadQueue go];
}
- (void)requestFinished:(ASIHTTPRequest *)request
{
// If no more elements are queued, release the queue
if ([[self downloadQueue] requestsCount] == 0) {
[self setDownloadQueue:nil];
}
NSLog(#"Request finished");
}
- (void)requestFailed:(ASIHTTPRequest *)request
{
// You could release the queue here if you wanted
if ([[self downloadQueue] requestsCount] == 0) {
[self setDownloadQueue:nil];
}
//... Handle failure
NSLog(#"Request failed");
}
- (void)queueFinished:(ASINetworkQueue *)queue
{
// You could release the queue here if you wanted
if ([[self downloadQueue] requestsCount] == 0) {
[self setDownloadQueue:nil];
}
NSLog(#"Queue finished");
}
-(void)addRequestToDownloadQueue:(NSString*)objectID :(NSString*)userID :(NSString*)filename{
...SourceCode for creating the request...
// add operation to queue
[[self downloadQueue] addOperation:request];
}
In another class a function is called an inside that function I'm doing the following:
-(void)downloadFiles{
File_Downloadmanager * downloadhandler = [[File_Downloadmanager alloc]init];
// initialize download queue
[downloadhandler initDownloadQueue];
for (int i = 0; i < [meetingObjects count]; i++) {
....some other code to get the objectID, userID, etc.
[downloadhandler addRequestToDownloadQueue:ID :[loginData stringForKey:#"userId"] :[NSString stringWithFormat:#"%#%#",currentObject.id,currentObject.name]]
}
[downloadhandler startDownload];
}
Everything works fine and the download begins. But when the first file is downloaded, I get an error in the ASINetworkQueue class that my selector "requestFinished" can't be called (I don't have the exact message, can't start the app at the moment, but the failure code was exc_bad_access code=1).
Is the time of declaration / initialization of my File_Downloadmanager object the problem? Because the function "downloadFiles" is called, the DownloadManager object created, the requests added and then the "downloadFiles" method returns because the queue works async?
I haven't used the ASI networking stuff before, but have seen lots of references to it on the net.
It sounds to me like the ASINetworkQueue class expects it's delegate to conform to a specific protocol. If it's set up correctly, you should get a warning when you try to assign yourself as the delegate of the ASINetworkQueue object but have not declared that your class conforms to the appropriate protocol. If you DO include a protocol declaration, then you should get a warning that you have not implemented required methods from that protocol.
Try cleaning your project and rebuilding, and then look carefully for warnings, specifically on your line:
[[self downloadQueue] setDelegate:self];
EDIT: I just downloaded one of the ASIHTTPRequest projects, and to my dismay, the delegate property of the ASINetworkQueue class does not have to conform to a specific protocol. This is bad programming style. If you set up a delegate, you should make the delegate pointer conform to a specific protocol.
Also, be aware that the ASI networking classes have not been maintained for several years now and are getting badly out of date. There are better alternatives out there, and you should look at moving to a different networking framework.
It looks like the downloadhandler object that ASINetworkQueue is attempting to send the requestFinished message to no longer exists at the time that the message is sent to it, as it's probably being deallocated when the downloadFiles method finishes executing. Instead of making the downloadhandler object local to the downloadFiles method, instead make it a (strong, nonatomic) property within the class that contains the downloadFiles method. That way, you can ensure that it will still exist when requestFinished is called.

iphone nsoperation application freezes

I have made a subclass of NSOperation called ˚ to achieve multiple movie downloads . In the appDelegate.m , I have made an object of NSOperationQueue .
- (void)applicationDidFinishLaunching:(UIApplication *)application {
queue = [[NSOperationQueue alloc] init];
[queue setMaximumConcurrentOperationCount:5]
}
MovieDownloadOperation depends on a class called Downloader which actually downloads the movie
and gives callback movieCompletelyDownloadedWithUrl: .
Then , I have made a property called downloadState in MovieDownloadOperation . It has different values like "STARTED" , "DOWNLOADING" , "COMPLETED" , "ERROR".
MyDownloadOperation looks like
-(id)initWithUrl:(NSURL *)url
{
if (self = [super init])
{
_downloader = [[Downloader alloc] initWithUrl:url];
_downloadState = #"STARTED" ;
}
}
-(void)main
{
while(1)
{
if ([_downloadState isEqualToString:#"COMPLETED"])
{
NSLog(#"movie downloaded successfully");
break ;
}
}
}
-(void)movieCompletelyDownloadedWithUrl:(NSURL *)url
{
_downloadState = #"COMPLETED" ;
}
This works well for one movie , but when I try to download more than one movie , the UI freezes until the first is downloaded . I think the the problem is the while loop inside the main method , is there a better way to check if the _downloadState is changed to "COMPLETED" ??
It's unclear why the UI freezes with multiple operations, but not with only one download. But, your code sample provokes a couple of thoughts:
Concurrent Operation:
Rather than having a while loop in main, and you'd generally would define your operation to be concurrent (i.e. return YES from isConcurrent). Then movieCompletelyDownloadedWithUrl would post the isFinished event, which would trigger the completion of the operation.
In terms of how to make a concurrent operation, you might define properties for executing and finished:
#property (nonatomic, readwrite, getter = isFinished) BOOL finished;
#property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
You'd probably want to have a strong property for the URL and the downloader:
#property (nonatomic, strong) NSURL *url;
#property (nonatomic, strong) Downloader *downloader;
And then you might have the following code in the operation subclass:
#synthesize finished = _finished;
#synthesize executing = _executing;
- (id)init
{
self = [super init];
if (self) {
_finished = NO;
_executing = NO;
}
return self;
}
- (id)initWithUrl:(NSURL *)url
{
self = [self init];
if (self) {
// Note, do not start downloader here, but just save URL so that
// when the operation starts, you have access to the URL.
_url = url;
}
return self;
}
- (void)start
{
if ([self isCancelled]) {
self.finished = YES;
return;
}
self.executing = YES;
[self main];
}
- (void)main
{
// start the download here
self.downloader = [[Downloader alloc] initWithUrl:self.url];
}
- (void)completeOperation
{
self.executing = NO;
self.finished = YES;
}
// you haven't shown how this is called, but I'm assuming you'll fix the downloader
// to call this instance method when it's done
- (void)movieCompletelyDownloadedWithUrl:(NSURL *)url
{
[self completeOperation];
}
#pragma mark - NSOperation methods
- (BOOL)isConcurrent
{
return YES;
}
- (void)setExecuting:(BOOL)executing
{
[self willChangeValueForKey:#"isExecuting"];
_executing = executing;
[self didChangeValueForKey:#"isExecuting"];
}
- (void)setFinished:(BOOL)finished
{
[self willChangeValueForKey:#"isFinished"];
_finished = finished;
[self didChangeValueForKey:#"isFinished"];
}
So, with these methods, you might then have movieCompletelyDownloadedWithUrl call completeOperation like above, which will ensure that isExecuting and isFinished notifications get posted. You'd also want to respond to cancellation event, too, making sure to cancel the download if the operation is canceled.
See Configuring Operations for Concurrent Execution section of the Concurrency Programming Guide for more details.
Don't initiate download until main:
I don't see your main method initiating the download. That makes me nervous that your Downloader initialization method, initWithURL, might be initiating the download, which would be bad. You don't want downloads initiating when you create the operation, but rather you shouldn't do that until the operation starts (e.g. start or main). So, in my above example, I only have initWithURL save the URL, and then main is what starts the download.
Using NSURLConnectionDataDelegate methods in NSOperation:
As an aside, you didn't share how your operation is doing the network request. If you're using NSURLConnectionDataDelegate methods, when you get rid of that while loop in main, you might have problems if you don't schedule the NSURLConnection in a particular run loop. For example, you might do:
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
If you're not using NSURLConnectionDataDelegate methods, or if you've already addressed this run loop issue, then ignore this counsel, but, bottom line, when you fix the main method in your operation, you might expose the NSURLConnection issue that your old main might have hidden from you.
How does Downloader invoke moveCompleteDownloadedWithUrl?
BTW, you're not showing how Downloader could possibly invoke moveCompleteDownloadedWithUrl. That looks suspicious, but I'm just hoping you simplified your code when you posted it. But if you're not using a protocol-delegate pattern or completion block pattern, then I'd be very nervous about how your multiple Downloader objects are informing the respective MyDownloadOperation objects that the download is done. Personally, I might be inclined to refactor these two differ classes into one, but that's a matter of personal taste.
You can use NSTimer to check whether your download is completed or not. It'll not freezes your UI
NSTimer *localTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(checkDownloadStatus) userInfo:nil repeats:YES];
-(void)checkDownloadStatus
{
if ([_downloadState isEqualToString:#"COMPLETED"])
{
NSLog(#"movie downloaded successfully");
[localTimer invalidate];
}
}

NSOperation KVO isFinished

Im trying to subclass a NSOperation, and read some sample from,
they say: when the task finished, using KVO of NSOperation, to finish the operation,
code here:
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"]
finished = YES;
executing = NO;
[self didChangeValueForKey:#"isFinished"];
[self didChangeValueForKey:#"isExecuting"];
then isFinished get called
- (BOOL) isFinished{
return(finished);
}
anyone could explain this to me? why isFinished gets called, will the isFinished finish the operation? as I understanded, do KVO manually need [self didChangeValueForKey:#"isExecuting"]; and I didnt see code like addobserver: and observeValueForKeyPath:
I write
-(void)call
{
[self willChangeValueForKey:#"isVip"];
[self didChangeValueForKey:#"isVip"];
}
-(void)isVip
{
NSLog(#"Im vip");
}
isVip is not called when do [self call];
The NSOperationQueue implementation will observe the "isFinished" property of your operation (using KVO) so it knows when to remove it from the queue. isFinished is most likely being called by internal Apple code after it has been told of the change to its value.
Adding to quellish answer, this is how you would override executing, finished, cancelled.
//.m
#interface MyOperation () //class extension, make these otherwise read-only properties read-write, we must synthesize
#property(atomic, assign, readwrite, getter=isExecuting) BOOL executing;
#property(atomic, assign, readwrite, getter=isFinished) BOOL finished;
#property(atomic, assign, readwrite, getter=isCancelled) BOOL cancelled;
#end
#implementation CoreLocationOperation
#synthesize executing, finished, cancelled;
+ (BOOL)automaticallyNotifiesObserversForKey {
return YES;
}
+ (NSSet *)keyPathsForValuesAffectingIsCancelled {
NSSet *result = [NSSet setWithObject:#"cancelled"];
return result;
}
+ (NSSet *)keyPathsForValuesAffectingIsExecuting {
NSSet *result = [NSSet setWithObject:#"executing"];
return result;
}
+ (NSSet *)keyPathsForValuesAffectingIsFinished {
NSSet *result = [NSSet setWithObject:#"finished"];
return result;
}
- (void)start {
//..
//You can use self.executing = YES; (note we can change executing which would otherwise be read-only because we synthesized our own ivar.
[self setExecuting:YES];
...
}
- (void)cancel {
//..
//super will change the properties executing/finished for us or we can do it manually
[super cancel];
...
}
#end
I think this is clearer than having
[self willChangeValueForKey:_NSURLOperationIsFinished];
[self setIsFinished:YES];
[self didChangeValueForKey:_NSURLOperationIsFinished];
First, you should not need to do manual KVO notifications. For an NSOperation subclass KVO notifications should be sent automatically unless your class has opted out of automatic KVO notifications by implementing +automaticallyNotifiesObserversForKey or +automaticallyNotifiesObserversOf<Key> to return NO.
NSOperation subclasses are not very useful unless they are added to an NSOperationQueue. When an operation is added to a queue, the queue uses KVO to observe the properties that indicate state changes, such as finished, executing, cancelled, etc. Note that these are not isFinished, isExecuting, or isCancelled - those are the names of the synthesized get accessors for those properties.
In your question you include this code:
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"]
finished = YES;
executing = NO;
[self didChangeValueForKey:#"isFinished"];
[self didChangeValueForKey:#"isExecuting"];
What you're doing here is sending manual KVO notifications for the get accessors, not the properties being observed. Instead, this would accomplish what you seem to be trying to do:
[self setFinished:YES];
[self setExecuting:NO];
Rather than accessing instance variables directly, use the accessor methods. This will correctly send automatic change notifications for these properties.
If you are truly paranoid about KVO and want to send notifications for the get accessor key paths such as isFinished, register your property as a dependency of the key path:
+ (NSSet *) keyPathsForValuesAffectingIsFinished {
NSSet *result = [NSSet setWithObject:#"finished"];
return result;
}
Registering dependencies is part of KVO compliance.

Objective-C concurrency processing

I've got class:
ClassX.m
#property (assign) BOOL wasProcessed;
-(void) methodA { //<- this can be called many times in short period of time
dispatch_async(dispatch_get_main_queue(), ^{
[self methodB];
});
}
- (void) methodB {
if (!self.wasProcessed) {
self.wasProcessed = YES;
//... some code
}
}
Since dispatch_async is used so a few calls to methodB can be processed concurrently at the same time and following code needs to be atomic:
if (!self.wasProcessed) {
self.wasProcessed = YES; //e.g two calls can enter here before setting YES and it would be bad because I want to process it only one time
How can those 2 lines be made atomic (checking and setting variable)? I dont want to make atomic code that is after "self.wasProcessed = YES;" so moving whole if to #synchronize(self) won't be good solution. If there is anything wrong with my thinking please point it out as I'm not very experienced in those topics, Thank you.
Try #synchronized. While the enclosed code is being executed on a thread, it will block other threads from executing it.
- (void) methodB {
#synchronized(self) {
if (!self.wasProcessed) {
self.wasProcessed = YES;
//... some code
}
}
}
-(void) methodA {
dispatch_async(dispatch_get_main_queue(), ^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^(){
[self methodB];
}];
});
}
Your's methodB will be only called in main thread, so it will be never performed simultaneously.

NSOperation running but missing from NSOperationQueue

I have recently been debugging a zombie issue with operations and found out that calling cancelAllOperations on the queue didn't cancel the operation in question, and in fact, the operation queue was empty even though the operation was still running.
The structure was a viewcontroller asynchronously loading a set of images off the web and perform some changes on them. Relevant (anonymised) exerpts follow:
#implementation MyViewController
- (id) init
{
(...)
mOperationQueue = [[NSOperationQueue alloc] init];
(...)
}
- (void) viewDidAppear:(BOOL)animated
{
(...)
MyNSOperation * operation = [[MyNSOperation alloc] initWithDelegate:self andData:data];
[mOperationQueue addOperation:operation];
[operation release];
(...)
}
- (void) dealloc
{
(...)
[mOperationQueue cancelAllOperations];
[mOperationQueue release];
(...)
}
- (void) imagesLoaded:(NSArray *)images
{
(...)
}
And the operation in question:
#implementation MyNSOperation
- (id) initWithDelegate:(id)delegate andData:(NSDictionary *)data
{
self = [super init];
if (self)
{
mDelegate = delegate; // weak reference
mData = [data retain];
(...)
}
return self;
}
- (void) main
{
NSAutoReleasePool * pool = [[NSAutoReleasePool alloc] init];
mImages = [[NSMutableArray alloc] init];
// load and compose images
mAlteredImages = (...)
[self performSelectorOnMainThread:#selector(operationCompleted) withObject:nil waitUntilDone:YES];
[pool release];
}
- (void)operationCompleted
{
if (![self isCancelled])
{
[mDelegate imagesLoaded:mAlteredImages];
}
}
The observed flow is as follows:
The viewcontroller is shown, calling init and viewDidAppear starting the operation.
[mOperationQueue operations] contains exactly one element;
Shortly after, the operation enters main and
The viewcontroller is exited by the user before the operation completes.
dealloc is called on the viewcontroller (because the operation keeps a weak reference)
[mOperationQueue operations] contains zero (!) elements
cancelAllOperations is sent to the operation queue
[NSOperation cancel] is not called, resulting in an app-visible bogus state.
dealloc finishes
the operation completes
isCancelled returns false, resulting in a zombie call
The documentation of NSOperationQueue however explicitly states that "Operations remain queued until they finish their task." which looks like a breach of contract.
I've fixed the crash by keeping a reference to the operation and manually sending cancel, but I would like to know why the original approach isn't working to prevent further problems. Can someone shed some light on this?
Thanks in advance.
cancelAllOperations does not cancel an already started operation. It only informs the operation about that fact and let the operation cancel themself, whenever it want. Thus, you can get a raise condition. Proceed with deallocacion, after you are sure that the operation is canceled.

Resources