Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS 7.0 and ARC: UITableView never deallocated after rows animation

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
like image 633
Denis Avatar asked Sep 20 '13 14:09

Denis


1 Answers

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.

like image 148
gabbayabb Avatar answered Nov 16 '22 00:11

gabbayabb