I'm at a loss! It's one of those pesky bugs that happens only under specific conditions, yet I cannot link the conditions and the results directly.
My app has a paged UIScrollView where each page's view comes from a MyViewController, a subclass of UITableViewController. To minimize memory usage I unload those controllers that are not currently visible. Here is my "cleaning" method:
- (void) cleanViewControllers:(BOOL)all {
if (all) {
// called if some major changes occurred and ALL controllers need to be cleared
for (NSInteger i = 0; i < [viewControllers count]; i++)
[viewControllers replaceObjectAtIndex:i withObject:[NSNull null]];
}
else if ([viewControllers count] > 2) {
// called if only the nearest, no longer visible controller need to be cleared
NSInteger i = pageControl.currentPage - 2;
if (i > -1) [viewControllers replaceObjectAtIndex:i withObject:[NSNull null]];
i = pageControl.currentPage + 2;
if (i < [viewControllers count]) [viewControllers replaceObjectAtIndex:i withObject:[NSNull null]];
}
}
It's this line that crashes the app:
viewControllers replaceObjectAtIndex:i withObject:[NSNull null]];
viewControllers is an NSMutableArray containing objects of type MyViewController. MyViewController has no custom properties and its dealloc method contains nothing but a [super dealloc] call.
Here is what the debugger shows:
alt text http://a.imageshack.us/img831/3610/screenshot20100806at126.png
The thing is that this does not happen every time the controller is cleared, but only sometimes. Specifically, after certain changes trigger a complete cleaning and re-drawing of the ScrollView, it displays the current page (call it X) fine, but as soon as I scroll far enough to cause cleaning of X, this crash happens. It's driving me nuts!
Another thing, this does not happen in the 4.0 Simulator, nor on an iPad, but happens very consistently on a 1st gen iPod touch running 3.1.3.
Something is being released but a pointer is still dangling. Set NSZombieEnabled to YES and run it again. Here's how:
Product -> Edit Scheme
Select the "Arguments" tab
Add to "Variables to be set in the environment"
Name: NSZombieEnabled
Value: YES
In Xcode 4.1 and above:
Product -> Edit Scheme
Select the "Diagnostics" tab
You have an option to enable zombie objects.
Run the app again. At some point it'll tell you you're accessing an already released object. From that you'll need to figure out who's not retaining or who's over-releasing the object.
Happy Zombie Hunting.
Often this can be caused by allocing something then setting it to something within the ViewController then releasing it, for example:
UIBarButtonItem* timeLabel = [[UIBarButtonItem alloc] initWithTitle:#"time" style:UIBarButtonItemStylePlain target:nil action:nil];
NSArray *items = [NSArray arrayWithObjects: timeLabel, nil];
self.toolbarItems = items;
Now the natural thing to do after this is :
[timeLabel release];
But this will cause EXC_BAD_ACCESS on [super dealloc], presumably because the view controller releases the array AND all the items within it.
View controllers will automatically unload their views on a memory warning by default, so there's no reason to unload the controllers themselves unless they include significant overhead.
Is the view controller's view still in the view hierarchy? You can check with something like [viewController isViewLoaded] && viewController.view.superview. If so, it's probably not safe to remove the view controller.
(Note the isViewLoaded check, since UIViewController.view will load the view if it's not already loaded.)
Instead of replacing the elements in the array you can simply call removeObjectAtIndex: or removeAllObjects: to make sure there is nothing holding reference where it shouldn't.
Related
Basically I have an app that has introductory views. Once you reach a certain view, the previous views are no longer accessible at all, so I want to remove them from the stack and free any memory they have consumed. What is the best way to do this? Right now I am doing something like
NSMutableArray *allViewControllers = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
NSArray *allControllersCopy2 = [allViewControllers copy];
for (id object in allControllersCopy2) {
if([allControllersCopy2 indexOfObject:object] == ([allControllersCopy2 count] - 1)){
NSLog(#"IGNORE CURRENT VIEW");
}
else{
[allViewControllers removeObject:object];
[object release];
}
}
self.navigationController.viewControllers = allViewControllers;
[allControllersCopy2 release];
Does this actually release the memory consumed by these views? or does it simply remove the view from the stack array?
When you are ready to push "that certain view", use setViewControllers:animated: to replace the stack with your final ViewController, instead of using pushViewController to add it to the stack.
assuming controller = 'that certain view'...
don't do [self.navigation pushViewController:controller animated:YES], do:
[self.navigationController setViewControllers:[NSArray arrayWithObject:controller] animated:YES];
this will release all the previous View Controllers, their dealloc methods will get called, memory release, etc
The Instruments tells me that this piece of code has a leak. However, I am pretty sure I have released it later on. Can anyone tell me what's going on here?
- (void) addReminderEntry{
DataEntryController* item = [[DataEntryController alloc] initWithEntryType:REMINDER]; // it says that the leak was instantiated here
item.delegate = self;
[[self navigationController] pushViewController:item animated:YES];
[item setEditing:YES animated:YES];
[item release];// this is the place I release it
}
Thanks
More than likely it has to do with something not being released within the DataEntryController class. Make sure you are releasing all your properties/etc within that class.
Leaks tells you only where memory was allocated, what it cannot tell you is where to put the code that should have released it to start with!
So this is saying that you made a view controller, and it was still around in memory after you finished with it. Yes you release the VC in that code, but only after you present it - which means the navigation controller has retained it, and possibly other things. It only gets deallocated when the final release is called.
The main culprit for view controllers not being released is usually having the view controller set itself as a delegate for something it retains, and then not undoing that when the view controller goes offscreen. If your view controller is a delegate of something that retains it, it's never going to be deallocated.
It turns out that this is caused by this constructor:
- (DataEntryController*) initWithEntryType:(DataType) eType{
DataEntryController* item = [[DataEntryController alloc] init];//<- here
item.entryType = eType;
item.allowEdit = YES;
return item;
}
Apparently iOS adds retain 1 to each constructor with an initial 'init'.
It works fine after switching to:
DataEntryController* item = [super init];
I have a DetailsViewController class and an ItemsViewController class. (Both derived from UITableViewController)
Selecting any of the items in the ItemsViewController brings up the DetailsViewController. In order to get it to show the new data on any but the first one, I currently have
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[[self navigationItem] setTitle:title];
[[self tableView] reloadData];
}
This works, however it feels like killing a fly with a sledgehammer. What is a better way to do this?
Thanks in advance,
Alan
Combining ideas from several comments here:
Adding BOOL needReload as a member variable to the Details Controller.
Then in the details controller:
- (void)setData:(DataClass *)value {
if (value == data)
return;
id pointer = data;
data = [value retain];
[pointer release]; // release after retain
needReload = TRUE;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(needReload){
[[self navigationItem] setTitle:title];
[[self tableView] reloadData];
needReload = FALSE;
}
}
If you know that only specific rows or sections will be changing, you can direct the bake view to reload only those rows or sections. Other than that, -reloadData is the way to go for most table views.
I assume the items on the detail table changes depending on the selected item on the items table. So, yeah, this should be alright.
Other than that, you can check if the same item is selected the last time and not call reloadData during that case.
Alan,
Your statement of "In order to get it to show the new data on any but the first one" concerns me - because it tells me that you likely have a single DetailsViewController instance.
In your first table view, ItemsViewController, you probably have a didSelectRowAtIndexPath: method that you're using to push the DetailsViewController onto the UINavigationController stack.
How I solve this issue is simply creating/destroying a new DetailsViewController every time my user taps between views. So, my didSelectRowAtIndexPath: often looks like:
- (void) didSelectRowAtIndexPath:(NSIndexPath*)indexPath
{
NSInteger selectedRow = indexPath.row;
// Create a new view controller
DetailsViewController *tmpVC = [[DetailsViewController alloc] initWithNibName:#"foo" bundle:nil];
// Tell our new view controller what data it should be using
tmpVC.tableData = [self.someArrayOfData objectAtIndex:selectedRow];
// Push view controller and release it
[self.navigationController pushViewController:tmpVC animated:YES];
[tmpVC release];
}
This example assumes that you have all the data necessary for both view controllers in your ItemsViewController - that may not be the case..?
Anyway, by doing it this way, your DetailsViewController automatically loads the data. When you tap "Back" to go back to ItemsViewController, the UINavigationController would release it, destroying it. Then, when the user taps a different cell, we run this code again, creating a brand-new controller with brand-new data - so of course when it displays, it will load the data automatically - it's never displayed before.
What it sounds like you may be doing in your code is retaining the DetailsViewController as a property of the ItemsViewController class and then reusing the object. This can also work as well if you're concerned about allocations (for example, if it is a very "heavy" allocation to make a DetailsViewController), but then I think the best place to call reloadData is not inside the class itself - but rather from the didSelectRowAtIndexPath: method of ItemsViewController.
The reason I promote the creation/destruction approach as opposed to the "flyweight pattern" approach is that it keeps your code more separate - the fewer linkages between view controllers, the better. Of course, ItemsViewController will always dependo on and know about DetailsViewController, but it shouldn't necessarily have to be the other way around - and if you add the reloadData call to viewWillAppear:animated:, you're implicitly adding a non-code dependency between the two. You know that when ItemsViewController is the "parent" in the navigation stack, that's the right behavior -- but what if you suddenly started reusing that view in other part of your app that doesn't require a reload? It's a performance hit for one, and moreover, it's the kind of hidden dependency that may end up in a nasty-to-trace bug someday. So, I'd keep Details stupid and make Items contain all the complexity, if it is indeed required to only have 1 DetailsViewController (as opposed to my first idea of recreating it each time).
I would propose the reloadData and setTitle to be in the viewDidLoad and in the setter - I assume you set a property in DetailsViewController that changes the datasource of the table. So viewDidLoad reloads and sets the title, if the property has been set, the setter reloads and sets the title if isViewLoaded and the new value is different than the old one.
- (void)setSmth:(SmthClass *)value {
if (value == smth) // if they are the same and SmthClass is immutable,
// otherwise use isEqual and [self.tableView reloadData]
// before returning...
return;
id pointer = smth; // if it's a retain property
smth = [value retain];
[pointer release]; // release after retain just to be extra safe
if ([self isViewLoaded]) {
[self.tableView reloadData];
[self setTitle:title];
}
}
- (void)viewDidLoad {
if (smth) {
[self.tableView reloadData]; // maybe redundant...
[self setTitle:title];
}
}
Or you can use Key-Value observing (NSKeyValueObserving protocol) to observe your property and reloadData on notification...
I have a very strange problem with UINavigationController on the iphone and I am banging my head against the wall on this.
Gist of it is I am executing a call to a server and when that call fails I swap out the current view with a view containing an error message. The code in question is called on the main thread by using performSelectorOnMainThread
What happens in practice is that on the device it shows a blank white screen about half the time. On the simulator it presents a blank screen every time leading me to think this is perhaps some kind of timing problem that is more prominent due to better processing speeds in a simulator. This works perfectly if I call the same function by clicking a button in the ui to display the page so I don't think its a problem with the code itself.
I have verified that the controller I am adding is in the navigation stack. Verified it is being called on the main thread, it is visible, the frame size and location are correct. I have tried explicitly setting the view to be visible, moved it to the front in its parent view and called setNeedsDisplay and even manually called drawRect. None of this works.
Any thoughts on what could be going on here? I am assuming it has something to do with the run loop but I can't figure it out. Help would be much appreciated. The relatively simple code in question is below
UINavigationController* navController = self.navigationController;
int count = [navController.viewControllers count];
NSMutableArray* controllers = [[NSMutableArray alloc] initWithCapacity:count];
for (int i=0; i<count; i++) {
if (self == [self.navigationController.viewControllers objectAtIndex:i]) {
[controllers addObject:newController];
}
else {
[controllers addObject:[self.navigationController.viewControllers objectAtIndex:i]];
}
}
[self.navigationController setViewControllers:controllers animated:YES];
[controllers release];
I really don't understand what you're doing there. Something like this won't work?
- (void)displayMyErrorVC {
MyErrorVC *errorVC = [[[MyErrorVC alloc] init] autorelease];
[self.navigationController pushViewController:errorVC animated:YES];
}
And then in your other thread, if you have an error:
[self performSelectorOnMainThread:#selector(displayMyErrorVC) withObject:nil waitUntilDone:NO];
- (void)launchSearch
{
EventsSearchViewController *searchController = [[EventsSearchViewController alloc] initWithNibName:#"EventsSearchView" bundle:nil];
[self.navigationController pushViewController:searchController animated:YES];
//[searchController release];
}
Notice the [searchController release] is commented out. I've understood that pushing searchController onto the navigation controller retains it, and I should release it from my code. I did just alloc/init it, after all, and if I don't free it, it'll leak.
With that line commented out, navigation works great. With it NOT commented out, I can navigate INTO this view okay, but coming back UP a level crashes with a *** -[CFArray release]: message sent to deallocated instance 0x443a9e0 error.
What's happening here? Is the NavigationController releasing it for me somehow when it goes out of view?
The boilerplate that comes on a UINavigationController template in XCode has the newly-pushed controller getting released. But when I do it, it fails.
---EDIT----
So this morning, I sit down, and it works. No real clue why. Sigh.
Taking what I thought I learned, then, and applying it to another piece of this same controller, I did the following. Yesterday I had this code WITHOUT the release statements, because it didn't work right with them. So this morning I added them to create:
- (IBAction)switchView:(id)sender
{
UISegmentedControl *seg = (UISegmentedControl *)sender;
NSInteger choice = [seg selectedSegmentIndex];
NSArray *array = [mainView subviews];
UIView *oldView = [array objectAtIndex:0];
[oldView removeFromSuperview];
if (choice == 0) {
tableController = [[EventsTableViewController alloc]
initWithNibName:#"EventsTableView" bundle:nil];
[mainView addSubview:tableController.view];
[tableController release];
}
if (choice == 1) {
calendarController = [[EventsCalendarViewController alloc]
initWithNibName:#"EventsCalendarView" bundle:nil];
[mainView addSubview:calendarController.view];
[calendarController release];
}
if (choice == 2) {
mapController = [[EventsMapViewController alloc]
initWithNibName:#"EventsMapView" bundle:nil];
[mainView addSubview:mapController.view];
[mapController release];
}
}
With it set up like this, when I come onto the view, the main portal of my view is filled with the EventsTableViewController's view, I can click to mapView and calendarView, but when I go BACK to tableView, I die because the table delegate methods are being called on a deallocated instance.
So I went and made all of these controllers into synthesized properties, so I can release them in [dealloc]. Which seems to work, but the real question is why adding these views as subviews doesn't retain them, passing ownership to the new view it's a member of, allowing me to release them right there?
Wow, guys. Thanks so much for all your responses--tragically I sent you all on a horrible goosechase.
My NavigationView navigates a NSArray of Event objects (local arts events). My table view drills down to a detail view.
My detail view has in it the following:
-(void)loadEvent:(Event *)event
{
thisEvent = event;
}
And I call that from my table view before pushing the detail view onto the nav stack. thisEvent is a synthesized property of type Event, and so since it's synthesized, I dutifully release'd it in [dealloc].
Many of you already see the problem. Backing up to the table view, when I scroll such that the one I just saw is displayed, it builds the custom table row, and so it goes to get the title property from the Event.... which I just released inside the detail controller. Boom.
I added a retain to that loadEvent: method above and the crashes, they are gone.
NONE of this was really about the views getting retained and released by the navcontroller. It was about accidentally over-releasing the data objects I'm navigating. Part of what had me discover this was, I NSLogged myself in the [dealloc] of each of these view controllers, and I can now see they're behaving exactly as they should.
Thanks! I SO love this site.
I'd guess the fault lies in EventsSearchViewController's init. Is it returning an autoreleased self by mistake ?
Looks like EventsSearchViewController is allocating an array and then over-releasing it, with one of the releases probably in its dealloc.
If you comment out the release, your EventsSearchViewController is never deallocated (it leaks). So, errors that occur as a result of its own dealloc will be masked since that method won't be called. Releasing the controller is the right thing, but you have another bug in the controller itself that only appears at dealloc time.
It could also be that dealloc is releasing an autoreleased array, so you may not have two explicit release calls in your code. But it looks very much like releasing something in dealloc that's causing the problem.