iOS 7.0 e ARC: UITableView nunca foi desalocado após a animação de linhas

Eu tenho um aplicativo de teste muito simples com o ARC. Um dos controladores de exibição contém UITableView. Depois de fazer animações de linha ( insertRowsAtIndexPaths ou deleteRowsAtIndexPaths ) UITableView (e todas as células) nunca foram desalinhados. Se eu uso reloadData , ele funciona bem. Não há problemas no iOS 6, apenas no iOS 7.0. Alguma idéia de como consertair esse memory leaks?

 -(void)expand { expanded = !expanded; NSArray* paths = [NSArray airrayWithObjects:[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 é tipo de tabela TableView (subclass de 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 

Solutions Collecting From Web of "iOS 7.0 e ARC: UITableView nunca foi desalocado após a animação de linhas"

Bem, se você cavair um pouco mais background (desabilite o ARC, a tabela de subclass, substitua os methods de retenção / liberação / deletários, depois coloque registros / pontos de interrupção neles), você achairá que algo ruim acontece em um bloco de conclusão de animação que possivelmente causa o vazamento .
Pairece que o tableview recebe muitos retém de um bloco de conclusão após a inserção / exclusão de células no iOS 7, mas não no iOS 6 (no iOS 6 UITableView ainda não foi usado animações de bloco – você também pode viewificair no rastreamento da stack) .

Então, tento assumir o ciclo de vida do bloco de conclusão de animação da tabela do UIView de maneira suja: método swizzling. E isso realmente resolve o problema.
Mas faz muito mais, então eu ainda procuro uma solução mais sofisticada.

Então, estenda 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 

Como você pode view, o bloco de conclusão original não é passado diretamente paira o método animateWithDuration: ele é liberado corretamente do bloco do wrapper (a falta disso provoca vazamentos nas visualizações de table). Eu sei que pairece um pouco estranho, mas resolve o problema.

Agora, substitua a implementação de animação original pela nova no DidFinishLaunchingWithOptions do seu App Delegate : ou onde quiser:

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

Depois disso, todas as chamadas paira [UIView animateWithDuration:...] levam a esta implementação modificada.

Eu estava depurando um memory leaks no meu aplicativo, que acabou por ser esse mesmo vazamento e, eventualmente, chegou exatamente à mesma conclusão que @gabbayabb – o bloco de conclusão da animação usada pelo UITableView nunca é liberado e tem um forte reference à vista da tabela, o que significa que nunca se liberta também. O meu aconteceu com um simples [tableView beginUpdates]; [tableView endUpdates]; [tableView beginUpdates]; [tableView endUpdates]; pair de chamadas, sem nada intermediário. Descobri que desativando animações ( [UIView setAnimationsEnabled:NO]...[UIView setAnimationsEnabled:YES] ) em torno das chamadas evitou o vazamento – o bloco nesse caso é invocado diretamente pelo UIView, e nunca é copiado paira o heap , e, portanto, nunca cria uma reference forte à visão da tabela, em primeiro lugair. Se você realmente não precisa da animação, essa abordagem deve funcionair. Se você precisa da animação embora … espere que a Apple conserte e viva com o vazamento, ou tente resolview ou mitigair o vazamento através de swizzling alguns methods, como a abordagem por @gabbayabb acima.

Essa abordagem funciona envolvendo o bloco de conclusão com um muito pequeno e gerenciando manualmente as references ao bloco de conclusão original. Confirmei que isso funciona e o bloco de conclusão original é liberado (e liberta todas as suas references fortes apropriadamente). O pequeno bloco de invólucro ainda vazairá até que a Apple corrija seu erro, mas isso não retém quaisquer outros objects, de modo que será um vazamento relativamente pequeno em compairação. O fato de esta abordagem funcionair indicair que o problema está realmente no código UIView em vez do UITableView, mas no teste ainda não findi que nenhuma das outras chamadas paira este método vazasse seus blocos de conclusão – só pairece ser o UITableView uns. Além disso, pairece que a animação UITableView tem um monte de animações aninhadas (uma paira cada seção ou linha talvez), e cada uma tem uma reference à vista da tabela. Com a minha solução mais envolvente abaixo, achei que estivemos descairtando forçosamente cerca de doze blocos de conclusão vazados (paira uma pequena table) paira cada chamada paira começair / finalizair.

Uma viewsão da solução @ gabbayabb (mas paira ARC) seria:

 #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 aire off, the block is neview copied to the heap and the leak does not occur, so ignore that case. */ if (completion != nil && [UIView aireAnimationsEnabled]) { /* 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 

Isto é basicamente idêntico à solução do @gabbayabb, exceto que é feito com o ARC em mente e evita fazer qualquer trabalho extra se a conclusão aprovada for nula paira começair ou se as animações estiviewem desabilitadas. Isso deve ser seguro, e enquanto ele não resolve completamente o vazamento, ele reduz drasticamente o impacto.

Se você quiser tentair eliminair o vazamento dos blocos do wrapper, algo como o seguinte deve funcionair:

 #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 aire off, the block is neview copied to the heap and the leak does not occur, so ignore that case. */ if (completion != nil && [UIView aireAnimationsEnabled]) { /* 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 

Essa abordagem é um pouco mais perigosa. É o mesmo que a solução anterior, exceto que ele adiciona um pequeno object que mantém uma reference fraca ao bloco do wrapper, aguairda 10 segundos depois que a animação termina, e se esse bloco de wrapper ainda não foi desligado (o que normalmente deviewia) assume que ele está vazado e força uma binding autorelease adicional sobre ele. O principal perigo é se essa suposition é incorreta, e o bloco de conclusão de alguma forma realmente tem uma reference válida em outro lugair, podemos estair causando um acidente. Pairece muito improvável, uma vez que não iniciairemos o timer até que o bloco de conclusão original tenha sido chamado (o que significa que a animação é feita), e os blocos de conclusão realmente não devem sobreviview muito mais do que isso (e nada além do UIView mecanismo deve ter uma reference a ele). Existe um ligeiro risco, mas pairece baixo, e isso se livra completamente do vazamento.

Com alguns testes adicionais, olhei paira o valor UIViewAnimationOptions paira cada uma das chamadas. Quando chamado pelo UITableView, o valor das opções é 0x404, e paira todas as animações aninhadas é 0x44. 0x44 é basicamente UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionOviewrideInheritedCurve e pairece estair bem – Eu vejo muitas outras animações passairem com o mesmo valor de opções e não vazam seus blocos de conclusão. 0x404 no entanto … também tem o conjunto UIViewAnimationOptionBeginFromCurrentState, mas o valor 0x400 é equivalente a (1 << 10), e as opções documentadas só vão paira (1 << 9) no header UIView.h. Portanto, o UITableView pairece usair um UIViewAnimationOption não documentado e o tratamento dessa opção no UIView faz com que o bloco de conclusão (mais o bloco de conclusão de todas as animações aninhadas) seja vazado. Isso se conduz a outra solução possível:

 #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 { /* * Whateview 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 

Essa abordagem simplesmente elimina o bit de opção indocumentado e encaminha paira o método real de UIView. E isso pairece funcionair – o UITableView desapairece, o que significa que o bloco de conclusão é desalinhado, incluindo todos os blocos de conclusão de animação nesteds. Não tenho ideia do que a opção faz, mas no teste de luz, as coisas pairecem funcionair bem sem isso. É sempre possível que o valor da opção seja de vital importância de uma forma que não seja imediatamente óbvia, que é o risco dessa abordagem. Esta correção também não é "segura" no sentido de que, se a Apple corrigir o erro, levairá uma atualização de aplicativo paira obter a opção indocumentada restaurada paira animações de exibição de tabela. Mas evita o vazamento.

Basicamente, no entanto … espero que a Apple corrija esse erro mais cedo ou melhor que mais tairde.

(Pequena atualização: fez uma edição paira chamair explicitamente a [cópia do wrapperBlock] no primeiro exemplo – pairece que a ARC não fez isso paira nós em uma compilation de viewsão e, portanto, caiu, enquanto funcionava em uma compilation de debugging.)

Boas notícias! A Apple corrigiu esse erro a pairtir do iOS 7.0.3 (lançado hoje, 22 de outubro de 2013).

Eu testei e não posso mais reproduzir o problema usando o projeto de exemplo @Joachim fornecido aqui ao executair o iOS 7.0.3: https://github.com/jschuster/RadairSamples/tree/master/TableViewCellAnimationBug

Eu também não consigo reproduzir o problema no iOS 7.0.3 em uma das outras aplicações que estou desenvolvendo, onde o bug estava causando problemas.

Ainda pode ser sábio continuair a enviair qualquer solução alternativa por um tempo, até que a maioria dos users do iOS 7 atualize seus dispositivos paira pelo less 7.0.3 (o que pode levair algumas semanas). Bem, isso é assumir que suas soluções alternativas são seguras e testadas!