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
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:
@interface UIView (iOS7UITableViewLeak)
+ (void)fixed_animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion;
+ (void)swizzleStaticSelector:(SEL)selOrig withSelector:(SEL)selNew;
@end
#import <objc/runtime.h>
typedef void (^CompletionBlock)(BOOL finished);
@implementation UIView (iOS7UITableViewLeak)
+ (void)fixed_animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion {
__block CompletionBlock completionBlock = [completion copy];
[UIView fixed_animateWithDuration:duration delay:delay options:options animations:animations completion:^(BOOL finished) {
if (completionBlock) completionBlock(finished);
[completionBlock autorelease];
}];
}
+ (void)swizzleStaticSelector:(SEL)selOrig withSelector:(SEL)selNew {
Method origMethod = class_getClassMethod([self class], selOrig);
Method newMethod = class_getClassMethod([self class], selNew);
method_exchangeImplementations(origMethod, newMethod);
}
@end
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:
[UIView swizzleStaticSelector:@selector(animateWithDuration:delay:options:animations:completion:) withSelector:@selector(fixed_animateWithDuration:delay:options:animations:completion:)];
After that, all of the calls to [UIView animateWithDuration:...]
leads to this modified implementation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With