我正在调试我的应用程序中的内存泄漏,结果证明是同样的泄漏,最终得出了与@gabbayabb完全相同的结论——UITableView使用的动画完成块永远不会被释放,它有一个强大的对表视图的引用,这意味着它也永远不会被释放。我的电话是[tableView beginUpdates]; [tableView endUpdates];
一对简单的电话,中间没有任何东西。我确实发现禁用动画([UIView setAnimationsEnabled:NO]...[UIView setAnimationsEnabled:YES]
) 周围的调用避免了泄漏——这种情况下的块由 UIView 直接调用,并且它永远不会被复制到堆中,因此永远不会首先创建对表视图的强引用。如果您真的不需要动画,那么这种方法应该有效。但是,如果您需要动画……要么等待 Apple 修复它并忍受泄漏,要么尝试通过混合一些方法来解决或减轻泄漏,例如上面@gabbayabb 的方法。
这种方法的工作原理是用一个非常小的完成块包装完成块,并手动管理对原始完成块的引用。我确实确认了这项工作,并且原始完成块被释放(并适当地释放其所有强引用)。在 Apple 修复他们的错误之前,小包装块仍然会泄漏,但它不会保留任何其他对象,因此相比之下它将是一个相对较小的泄漏。这种方法有效的事实表明问题实际上出在 UIView 代码而不是 UITableView 中,但在测试中我还没有发现对该方法的任何其他调用都泄漏了它们的完成块——它似乎只是 UITableView那些。此外,UITableView 动画似乎有一堆嵌套动画(每个部分或行可能有一个),并且每个都有对表格视图的引用。通过下面我更复杂的修复,我发现我们在每次调用 begin/endUpdates 时都强制处理了大约 12 个泄漏的完成块(对于一个小表)。
@gabbayabb 的解决方案的一个版本(但对于 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 的解决方案基本相同,除了它是在考虑 ARC 的情况下完成的,并且如果传入的完成一开始为零或动画被禁用,则避免做任何额外的工作。这应该是安全的,虽然它不能完全解决泄漏,但它大大减少了影响。
如果您想尝试消除包装块的泄漏,应该可以使用以下方法:
#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
这种方法有点危险。它与之前的解决方案相同,只是它添加了一个小对象,该对象持有对包装块的弱引用,在动画完成后等待 10 秒,并且如果该包装块尚未被释放(通常应该这样做),假设它已泄漏并强制对其进行额外的自动释放调用。主要的危险是,如果该假设不正确,并且完成块确实在其他地方确实有有效的引用,我们可能会导致崩溃。但这似乎不太可能,因为直到调用原始完成块(意味着动画完成)之后我们才会启动计时器,并且完成块真的不应该比这更长时间(除了 UIView机制应该有它的参考)。
通过一些额外的测试,我查看了每个调用的 UIViewAnimationOptions 值。当被 UITableView 调用时,options 的值为 0x404,对于所有的嵌套动画,它都是 0x44。0x44 基本上是 UIViewAnimationOptionBeginFromCurrentState| UIViewAnimationOptionOverrideInheritedCurve 看起来还可以——我看到很多其他动画都使用相同的选项值,并且没有泄漏它们的完成块。0x404 然而......也有 UIViewAnimationOptionBeginFromCurrentState 设置,但 0x400 值相当于 (1 << 10),并且记录的选项仅在 UIView.h 标头中上升到 (1 << 9)。所以 UITableView 似乎正在使用一个未记录的 UIViewAnimationOption,并且在 UIView 中处理该选项会导致完成块(加上所有嵌套动画的完成块)被泄露。
#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
这种方法简单地消除了未记录的选项位并转发到真正的 UIView 方法。这似乎确实有效—— UITableView 确实消失了,这意味着完成块被释放,包括所有嵌套的动画完成块。我不知道该选项有什么作用,但在轻度测试中,没有它似乎一切正常。期权价值总是有可能以一种不是立即显而易见的方式至关重要,这就是这种方法的风险。从某种意义上说,此修复也不是“安全的”,如果 Apple 修复了他们的错误,则需要更新应用程序才能将未记录的选项恢复为表视图动画。但它确实避免了泄漏。
不过基本上……让我们希望 Apple 早日修复这个错误。
(小更新:在第一个示例中进行了一次编辑以显式调用 [wrapperBlock copy] —— 似乎 ARC 在 Release 版本中没有为我们这样做,因此它崩溃了,而它在 Debug 版本中工作。)