SOLVED (I think): Set ASIHTTPRequest delegate/NSOperationQueue delegate to nil after the vc is popped (in the dealloc).
Before I start, this question is similar but not the same.
My app is very near release, however this is the only persistent bug which has existed throughout development. It is also the only bug which has occured when demoing the app to somebody, and because this bug results in termination of the app, it's highly embarassing. If someone can find a fix for this (for good), I will award a decent-sized bounty.
Start of question:
Basically, this bug occurs when the ASIHTTPRequest tries to call the requestFinished
selector, onto an object which is a zombie.
From looking around, it appears that what ASIHTTPRequest, when faced with sending a selector to a zombie, will send it to a similar object that isn't a zombie. I've heard it's quite common to have it send the selector to the CALayer of the controller it was meant to send to, and also with me it's sent to my UINavigationButton!? - Anyhow, the effect of this is that it throws an exception.
The problem occurs inside the ASIHTTPRequest class when it tries to call the 'requestFinished' selector:
[queue performSelector:@selector(requestFinished:) withObject:self];
That line of code is the one it fails on - what puzzles me is why? On the question I linked to, the OP said that they fixed it by calling [self retain]
from the view controller. Surely that isn't right?
Structure
The way my ASIHTTPRequest's work, is they are added to a downloadQueue, (NSOperationQueue), which resides within my app delegate. The structure/style of my app is mainly based on this sample code, which does a very similar thing but with the flickr RSS feed.
The section of my app that uses ASIHTTPRequest, is a webcam viewer. It has a navigation controller with a table view in, that displays three webcams. These images are JPEGs and updated every 30 seconds to a static file (on a remote server). The cells within this table view are custom, and each load their own ASIHTTPRequest. The cells download their respective images, and display them.
Here is the method I created which is called on each of my cells:
- (void)loadURL:(NSURL *)url {
ASIHTTPRequest *request = [[ASIHTTPRequest alloc] initWithURL:url];
[request setDelegate:self];
[request setDidFinishSelector:@selector(requestDone:)];
[request setDidFailSelector:@selector(requestWentWrong:)];
NSOperationQueue *queue = [[MyAppDelegate sharedAppDelegate] downloadQueue];
[queue addOperation:request];
[spinner startAnimating];
[self addSubview:spinner];
[request release];
}
When one of these cells is selected, it takes the user to a detail view, where another instance of ASIHTTPRequest is loaded, which downloads the image for the selected webcam.
How the bug is triggered
Under normal "calm" usage of my app, it behaves fine. The problem occurs when rapid pushing and popping occurs between these views (between the root and detail webcam views). It most often occurs if you enter a detail view, the progress bar begins moving, and you pop to the root again. This leads me to believe that because the controller is not active, when the ASIHTTPRequest finishes, and it tries to call the controller, it fails.
Crash log
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Codes: KERN_PROTECTION_FAILURE at 0x00000480
Crashed Thread: 0
Thread 0 Crashed:
0 libobjc.A.dylib 0x30183f24 objc_msgSend + 24
1 iCompton 0x0003c4d4 0x1000 + 242900
2 CoreFoundation 0x35ea3f72 -[NSObject(NSObject) performSelector:withObject:] + 18
3 iCompton 0x00029082 0x1000 + 163970
4 CoreFoundation 0x35ea3f72 -[NSObject(NSObject) performSelector:withObject:] + 18
5 Foundation 0x33fd3e66 __NSThreadPerformPerform + 266
6 CoreFoundation 0x35ebc8ca __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 8
7 CoreFoundation 0x35e8cec6 __CFRunLoopDoSources0 + 378
8 CoreFoundation 0x35e8c6f2 __CFRunLoopRun + 258
9 CoreFoundation 0x35e8c504 CFRunLoopRunSpecific + 220
10 CoreFoundation 0x35e8c412 CFRunLoopRunInMode + 54
11 GraphicsServices 0x35261d1c GSEventRunModal + 188
12 UIKit 0x33865574 -[UIApplication _run] + 580
13 UIKit 0x33862550 UIApplicationMain + 964
14 iCompton 0x00002b92 0x1000 + 7058
15 iCompton 0x00002b44 0x1000 + 6980
It most often occurs if you enter a detail view, the progress bar begins moving, and you pop to the root again. This leads me to believe that because the controller is not active, when the ASIHTTPRequest finishes, and it tries to call the controller, it fails.
I think you are right on track on this.
What you should try to do is cancelling the ASIHTTPRequest when you pop to the root controller (or to whatever other controller). Before you cancel the request, you should set its delegate
to nil; or you can use clearDelegatesAndCancel
.
Possibly this will require you to keep a reference to the request in your view controller, but that should be no problem.
An interesting S.O. question/answer about detecting UIViewController being popped is here.
You should not be using NSZombieEnabled in live apps, the memory is never released. You need to turn it off when not in development. The rapid pushing and pulling is probably exhausting the memory, triggering memory warnings, and causing things to be released and dealloc'd. We won't know for sure though until you post the crash log.
Here's how I solved this problem: set the delegate
of any outstanding ASIHTTPRequest
s to nil
in your viewController's dealloc
method.
This prevented the outstanding ASIHTTPRequest
s from sending messages to the didFinishSelector
s, didFailSelector
s etc on the viewController after the viewController was popped off the navigation stack and dealloc
'd.
Keep in mind: the ASIHTTPRequest
method cancelAllOperations
possibly calls the didFailSelector
asynchronously. In this case the selector might be called on the viewController which no longer exists.
User contributions licensed under CC BY-SA 3.0