iOS 7.0和ARC:UITableView从未在行animation之后解除分配

我有一个非常简单的testing应用程序与ARC。 其中一个视图控制器包含UITableView。 在做animation之后( insertRowsAtIndexPaths或者deleteRowsAtIndexPaths ),UITableView(和所有的单元格)永远不会被释放。 如果我使用reloadData ,它工作正常。 iOS 6没有问题,只有iOS 7.0。 任何想法如何解决这个内存泄漏?

 -(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是一种类TableView(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 

那么,如果你深入挖掘(禁用ARC,子类tableview,覆盖保留/释放/ dealloc方法,然后把日志/断点放在它们上),你会发现一个坏的事情发生在animation完成块可能导致泄漏。
看起来,在iOS 7上插入/删除单元格之后,tableview从完成块中收到了太多的保留,但在iOS 6上却没有(在iOS 6上)UITableView尚未使用块animation – 您也可以在堆栈轨迹上检查它) 。

所以我尝试从UIView以一种肮脏的方式来接pipetableview的animation完成块生命周期:方法调整。 这实际上解决了这个问题。
但它还有很多,所以我仍然在寻找更复杂的解决scheme。

所以扩展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 

正如你所看到的,原始的完成块不会直接传递给animateWithDuration:方法,并且它会正确地从包装块中释放(缺less这会导致tableview泄漏)。 我知道它看起来有点奇怪,但它解决了这个问题。

现在用App Delegate的didFinishLaunchingWithOptions中的新代码replace原来的animation实现或者你想要的任何地方:

 [UIView swizzleStaticSelector:@selector(animateWithDuration:delay:options:animations:completion:) withSelector:@selector(fixed_animateWithDuration:delay:options:animations:completion:)]; 

之后,所有对[UIView animateWithDuration:...]的调用[UIView animateWithDuration:...]导致这个修改的实现。

我正在debugging我的应用程序中的内存泄漏,结果是这个泄漏,最后得出了与@gabbayabb完全相同的结论 – UITableView使用的animation的完成块永远不会被释放,并且具有强大的引用表视图,这意味着永远不会被释放。 我发生了一个简单的[tableView beginUpdates]; [tableView endUpdates]; [tableView beginUpdates]; [tableView endUpdates]; 一对电话,两者之间没有任何东西。 我发现在调用周围禁用animation( [UIView setAnimationsEnabled:NO]...[UIView setAnimationsEnabled:YES] )避免了泄漏 – 这种情况下的块被UIView直接调用,并且永远不会被复制到堆,因此从来没有创build一个强有力的表格视图的参考。 如果你真的不需要animation,那么这个方法应该可行。 如果你需要animation,不pipe是等待苹果修复它,并且忍受漏洞,或者尝试通过混合一些方法(比如上面的@gabbayabb的方法)来解决或者减轻泄漏。

这种方法的工作方式是用一个非常小的包装完成块,并手动pipe理对原始完成块的引用。 我确实证实了这个工作,原来的完成块得到释放(并且适当地释放它的所有强引用)。 小包装块将仍然泄漏,直到苹果修复他们的错误,但是不保留任何其他对象,所以这将是一个相对较小的泄漏。 这种方法工作的事实表明,问题实际上是在UIView代码,而不是UITableView,但在testing中,我还没有发现任何其他调用这个方法泄漏他们的完成块 – 它似乎只是UITableView那些。 另外,看起来UITableViewanimation有一堆嵌套的animation(每个节或行可能有一个),每个animation都有一个对表视图的引用。 下面我介绍了更多的修复方法,我发现每次调用begin / endUpdates时,我们都强行处理了大约十二个泄漏完成块(对于一个小表)。

@ gabbayabb的解决scheme(但对于ARC)的版本将是:

 #import <objc/runtime.h> typedef void (^CompletionBlock)(BOOL finished); @implementation UIView (iOS7UITableViewLeak) + (void)load { if ([UIDevice currentDevice].systemVersion.intValue >= 7) { Method animateMethod = class_getClassMethod(self, @selector(animateWithDuration:delay:options:animations:completion:)); Method replacement = class_getClassMethod(self, @selector(_leakbugfix_animateWithDuration:delay:options:animations:completion:)); method_exchangeImplementations(animateMethod, replacement); } } + (void)_leakbugfix_animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { CompletionBlock realBlock = completion; /* If animations are off, the block is never copied to the heap and the leak does not occur, so ignore that case. */ if (completion != nil && [UIView areAnimationsEnabled]) { /* Copy to ensure we have a handle to a heap block */ __block CompletionBlock completionBlock = [completion copy]; CompletionBlock wrapperBlock = ^(BOOL finished) { /* Call the original block */ if (completionBlock) completionBlock(finished); /* Nil the last reference so the original block gets dealloced */ completionBlock = nil; }; realBlock = [wrapperBlock copy]; } /* Call the original method (name changed due to swizzle) with the wrapper block (or the original, if no wrap needed) */ [self _leakbugfix_animateWithDuration:duration delay:delay options:options animations:animations completion:realBlock]; } @end 

这与@gabbayabb的解决scheme基本相同,只不过是考虑到了ARC,并且如果传入的完成无效,或者禁用了animation,则避免执行任何额外的工作。 这应该是安全的,虽然它不能完全解决泄漏,但它会大大减less影响。

如果你想尝试消除包装块的泄漏,像下面这样的东西应该工作:

 #import <objc/runtime.h> typedef void (^CompletionBlock)(BOOL finished); /* Time to wait to ensure the wrapper block is really leaked */ static const NSTimeInterval BlockCheckTime = 10.0; @interface _IOS7LeakFixCompletionBlockHolder : NSObject @property (nonatomic, weak) CompletionBlock block; - (void)processAfterCompletion; @end @implementation _IOS7LeakFixCompletionBlockHolder - (void)processAfterCompletion { /* If the block reference is nil, it dealloced correctly on its own, so we do nothing. If it's still here, * we assume it was leaked, and needs an extra release. */ if (self.block != nil) { /* Call an extra autorelease, avoiding ARC's attempts to foil it */ SEL autoSelector = sel_getUid("autorelease"); CompletionBlock block = self.block; IMP autoImp = [block methodForSelector:autoSelector]; if (autoImp) { autoImp(block, autoSelector); } } } @end @implementation UIView (iOS7UITableViewLeak) + (void)load { if ([UIDevice currentDevice].systemVersion.intValue >= 7) { Method animateMethod = class_getClassMethod(self, @selector(animateWithDuration:delay:options:animations:completion:)); Method replacement = class_getClassMethod(self, @selector(_leakbugfix_animateWithDuration:delay:options:animations:completion:)); method_exchangeImplementations(animateMethod, replacement); } } + (void)_leakbugfix_animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { CompletionBlock realBlock = completion; /* If animations are off, the block is never copied to the heap and the leak does not occur, so ignore that case. */ if (completion != nil && [UIView areAnimationsEnabled]) { /* Copy to ensure we have a handle to a heap block */ __block CompletionBlock completionBlock = [completion copy]; /* Create a special object to hold the wrapper block, which we can do a delayed perform on */ __block _IOS7LeakFixCompletionBlockHolder *holder = [_IOS7LeakFixCompletionBlockHolder new]; CompletionBlock wrapperBlock = ^(BOOL finished) { /* Call the original block */ if (completionBlock) completionBlock(finished); /* Nil the last reference so the original block gets dealloced */ completionBlock = nil; /* Fire off a delayed perform to make sure the wrapper block goes away */ [holder performSelector:@selector(processAfterCompletion) withObject:nil afterDelay:BlockCheckTime]; /* And release our reference to the holder, so it goes away after the delayed perform */ holder = nil; }; realBlock = [wrapperBlock copy]; holder.block = realBlock; // this needs to be a reference to the heap block } /* Call the original method (name changed due to swizzle) with the wrapper block (or the original, if no wrap needed */ [self _leakbugfix_animateWithDuration:duration delay:delay options:options animations:animations completion:realBlock]; } @end 

这种方法有点危险。 它和前面的解决scheme是一样的,只是它添加了一个小对象,该对象持有对包装块的弱引用,在animation完成后等待10秒,并且如果该包装块还没有被处理(通常应该),假定它被泄露并且强制另外的autorelease呼叫。 主要的危险是如果这种假设是不正确的,完成块不知何故真的在其他地方有一个有效的参考,我们可能会导致崩溃。 这似乎是不太可能的,因为我们不会启动计时器,直到原始的完成块被调用(意味着animation完成),并且完成块实际上不应该存在比这更长的时间(除了UIView机制应该有一个参考)。 有一点风险,但似乎很低,这完全摆脱了泄漏。

通过一些额外的testing,我查看了每个调用的UIViewAnimationOptions值。 当被UITableView调用时,选项值是0x404,对于所有的嵌套animation,它是0x44。 0x44基本上是UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionOverrideInheritedCurve和似乎确定 – 我看到很多其他的animation通过相同的选项值,而不是泄漏他们的完成块。 0x404但是…也有UIViewAnimationOptionBeginFromCurrentState设置,但0x400的值相当于(1 << 10),logging的选项只能上升到(1 << 9)在UIView.h头。 所以UITableView看起来正在使用一个未公开的UIViewAnimationOption,并且UIView中该选项的处理导致完成块(加上所有嵌套animation的完成块)被泄漏。 这导致了另一个可能的解决scheme:

 #import <objc/runtime.h> enum { UndocumentedUITableViewAnimationOption = 1 << 10 }; @implementation UIView (iOS7UITableViewLeak) + (void)load { if ([UIDevice currentDevice].systemVersion.intValue >= 7) { Method animateMethod = class_getClassMethod(self, @selector(animateWithDuration:delay:options:animations:completion:)); Method replacement = class_getClassMethod(self, @selector(_leakbugfix_animateWithDuration:delay:options:animations:completion:)); method_exchangeImplementations(animateMethod, replacement); } } + (void)_leakbugfix_animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { /* * Whatever option this is, UIView leaks the completion block, plus completion blocks in all * nested animations. So... we will just remove it and risk the consequences of not having it. */ options &= ~UndocumentedUITableViewAnimationOption; [self _leakbugfix_animateWithDuration:duration delay:delay options:options animations:animations completion:completion]; } @end 

这种方法简单地消除了未logging的选项位,并转发到真正的UIView方法。 而这似乎工作 – UITableView消失,意味着完成块交易,包括所有嵌套的animation完成块。 我不知道这个选项是干什么用的,但是在轻度testing中,没有它,事情似乎可以正常工作。 期权价值总是有可能以非常明显的方式变得非常重要,这就是风险。 这个修复也不是“安全的”,因为如果苹果修复了它们的错误,它将需要一个应用程序更新来将未公开的选项恢复到表视图animation。 但它确实避免了泄漏。

基本上,虽然…让我们希望苹果早日修复这个错误,而不是稍后。

(小更新:在第一个例子中做了一个编辑来明确地调用[wrapperBlock copy] – 看起来ARC在Release版本中并没有这样做,所以它崩溃,而它在Debug版本中工作。

好消息! 苹果已经修复了iOS 7.0.3(今天发布,2013年10月22日)的这个bug。

我testing过,并且不能再使用运行iOS 7.0.3时提供的示例项目@Joachim来重现问题: https : //github.com/jschuster/RadarSamples/tree/master/TableViewCellAnimationBug

我也不能在iOS 7.0.3上重现我正在开发的其中一个应用程序的问题,在这个应用程序中导致问题。

在iOS 7的大部分用户更新设备至less7.0.3(这可能需要几个星期)之前,继续发布任何解决方法仍然是明智之举。 那么,假设你的解决方法是安全的,并经过testing!