Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>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 <code>[tableView beginUpdates]; [tableView endUpdates];</code> pair of calls, with nothing in between. I did discover that disabling animations (<code>[UIView setAnimationsEnabled:NO]...[UIView setAnimationsEnabled:YES]</code>) 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.</p> <p>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.</p> <p>A version of @gabbayabb's solution (but for ARC) would be:</p> <pre class="lang-c prettyprint-override"><code>#import &lt;objc/runtime.h&gt; typedef void (^CompletionBlock)(BOOL finished); @implementation UIView (iOS7UITableViewLeak) + (void)load { if ([UIDevice currentDevice].systemVersion.intValue &gt;= 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 &amp;&amp; [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 </code></pre> <p>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.</p> <p>If you want to try to eliminate the leak of the wrapper blocks, something like the following should work:</p> <pre class="lang-c prettyprint-override"><code>#import &lt;objc/runtime.h&gt; 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 &gt;= 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 &amp;&amp; [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 </code></pre> <p>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.</p> <p>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 &lt;&lt; 10), and the documented options only go up to (1 &lt;&lt; 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:</p> <pre class="lang-c prettyprint-override"><code>#import &lt;objc/runtime.h&gt; enum { UndocumentedUITableViewAnimationOption = 1 &lt;&lt; 10 }; @implementation UIView (iOS7UITableViewLeak) + (void)load { if ([UIDevice currentDevice].systemVersion.intValue &gt;= 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 &amp;= ~UndocumentedUITableViewAnimationOption; [self _leakbugfix_animateWithDuration:duration delay:delay options:options animations:animations completion:completion]; } @end </code></pre> <p>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 <em>seem</em> 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.</p> <p>Basically though... let's hope Apple fixes this bug sooner rather than later.</p> <p>(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.)</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload