Analysis and Solutions for a Memory Leak Caused by a Repeating Animation in iOS
The article explains how a repeating iOS animation caused a hidden memory leak by retaining its view controller through a strong CABasicAnimation delegate and a performSelector‑afterDelay timer, and shows how replacing the delegate‑based animation with a CAKeyframeAnimation/CAAnimationGroup and using weak references or cancelling pending selectors eliminates the retain cycle.
This article records a memory‑leak problem encountered in a project: a repeating animation that occasionally leaks memory. The leak is hidden and only appears under certain conditions, which are explained later.
Animation Description
When entering AController, an animation is started that performs the following steps:
Move a view from left to right over 0.5s.
After the movement finishes, hide the view for 1 second.
After the 1 second pause, show the view again, reset its position, and repeat.
Initial Implementation and Problems
The original code is:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self startBaseAnimation];
}
- (void)startBaseAnimation
{
if (!_baseAniMoveView) return;
self.navigationItem.title = @"动画进行中...";
[self.baseAniMoveView.layer removeAllAnimations];
self.baseAniMoveView.hidden = NO;
CABasicAnimation *baseAni = [CABasicAnimation animationWithKeyPath:@"position"];
CGPoint leftStarPosition = self.baseAniMoveView.center;
baseAni.fromValue = [NSValue valueWithCGPoint:self.baseAniMoveView.center];
baseAni.toValue = [NSValue valueWithCGPoint:CGPointMake(leftStarPosition.x + moveLength, leftStarPosition.y)];
baseAni.duration = moveDuration;
baseAni.removedOnCompletion = NO;
baseAni.delegate = self;
baseAni.fillMode = kCAFillModeForwards;
[self.baseAniMoveView.layer addAnimation:baseAni forKey:kBaseAnimationKey];
}
#pragma mark - animation delegate
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
if (flag && _baseAniMoveView) {
[self.baseAniMoveView.layer removeAllAnimations];
self.baseAniMoveView.hidden = YES;
[self performSelector:@selector(startBaseAnimation) withObject:nil afterDelay:pauseDuration];
}
}Two main issues are identified:
The delegate of CABasicAnimation is held strongly, which can keep the view controller alive.
The callback uses performSelector:withObject:afterDelay:, which creates an internal timer that retains self, forming a retain cycle.
To break the first issue, the delegate should be set to nil (e.g., in viewWillDisappear) or the animation should be removed with removeAllAnimations before the view disappears.
For the second issue, several alternatives are suggested:
Cancel the pending selector in viewWillDisappear:
[NSObject cancelPreviousPerformRequestsWithTarget:@selector(startBaseAnimation)];Replace performSelector with dispatch_after using a weak reference to self:
SCWeakSelf(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(pauseDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
SCStrongSelf(self);
if (!self) return;
[self startBaseAnimation];
});Implement a custom weak timer and ensure it is destroyed at the appropriate time.
Hidden Issue
The dealloc method sometimes does not run when the page is popped. This happens because the animation callback only executes the performSelector when the flag parameter is YES. If the user taps the back button while the animation is still running, flag is NO, the selector is not called, and the retain cycle remains, preventing deallocation.
Root Cause and Refactored Solution
The fundamental problem lies in the animation implementation. A refactored approach uses CAKeyframeAnimation and an CAAnimationGroup to avoid delegates and timers:
- (void)startKeyAnimation
{
if (!_baseAniMoveView) return;
[self.baseAniMoveView.layer removeAllAnimations];
// Show view instantly
CAKeyframeAnimation *showAni = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
showAni.duration = 0;
showAni.values = @[@1, @1];
// Move view
CGPoint leftStarPosition = self.baseAniMoveView.center;
CAKeyframeAnimation *baseAni = [CAKeyframeAnimation animationWithKeyPath:@"position"];
baseAni.duration = moveDuration;
baseAni.values = @[[NSValue valueWithCGPoint:self.baseAniMoveView.center],
[NSValue valueWithCGPoint:CGPointMake(leftStarPosition.x + moveLength, leftStarPosition.y)]];
baseAni.removedOnCompletion = NO;
baseAni.fillMode = kCAFillModeForwards;
// Hide view for 1 s
CAKeyframeAnimation *hideAni = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
hideAni.duration = pauseDuration;
hideAni.values = @[@0, @0];
hideAni.beginTime = moveDuration; // start after the move animation
// Group
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = @[showAni, baseAni, hideAni];
group.repeatCount = FLT_MAX;
group.duration = moveDuration + pauseDuration;
[self.baseAniMoveView.layer addAnimation:group forKey:kKeyAnimationKey];
}This eliminates the need for a strong delegate and the timer‑based selector, thereby removing the retain‑cycle source.
Additional Notes
When using performSelector for delayed execution, remember it creates a timer that retains self. Even though the timer eventually releases, it can delay deallocation and cause repeated calls.
Animation delegates should not be strong; they must be cleared appropriately.
For leak detection, Apple’s Instruments can be used, as well as third‑party tools such as PLeakSniffer, FBRetainCycleDetector, HeapInspector‑for‑iOS, MSLeakHunter, and MLeaksFinder. The project already integrates MLeaksFinder to assert on leaks during debugging.
Demo project: https://github.com/Aevit/SCAnimationMemoryLeakDemo
Recruitment notice: QQ Music team is hiring testers and developers. Send resumes to [email protected] and mention the public account source.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tencent Music Tech Team
Public account of Tencent Music's development team, focusing on technology sharing and communication.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
