11

我有一个非常简单的 ARC 测试应用程序。其中一个视图控制器包含 UITableView。在制作行动画(insertRowsAtIndexPathsdeleteRowsAtIndexPaths)后,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
4

3 回答 3

8

我正在调试我的应用程序中的内存泄漏,结果证明是同样的泄漏,最终得出了与@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 版本中工作。)

于 2013-10-12T03:12:42.767 回答
8

好吧,如果您深入挖掘(禁用 ARC、子类 tableview、覆盖保留/释放/dealloc 方法,然后在它们上放置日志/断点),您会发现动画完成块中发生了一些不好的事情,这可能导致泄漏.
在 iOS 7 上插入/删除单元格后,tableview 似乎从完成块接收了太多保留,但在 iOS 6 上没有(在 iOS 6 上,UITableView 尚未使用块动画 - 您也可以在堆栈跟踪中检查它) .

所以我尝试以一种肮脏的方式从 UIView 接管 tableview 的动画完成块生命周期:方法调动。这实际上解决了问题。
但它做得更多,所以我仍在寻找更复杂的解决方案。

所以扩展 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:方法,而是从包装块中正确释放(缺少此功能会导致 tableviews 泄漏)。我知道它看起来有点奇怪,但它解决了问题。

现在用你的 App Delegate 的didFinishLaunchingWithOptions:或任何你想要的地方替换原来的动画实现:

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

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

于 2013-09-25T14:45:52.380 回答
5

Good news! Apple has fixed this bug as of iOS 7.0.3 (released today, Oct 22 2013).

在运行 iOS 7.0.3 时,我使用此处提供的示例项目 @Joachim 进行了测试并且无法再重现该问题:https ://github.com/jschuster/RadarSamples/tree/master/TableViewCellAnimationBug

我也无法在我正在开发的其他应用程序之一上重现 iOS 7.0.3 下的问题,该错误导致问题。

在 iOS 7 上的大多数用户将他们的设备更新到至少 7.0.3(可能需要几周时间)之前,继续发布任何变通办法仍然是明智的。好吧,这是假设您的解决方法是安全且经过测试的!

于 2013-10-22T23:28:04.680 回答