I have a very simple test app with ARC. One of the view controllers contains UITableView. After making row animations (insertRowsAtIndexPaths
or deleteRowsAtIndexPaths
) UITableView (and all cells) never deallocated. If I use reloadData
, it works fine. No problems on iOS 6, only iOS 7.0.
Any ideas how to fix this memory leak?
-(void)expand {
expanded = !expanded;
NSArray* paths = [NSArray arrayWithObjects:[NSIndexPath indexPathForRow:0 inSection:0], [NSIndexPath indexPathForRow:1 inSection:0],nil];
if (expanded) {
//[table_view reloadData];
[table_view insertRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationMiddle];
} else {
//[table_view reloadData];
[table_view deleteRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationMiddle];
}
}
-(int)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return expanded ? 2 : 0;
}
table_view is kind of class TableView (subclass of UITableView):
@implementation TableView
static int totalTableView;
- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style
{
if (self = [super initWithFrame:frame style:style]) {
totalTableView++;
NSLog(@"init tableView (%d)", totalTableView);
}
return self;
}
-(void)dealloc {
totalTableView--;
NSLog(@"dealloc tableView (%d)", totalTableView);
}
@end
Good news! Apple has fixed this bug as of iOS 7.0.3 (released today, Oct 22 2013).
I tested and can no longer reproduce the issue using the sample project @Joachim provided here when running iOS 7.0.3: https://github.com/jschuster/RadarSamples/tree/master/TableViewCellAnimationBug
I also cannot reproduce the issue under iOS 7.0.3 on one of the other apps I am developing, where the bug was causing problems.
It still may be wise to continue shipping any workarounds for a while, until the majority of users on iOS 7 have updated their devices to at least 7.0.3 (which may take a couple weeks). Well, that is assuming your workarounds are safe and tested!
Well, if you dig a little bit deeper (disable ARC, subclass tableview, override retain/release/dealloc methods then put logs/breakpoints on them), you'll find that something bad happens in an animation completion block which possibly causes the leak.
It looks like the tableview receives too many retains from a completion block after cell inserting/deleting on iOS 7, but not on iOS 6 (on iOS 6 UITableView has not yet been use block animations - you can check it too on the stack trace).
So I try to take over the tableview's animation completion block lifecycle from UIView in a dirty way: method swizzling. And this actually solves the problem.
But it does a lot more so I still looking for a more sophisticated solution.
So extend UIView:
As you can see the original completion block is not passed directly to the animateWithDuration: method and it is released correctly from the wrapper block (the lack of this causes leaks in tableviews). I know it looks a little bit strange but it solves the problem.
Now replace the original animation implementation with the new one in your App Delegate's didFinishLaunchingWithOptions: or wherever you want:
After that, all of the calls to
[UIView animateWithDuration:...]
leads to this modified implementation.I was debugging a memory leak in my application, which turned out to be this same leak, and eventually came to the exact same conclusion as @gabbayabb -- the completion block of the animation used by UITableView never gets freed, and it has a strong reference to the table view, meaning that never gets freed either. Mine happened with a simple
[tableView beginUpdates]; [tableView endUpdates];
pair of calls, with nothing in between. I did discover that disabling animations ([UIView setAnimationsEnabled:NO]...[UIView setAnimationsEnabled:YES]
) around the calls avoided the leak -- the block in that case is invoked directly by UIView, and it never gets copied to the heap, and therefore never creates a strong reference to the table view in the first place. If you don't really need the animation, that approach should work. If you need the animation though... either wait for Apple to fix it and live with the leak, or attempt to solve or mitigate the leak via swizzling some methods, such as the approach by @gabbayabb above.That approach works by wrapping the completion block with a very small one, and managing the references to the original completion block manually. I did confirm this works, and the original completion block gets freed up (and releases all of its strong references appropriately). The small wrapper block will still leak until Apple fixes their bug, but that does not retain any other objects so it will be a relatively small leak in comparison. The fact this approach works indicates that the problem is actually in the UIView code rather than the UITableView, but in testing I have not yet found that any of the other calls to this method leak their completion blocks -- it only seems to be the UITableView ones. Also, it appears that the UITableView animation has a bunch of nested animations (one for each section or row maybe), and each one has a reference to the table view. With my more involved fix below, I found we were forcibly disposing of about twelve leaked completion blocks (for a small table) for each call to begin/endUpdates.
A version of @gabbayabb's solution (but for ARC) would be:
This is basically identical to @gabbayabb 's solution, except it is done with ARC in mind, and avoids doing any extra work if the passed-in completion is nil to begin with or if animations are disabled. That should be safe, and while it does not completely solve the leak, it drastically reduces the impact.
If you want to try to eliminate the leak of the wrapper blocks, something like the following should work:
This approach is a little bit more dangerous. It is the same as the previous solution, except it adds a small object which holds a weak reference to the wrapper block, waits 10 seconds after the animation finishes, and if that wrapper block has not been dealloced yet (which it normally should), assumes it is leaked and forces an additional autorelease call on it. The main danger is if that assumption is incorrect, and the completion block somehow really does have a valid reference elsewhere, we could be causing a crash. It seems very unlikely though, since we won't start the timer until after the original completion block has been called (meaning the animation is done), and the completion blocks really should not survive much longer than that (and nothing other than the UIView mechanism should have a reference to it). There is a slight risk, but it seems low, and this does completely get rid of the leak.
With some additional testing, I looked at the UIViewAnimationOptions value for each of the calls. When called by UITableView, the options value is 0x404, and for all of the nested animations it is 0x44. 0x44 is basically UIViewAnimationOptionBeginFromCurrentState| UIViewAnimationOptionOverrideInheritedCurve and seems OK -- I see lots of other animations go through with that same options value and not leak their completion blocks. 0x404 however... also has UIViewAnimationOptionBeginFromCurrentState set, but the 0x400 value is equivalent to (1 << 10), and the documented options only go up to (1 << 9) in the UIView.h header. So UITableView appears to be using an undocumented UIViewAnimationOption, and the handling of that option in UIView causes the completion block (plus the completion block of all nested animations) to be leaked. That leads itself to another possible solution:
This approach simply eliminates the undocumented option bit and forwards on to the real UIView method. And this does seem to work -- the UITableView does go away, meaning the completion block is dealloced, including all nested animation completion blocks. I have no idea what the option does, but in light testing things seem to work OK without it. It's always possible that option value is vitally important in a way that's not immediately obvious, which is the risk with this approach. This fix is also not "safe" in the sense that if Apple fixes their bug, it will take an application update to get the undocumented option restored to table view animations. But it does avoid the leak.
Basically though... let's hope Apple fixes this bug sooner rather than later.
(Small update: Made one edit to explicitly call [wrapperBlock copy] in the first example -- seems like ARC did not do that for us in a Release build and so it crashed, while it worked in a Debug build.)