iOS 7.0和ARC:UITableView从未在行动画后取消分配

Den*_*nis 11 memory-leaks uitableview ios automatic-ref-counting ios7

我有一个非常简单的测试应用程序与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;
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

gab*_*abb 8

好吧,如果你深入挖掘一下(禁用ARC,子类tableview,覆盖retain/release/dealloc方法,然后在它们上面放置日志/断点),你会发现在动画完成块中发生了一些不好的事情,这可能会导致泄漏.
看起来桌面视图在iOS 7上插入/删除单元格后从完成块中收到太多保留,但在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
Run Code Online (Sandbox Code Playgroud)
#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
Run Code Online (Sandbox Code Playgroud)

正如您所看到的那样,原始完成块不会直接传递给animateWithDuration:方法,而是从包装器块中正确释放(缺少这会导致tableviews中的泄漏).我知道它看起来有点奇怪,但它解决了这个问题.

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

[UIView swizzleStaticSelector:@selector(animateWithDuration:delay:options:animations:completion:) withSelector:@selector(fixed_animateWithDuration:delay:options:animations:completion:)];
Run Code Online (Sandbox Code Playgroud)

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


Car*_*erg 8

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

该方法的工作原理是将完成块包装得非常小,并手动管理对原始完成块的引用.我确认这是有效的,并且原始的完成块被释放(并适当地释放所有强引用).小包装块仍然会泄漏,直到Apple修复它们的bug,但是它不会保留任何其他对象,因此相比之下它将是一个相对较小的泄漏.这种方法工作的事实表明问题实际上是在UIView代码而不是UITableView,但在测试中我还没有发现任何其他对此方法的调用泄漏了它们的完成块 - 它似乎只是UITableView那些.此外,看起来UITableView动画有一堆嵌套动画(每个部分或行可能有一个),每个人都有一个表视图的引用.通过下面我更多参与的修复,我发现每次调用begin/updateUpdates都会强行处理大约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
Run Code Online (Sandbox Code Playgroud)

这基本上与@gabbayabb的解决方案完全相同,除了它是考虑到ARC,并且如果传入的完成开始时为nil或者禁用了动画,则避免执行任何额外的工作.这应该是安全的,虽然它不能完全解决泄漏,但它会大大减少影响.

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

#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
Run Code Online (Sandbox Code Playgroud)

这种方法有点危险.它与前面的解决方案相同,只是它添加了一个小对象,它包含对包装块的弱引用,在动画结束后等待10秒,如果该包装块尚未解除分配(通常应该),假设它被泄露并强制进行额外的自动释放呼叫.主要的危险是如果这个假设是不正确的,并且完成块在某种程度上确实在其他地方确实有一个有效的参考,我们可能会导致崩溃.这似乎不太可能,因为我们不会在调用原始完成块之后启动计时器(意味着动画完成),并且完成块实际上不应该存活的时间比那更长(除了UIView之外)机制应该参考它).

通过一些额外的测试,我查看了每个调用的UIViewAnimationOptions值.当由UITableView调用时,选项值为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
Run Code Online (Sandbox Code Playgroud)

这种方法简单地消除了未记录的选项位并转发到真正的UIView方法.这看起来确实有效 - UITableView确实消失了,这意味着完成块被解除分配,包括所有嵌套的动画完成块.我不知道选择做什么,但在光测试的东西似乎没有它的工作确定.选项价值总是有可能以一种不是很明显的方式非常重要,这就是这种方法的风险.此修复程序也不是"安全",因为如果Apple修复了它们的错误,它将需要进行应用程序更新才能将未记录的选项恢复为表格视图动画.但它确实避免了泄漏.

基本上虽然......让我们希望Apple尽快修复这个bug.

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


smi*_*org 5

好消息!Apple已经修复了iOS 7.0.3(今天发布,2013年10月22日)的这个错误.

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

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

在iOS 7上的大多数用户将其设备更新至至少7.0.3(可能需要几周)之前,继续发布任何变通办法仍然是明智之举.那么,假设您的解决方法是安全的并经过测试!