iOS 7.0 和 ARC:UITableView 在行动画后永远不会释放 [英] iOS 7.0 and ARC: UITableView never deallocated after rows animation

查看:28
本文介绍了iOS 7.0 和 ARC:UITableView 在行动画后永远不会释放的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个非常简单的带有 ARC 的测试应用程序.其中一个视图控制器包含 UITableView.在制作行动画(insertRowsAtIndexPathsdeleteRowsAtIndexPaths)之后,UITableView(和所有单元格)永远不会被释放.如果我使用 reloadData,它工作正常.在 iOS 6 上没有问题,只有 iOS 7.0.任何想法如何解决此内存泄漏?

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 是一种 TableView 类(UITableView 的子类):

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

推荐答案

我正在调试我的应用程序中的内存泄漏,结果是同样的泄漏,最终得出与@gabbayabb 完全相同的结论——UITableView 使用的动画的完成块永远不会被释放,并且它对 table view 有很强的引用,这意味着也永远不会被释放.我的发生在一个简单的 [tableView beginUpdates];[tableView endUpdates]; 一对调用,中间没有任何内容.我确实发现在调用周围禁用动画 ([UIView setAnimationsEnabled:NO]...[UIView setAnimationsEnabled:YES]) 避免了泄漏——在这种情况下,块由 UIView 直接调用,并且它永远不会被复制到堆中,因此永远不会首先创建对表视图的强引用.如果您真的不需要动画,那么这种方法应该可行.但是,如果您需要动画……要么等待 Apple 修复它并忍受泄漏,要么尝试通过混合一些方法来解决或减轻泄漏,例如上面@gabbayabb 的方法.

I was debugging a memory leak in my application, which turned out to be this same leak, and eventually came to the exact same conclusion as @gabbayabb -- the completion block of the animation used by UITableView never gets freed, and it has a strong reference to the table view, meaning that never gets freed either. Mine happened with a simple [tableView beginUpdates]; [tableView endUpdates]; pair of calls, with nothing in between. I did discover that disabling animations ([UIView setAnimationsEnabled:NO]...[UIView setAnimationsEnabled:YES]) around the calls avoided the leak -- the block in that case is invoked directly by UIView, and it never gets copied to the heap, and therefore never creates a strong reference to the table view in the first place. If you don't really need the animation, that approach should work. If you need the animation though... either wait for Apple to fix it and live with the leak, or attempt to solve or mitigate the leak via swizzling some methods, such as the approach by @gabbayabb above.

这种方法的工作原理是用一个非常小的完成块包装完成块,并手动管理对原始完成块的引用.我确实确认这是有效的,并且原始完成块被释放(并适当地释放其所有强引用).在 Apple 修复他们的错误之前,小包装器块仍然会泄漏,但这不会保留任何其他对象,因此相比之下,这将是一个相对较小的泄漏.这种方法有效的事实表明问题实际上出在 UIView 代码而不是 UITableView,但在测试中我还没有发现对此方法的任何其他调用泄漏了它们的完成块——它似乎只是 UITableView那些.此外,看起来 UITableView 动画有一堆嵌套动画(每个部分或行可能一个),每个动画都有一个对表视图的引用.通过下面更复杂的修复,我发现我们在每次调用 begin/endUpdates 时强行处理了大约 12 个泄漏的完成块(对于一个小表).

That approach works by wrapping the completion block with a very small one, and managing the references to the original completion block manually. I did confirm this works, and the original completion block gets freed up (and releases all of its strong references appropriately). The small wrapper block will still leak until Apple fixes their bug, but that does not retain any other objects so it will be a relatively small leak in comparison. The fact this approach works indicates that the problem is actually in the UIView code rather than the UITableView, but in testing I have not yet found that any of the other calls to this method leak their completion blocks -- it only seems to be the UITableView ones. Also, it appears that the UITableView animation has a bunch of nested animations (one for each section or row maybe), and each one has a reference to the table view. With my more involved fix below, I found we were forcibly disposing of about twelve leaked completion blocks (for a small table) for each call to begin/endUpdates.

@gabbayabb 的解决方案的一个版本(但对于 ARC)是:

A version of @gabbayabb's solution (but for ARC) would be:

#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 的情况下完成的,并且如果传入的完成开始为零或动画被禁用,则避免做任何额外的工作.这应该是安全的,虽然它不能完全解决泄漏问题,但它大大减少了影响.

This is basically identical to @gabbayabb 's solution, except it is done with ARC in mind, and avoids doing any extra work if the passed-in completion is nil to begin with or if animations are disabled. That should be safe, and while it does not completely solve the leak, it drastically reduces the impact.

如果您想尝试消除包装块的泄漏,则应该使用以下方法:

If you want to try to eliminate the leak of the wrapper blocks, something like the following should work:

#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 之外别无其他)机制应该有一个参考).有轻微的风险,但看起来很低,这确实完全消除了泄漏.

This approach is a little bit more dangerous. It is the same as the previous solution, except it adds a small object which holds a weak reference to the wrapper block, waits 10 seconds after the animation finishes, and if that wrapper block has not been dealloced yet (which it normally should), assumes it is leaked and forces an additional autorelease call on it. The main danger is if that assumption is incorrect, and the completion block somehow really does have a valid reference elsewhere, we could be causing a crash. It seems very unlikely though, since we won't start the timer until after the original completion block has been called (meaning the animation is done), and the completion blocks really should not survive much longer than that (and nothing other than the UIView mechanism should have a reference to it). There is a slight risk, but it seems low, and this does completely get rid of the leak.

通过一些额外的测试,我查看了每个调用的 UIViewAnimationOptions 值.当被 UITableView 调用时,选项值为 0x404,对于所有嵌套动画,它是 0x44.0x44 基本上是 UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionOverrideInheritedCurve 看起来还不错——我看到很多其他动画都使用相同的选项值,并且不会泄漏它们的完成块.然而 0x404... 也设置了 UIViewAnimationOptionBeginFromCurrentState,但 0x400 值等效于 (1 << 10),并且记录的选项在 UIView.h 标头中仅上升到 (1 << 9).所以 UITableView 似乎使用了一个未记录的 UIViewAnimationOption,并且在 UIView 中处理该选项会导致完成块(加上所有嵌套动画的完成块)被泄漏.这导致了另一种可能的解决方案:

With some additional testing, I looked at the UIViewAnimationOptions value for each of the calls. When called by UITableView, the options value is 0x404, and for all of the nested animations it is 0x44. 0x44 is basically UIViewAnimationOptionBeginFromCurrentState| UIViewAnimationOptionOverrideInheritedCurve and seems OK -- I see lots of other animations go through with that same options value and not leak their completion blocks. 0x404 however... also has UIViewAnimationOptionBeginFromCurrentState set, but the 0x400 value is equivalent to (1 << 10), and the documented options only go up to (1 << 9) in the UIView.h header. So UITableView appears to be using an undocumented UIViewAnimationOption, and the handling of that option in UIView causes the completion block (plus the completion block of all nested animations) to be leaked. That leads itself to another possible solution:

#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 修复了他们的错误,它将需要更新应用程序才能将未记录的选项恢复到表格视图动画中.但它确实避免了泄漏.

This approach simply eliminates the undocumented option bit and forwards on to the real UIView method. And this does seem to work -- the UITableView does go away, meaning the completion block is dealloced, including all nested animation completion blocks. I have no idea what the option does, but in light testing things seem to work OK without it. It's always possible that option value is vitally important in a way that's not immediately obvious, which is the risk with this approach. This fix is also not "safe" in the sense that if Apple fixes their bug, it will take an application update to get the undocumented option restored to table view animations. But it does avoid the leak.

基本上……但我们希望 Apple 尽快修复这个错误.

Basically though... let's hope Apple fixes this bug sooner rather than later.

(小更新:在第一个示例中进行了一个编辑以显式调用 [wrapperBlock copy] - 似乎 ARC 在发布版本中没有为我们这样做,因此它崩溃了,而它在调试版本中工作.)

(Small update: Made one edit to explicitly call [wrapperBlock copy] in the first example -- seems like ARC did not do that for us in a Release build and so it crashed, while it worked in a Debug build.)

这篇关于iOS 7.0 和 ARC:UITableView 在行动画后永远不会释放的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆