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

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

问题描述

我有一个非常简单的ARC测试应用程序。其中一个视图控制器包含UITableView。使行动画后( insertRowsAtIndexPaths deleteRowsAtIndexPaths )的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使用的动画的完成块永远不会被释放,并且它强烈引用了表视图,这意味着它永远不会被释放。我的一个简单的 [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修复它们的bug,但是它不会保留任何其他对象,因此相比之下它将是一个相对较小的泄漏。这种方法工作的事实表明问题实际上是在UIView代码而不是UITableView,但在测试中我还没有发现任何其他对此方法的调用泄漏了他们的完成块 - 它似乎只是UITableView那些。此外,看起来UITableView动画有一堆嵌套动画(每个部分或行可能有一个),每个动画都有一个对表视图的引用。通过下面我更多参与的修复,我发现每次调用begin / updateUpdates都会强行处理大约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.

A版本@ 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,并且如果传入的完成开始时为nil或者禁用了动画,则避免做任何额外的工作。这应该是安全的,虽然它不能完全解决泄漏,但它会大大减少影响。

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),并且记录的选项仅上升到(1 <<;&10 9)在UIView.h头。因此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修复了它们的bug,它将需要应用程序更新才能将未记录的选项恢复为表格视图动画。但它确实可以避免泄漏。

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尽快修复这个bug。

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

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

(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天全站免登陆