前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一个循环动画引起的内存泄露问题总结

一个循环动画引起的内存泄露问题总结

作者头像
QQ音乐技术团队
发布2018-07-13 16:24:58
2.3K0
发布2018-07-13 16:24:58
举报

前言

本文主要记录项目中遇到的一个内存泄露问题:由于一个循环动画引起的内存泄露,并且这个问题也是偶现的,在后面的 隐藏问题 里会说明。

先说下该动画: 进入 AController 后,需要执行一个动画,该动画会执行以下步骤:

  • 将一个 view 从左到右移动,动画时间 0.5s
  • 上一步的动画完成后,将 view hidden 1 秒
  • 1 秒后将 view 显示出来,并回到原来位置,重复执行上面步骤

下面将逐步分析问题并提供相应的解决方案,以及如何从根源上解决这个问题。

问题初步分析及解决

最开始该代码如下:

代码语言:javascript
复制
- (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];
    }
}

这里有两个问题:

  • CABasicAnimationdelegatestrong
  • 动画完成的回调里执行了 performSelector:  [self performSelector:@selector(startBaseAnimation) withObject:nil afterDelay:pauseDuration];

第一个问题要么在 viewWillDisappear 时,手动置该 delegate 为 nil,要么对该 view 的 layer 执行 removeAllAnimations 方法(之后记得在 viewWillAppear 重新启动动画)。

原本代码因为在 viewWillDisappear 里有执行了 removeAllAnimations,所以这个地方的内存泄露风险没有暴露出来。

第二个问题,因为 performSelector 这个方法内部是有一个 timer,该 timer 会持有 selfself 也持有该 timer,造成循环引用,所以 dealloc 就一直不调用了。

解决方法也有多个,比如说在 viewWillDisappear 里取消掉该 perform 的方法(之后记得在 viewWillAppear 重新启动动画):

代码语言:javascript
复制
[NSObject cancelPreviousPerformRequestsWithTarget:@selector(startBaseAnimation)];

或者不用 perfomrSelector,改用 dispatch_after,block 里用 weak self

代码语言:javascript
复制
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];
});

或是自己实现一个 weak timer,这种方式就要注意一定要用 weak timer,并在合适的时机进行定时器的销毁。

隐藏问题

这里还有一个隐藏的问题,就是发现 dealloc 方法,在 pop 页面时,有时能执行,有时不能执行,按理来说有执行了 performSelector 方法,应该是必现的问题。

后来发现,问题是出在动画完成的回调里,里面是判断 flagYES 时才会跑进去执行 performSelector 方法,而为 NO 时就不会有问题。

代码语言:javascript
复制
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    // 注意这里是判断 flag 为 YES 时才会进去
    if (flag && _baseAniMoveView) {
        [self.baseAniMoveView.layer removeAllAnimations];
        self.baseAniMoveView.hidden = YES;
        [self performSelector:@selector(startBaseAnimation) withObject:nil afterDelay:pauseDuration];
    }
}

而什么时候为 NO 呢,顾名思义就是动画未完成,所以动画正在执行中时,点击了返回按钮,回调的 flag 就为 NO,所以就不会执行 performSelector,所以也就不会造成内存泄露了。

所以这个内存泄露出现的时机,就为:动画完成后刚好点击了返回

问题根源

上面分析了问题,并给出了相应的解决方案,不过以上只是治标不治本的方法,问题的根源在动画的实现方式上。

以下是用 CAKeyframeAnimation 动画组来实现的方案:

代码语言:javascript
复制
- (void)startKeyAnimation
{
    if (!_baseAniMoveView) return;

    [self.baseAniMoveView.layer removeAllAnimations];

    // 显示 view
    CAKeyframeAnimation *showAni = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
    showAni.duration = 0;
    showAni.values = @[@1, @1];

    // 移动 view
    CGPoint leftStarPosition = self.baseAniMoveView.center;
    CAKeyframeAnimation *baseAni = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    baseAni.duration = moveDuration;
    NSValue *fromValue =  [NSValue valueWithCGPoint:self.baseAniMoveView.center];
    NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(leftStarPosition.x + moveLength, leftStarPosition.y)];
    baseAni.values = @[fromValue, toValue];
    baseAni.removedOnCompletion = NO;
    baseAni.fillMode = kCAFillModeForwards;

    // 隐藏 view
    CAKeyframeAnimation *hideAni = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
    hideAni.duration = pauseDuration;
    hideAni.values = @[@0, @0];
    hideAni.beginTime = moveDuration; // important!

    // 动画组
    CAAnimationGroup *group = [CAAnimationGroup animation];
    group.animations = @[showAni, baseAni, hideAni];
    group.repeatCount = FLT_MAX;
    group.duration = moveDuration + pauseDuration;

    // 添加动画
    [self.baseAniMoveView.layer addAnimation:group forKey:kKeyAnimationKey];
}

其中难点在于如何控制 平移动画 完成后,将 view 隐藏 1 秒后重新显现并继续执行。

这里就使用多一个关键帧动画操作其 opacity 参数实现隐藏 1 秒。将其 values 设置为 0 到 0,该帧动画持续 1 秒,并且该帧动画的开始时间要另外设置一下,改为在 平移动画完成后:

代码语言:javascript
复制
hideAni.beginTime = moveDuration;

并且在重新执行 平移动画 前将 view 重新显示出来,这里同样使用多一个关键帧动画,将该 view 的 opacity 设置为 从 1 到 1,持续 0 秒,这样就能立马显示出来:

代码语言:javascript
复制
CAKeyframeAnimation *showAni = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
showAni.duration = 0;
showAni.values = @[@1, @1];

最后将这三个关键帧动画加到 CAAnimationGroup 里即可,这样就不会有上面的 delegatetimer 相关的问题。

总结

使用 performSelector 来延时执行,要记得其内部是有一个 timer 的,会持有 self,所以要注意循环引用的问题,虽然在最后会自动释放,但是这样也会造成延时释放或是上述重复调用导致 self 一直不能被释放等问题。

写动画时,要注意其 delegatestrong,所以要注意释放。

demo 工程可以去这里查看: https://github.com/Aevit/SCAnimationMemoryLeakDemo

动画停止

另外,动画在 push 到新页面,或是回到桌面,再重新返回,动画会停止,猜测可能是系统某些机制,毕竟执行动画是要刷新 layer,所以是要耗电的,可能系统做了优化来节电。

节电这一点查了很久也没有查到明确的资料来证明,不过苹果关于 后台任务 的文档里有这样一段话:

代码语言:javascript
复制
When the user is not actively using your app, the system moves it to the
background state. For many apps, the background state is just a brief stop
on the way to the app being suspended. Suspending apps is a way of
improving battery life it also allows the system to devote important
system resources to the new foreground app that has drawn the user’s
attention.

在这里提到了进入后台及电池相关的,所以才推测是为了省电,不然在用户不可见的界面,还一直进行 layer 的刷新来做动画,是会对电池造成一点点损耗的,当动画一多就更明显了。

所以一般就在页面即将消失时移除动画,在 viewWillAppear,以及监听从桌面回到 app 的事件,重新添加动画。

内存泄露检测

苹果提供了 Instruments 工具来检测内存泄露,不过一般是想到要检测时才会去用,并且需要针对性地去某个页面查看,不能在开发阶段就发现问题。业界也有一些库来检测,如 PLeakSniffer、FBRetainCycleDetector(主要检测循环引用问题)、HeapInspector-for-iOS、MSLeakHunter、MLeaksFinder 等。

目前 github 上 star 较多的是 MLeaksFinder,其基本原理简单来说是 hook 掉 popdismiss 方法,在里面调用自定义的 willDealloc 方法,该方法会延时几秒后进行断言,如果命中断言,说明内存泄露了。详情可参见该团队的文章: MLeaksFinder:精准 iOS 内存泄露检测工具,这里不再赘述。

我们 APP 里已经接入该库,在 Debug 模式中检测到类似的内存泄露就弹框或者 Assert,及时地发现和解决。

QQ音乐团队诚聘测试、研发。有意者请发送简历至tmezp@tencent.com,请注明来自公众号,我们将优先拜读。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-07-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯音乐技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 问题初步分析及解决
      • 隐藏问题
    • 问题根源
      • 总结
        • 动画停止
        • 内存泄露检测
    相关产品与服务
    检测工具
    域名服务检测工具(Detection Tools)提供了全面的智能化域名诊断,包括Whois、DNS生效等特性检测,同时提供SSL证书相关特性检测,保障您的域名和网站健康。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档