I've written a asynchronous call that has a completion block to return a UIImage, then in the completion block the view controller sets a UIImageView to use that image. My question is what happens if that view controller is popped off UINavigationController stack and is no longer alive before the completion block executes?
[MyAPI getImage:imageID completionBlock:^(MyAPIStatus status, id result) {
if (status == kSuccessful) {
self.ImageView.image = [UIImage imageWithData:result];
}
}];
Because the block passed to your API captures(retains) self, self will be alive.
So if you use retain/release properly or use ARC, this is harmless.
Most likely EXC_BAD_ACCESS. Your API should probably have another method to cancel the asynchronous task that you can call when the view controller is deallocated.
Related
For the life of me, I cannot figure out what's going on here.
As an overview, I have an application that I've created a custom navigation bar, a custom containment view controller, and callbacks to tell me when expensive processes have been finished executing inside individual view controllers (such as a large set of images).
Inside my containment view controller when navigating to a child view controller, a call back is set on the child view controller to call the transition animation after it has finished it's set of expensive processes.
The callbacks are created as such
#interface UIViewController (CallBack)
typedef void (^CompletionBlock)(void);
#property (nonatomic, copy) CompletionBlock callBackBlock;
- (void)doneLoadingImages;
#end
static char const *const CompletionBlockTagKey = "CompletionBlockTag";
#implementation UIViewController (CallBack)
#dynamic callBackBlock;
- (CompletionBlock)callBackBlock
{
return objc_getAssociatedObject(self, CompletionBlockTagKey);
}
- (void)setCallBackBlock:(CompletionBlock)callBackBlock
{
objc_setAssociatedObject(self, CompletionBlockTagKey, callBackBlock, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)doneLoadingImages
{
[self callBackBlock]();
self.callBackBlock = nil;
}
#end
These callbacks are set before the child view controller is added through addChildViewcontroller:, and are fired in a dispatch_async block such as this
__block __weak ThisUIViewController *me = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image1 = [UIImage imageForNibFromFileName:#"picture_name"];
dispatch_async(dispatch_get_main_queue(), ^{
me.imageViewToSet.image = image1;
[me doneLoadingImages];
});
});
This process goes through fine the first time a UIViewcontroller gets called from my container view controller through my navigation code. According to the profiler, it dumps memory properly as well after I navigate away from it indicating my retain count is 0 for it.
BUT, what happens when I navigate to the same UIViewcontroller again is that the images are loaded super fast, and the doneLoadingImages gets called super fast as well, causing hang ups on my main thread and causing the UI to become unresponsive until everything has been set UI wise.
My imageForNibFromFileName: method is just a convenience category method that uses imageForContentsOfFile: internally. Anybody have some insight on what I may be doing wrong here?
Turns out it wasn't a retain issue. I'm not exactly sure why, but I had to separate the images into their own dispatch_async call from the global_queue instead of chaining them all in one async block. If anybody has an explanation of why this has to be done or what's going on in the background, that would be appreciated.
I have a really light ViewController, it does nothing in viewDidLoad. I'm pushing this view on top of a navigationController. The method who does this action is called from inside a block. After the call to showView I added an NSLog, and that log prints in the console really fast, but the view takes a lot to load... I really don't understand what maybe happening... any idea???
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) {
[self showView];
NSLog(#"EXECUTED");
});
- (void) showView{
TestViewController *test = [[TestViewController alloc]init];
[self.navigationController pushViewController:test animated:NO];
}
From the docs for ABAddressBookRequestAccessWithCompletion:
The completion handler is called on an arbitrary queue. If your app uses an address book throughout the app, you are responsible for ensuring that all usage of that address book is dispatched to a single queue to ensure correct thread-safe operation.
You should make sure your UI code is called on the main thread.
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error {
dispatch_async(dispatch_get_main_queue(), ^{
[self showView];
NSLog(#"EXECUTED");
});
});
This might not be the only problem, but according to the docs, the completion handler passed to ABAddressBookRequestAccessWithCompletion is called on an arbitrary queue. -showView should only be called on the main queue since it is dealing with UIViewController objects.
The other thing to ask is what else is happening on the main queue? Are there other long-running tasks that could be blocking UI updates?
My UIViewController is getting deallocated in the middle of a delegate callback. Here's what happens:
UIWebView begins loading
User presses cancel
UIWebView begins sliding out
The request finishes, calls the didFinish handler
IN THE MIDDLE of the didFinish handler (like right between two lines of code) the viewcontroller runs dealloc
Everything is deallocated, delegates cleared, web requests stopped
The handler resumes in a deallocated state, causing a BAD_ACCESS exception
I've checked - everything is running on the main thread.
How do I make sure dealloc isn't called in the middle of my handler?
Side question - how is this not a problem with all delegates? This terrifies me.
How do I make sure dealloc isn't called in the middle of my handler?
Don't release it in the middle of that callback. I would guess you're setting self.myWebView = nil, that releases the reference (unless it's declared assign), and if that is the end of its life cycle, i.e. would drop the retainCount to 0, it is deallocated immediately, in the middle of the callback. You can get around this by retaining the web view at the beginning of the method (or at least before you nil it) and autoreleaseing just before returning from the callback. The autorelease will wait until the callback finishes before processing the release. Or you can just not set that property to nil until some later time when you know it's safe.
Check if your webview is nil before releasing it.
if(_webView !=nil){
if (_webView.isLoading) {
[_webView stopLoading];
}
[_webView removeFromSuperview];
_webView.delegate = nil;
[_webView release];
_webView = nil;
}
Because there are multiple situations in which I would want to pop a view controller from the navigation stack, I have one method that does it and it is called from three different places.
- (void)dismissSelfCon {
NSLog(#"dismiss");
[locationManager stopUpdatingHeading];
[locationManager stopUpdatingLocation];
locationManager.delegate = nil;
mapView.delegate = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[[Trail_TrackerAppDelegate appDelegate] navCon] popViewControllerAnimated:YES];
}
In one situation, if the mapView has an annotation placed on it (I'm not sure if that is the defining characteristic, but I think it is), this method is called (and I am sure that it is called because #"dismiss" is printed to the console), but the location manager does not stop sending location updates! Also, because the delegate is not set to nil, the app crashes because the view controller receives respondsToSelector: from one of the objects of which it is a delegate.
How is this possible?
The most likely cause of this is that locationManager at this point is nil. First rule: always use accessors; don't directly access your ivars except in init and deallloc.
My suspicion from your description would be that this object (the one with dismissSelfCon) doesn't clear locationManager.delegate during dealloc, and that you're being deallocated without calling dismissSelfCon.
The solution was this:
The way I have my view controller set up (which is a little strange I know, and is something I'm trying to change/fix if you will see my question here: Can't allocate CLLocationManager), the CLLocationManager is being allocated, delegate set, etc in viewDidAppear. I present a MFMessageComposeViewController during the app, and when it gets dismissed, viewDidAppear is called again, re-allocating the CLLocationManager and causing my problem. With a little boolean magic, I adjusted the viewDidAppear code so that the CLLocationManager is only set up and allocated one time.
This is by far the weirdest problem I've been stuck with.
I have a UIViewController on a UINavigationController and I want to call a method at viewDidAppear using NSInvocationOperation so it can run on a back thread when the view becomes visible.
The problem is that if I pop the view controller BEFORE the operation (in this case the testMethod method) completes running, the app crashes.
Everything works fine if I pop the view controller AFTER the operation runs it's course.
When the app crashes, it stops at [super dealloc] with "EXC-BAD-ACCESS" and gives me the following error.
bool _WebTryThreadLock(bool), xxxxxxxxx: Tried to obtain the web lock
from a thread other than the main thread or the web thread. This may
be a result of calling to UIKit from a secondary thread. Crashing
now...
And this is my code (super simplified)..
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSInvocationOperation *theOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(testMethod) object:nil];
[operationQueue addOperation:theOperation];
[theOperation release];
}
- (void)testMethod
{
NSLog(#"do some stuff that takes a few seconds to complete");
}
- (void)dealloc
{
[_tableView release];
[super dealloc];
}
The testMethod has some code that takes a few seconds to complete. I only have a few clues and I really don't know how and where to start debugging this.
Clue #1: The funniest thing is that if I remove the [_tableView release]; from dealloc then the app doesn't crash. But of course this would cause a leak and I can't remove it.
Clue #2: I've tested this code on a separate "clean" UIViewController with a UITableView and to my surprise it didn't crash.
Clue #3: The app doesn't crash is the UITableView's datasource is set to nil in viewDidLoad.
Clue #4: The app doesn't seem crash if I use the same code in viewDidAppear somewhere else like an IBAction.
Clue #5: I've tried looking over stack data with NSZombie but it gives me tons of data and it leads me nowhere.
I have some very complicated code within my UITableViewDelegate and UITableViewDataSource and I really don't know where to start debugging this. I really hope I don't have to go through line by line or rewrite the entire thing because of this.
Any pointers on where I should be looking?
The problem is likely that your view controller's last reference is the operation queue holding onto it, which means you are technically calling (or having the system call) some UIKit methods in a background thread (a big no-no) when the operation cleans up.
To prevent this from happening, you need to send a keep-alive message to your controller on the main thread at the end of your operation, by adding something like this to the last line in your testMethod:
[self performSelectorOnMainThread:#selector(description) withObject:nil waitUntilDone:NO];
There is still a chance that this message may get processed before the operation queue releases your view controller, but that chance is pretty remote. If it's still happening, you could do something like this:
[self performSelectorOnMainThread:#selector(keepAlive:)
withObject:[NSNumber numberWithBool:YES]
waitUntilDone:NO];
- (void)keepAlive:(NSNumber *)fromBackground
{
if (fromBackground)
[self performSelector:#selector(keepAlive:) withObject:nil afterDelay:1];
}
By sending a message to your view controller on the main thread, it will keep the object alive (NSObject retains your view controller until the main thread handles the message). It will also keep the view controller alive if you perform a selector after a delay.
You're crashing because the controller is still trying to use your tableView reference and since you poped the viewController, everything will go away in the dealloc and the tableView is still populating itself.
You can try asking in your dealloc method if your operation is still running, so you can cancel it and the everything should be fine.
Once you add an operation to a queue, the operation is out of your
hands. The queue takes over and handles the scheduling of that task.
However, if you decide later that you do not want to execute the
operation after all—because the user pressed a cancel button in a
progress panel or quit the application, for example—you can cancel the
operation to prevent it from consuming CPU time needlessly. You do
this by calling the cancel method of the operation object itself or by
calling the cancelAllOperations method of the NSOperationQueue class.
Cancelling an operation does not immediately force it to stop what it
is doing. Although respecting the value returned by the isCancelled is
expected of all operations, your code must explicitly check the value
returned by this method and abort as needed. The default
implementation of NSOperation does include checks for cancellation.
For example, if you cancel an operation before its start method is
called, the start method exits without starting the task.