专栏首页QQ音乐技术团队的专栏一个循环动画引起的内存泄露问题总结

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

前言

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

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

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

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

问题初步分析及解决

最开始该代码如下:

- (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 重新启动动画):

[NSObject cancelPreviousPerformRequestsWithTarget:@selector(startBaseAnimation)];

或者不用 perfomrSelector,改用 dispatch_after,block 里用 weak 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];
});

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

隐藏问题

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

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

- (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 动画组来实现的方案:

- (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 秒,并且该帧动画的开始时间要另外设置一下,改为在 平移动画完成后:

hideAni.beginTime = moveDuration;

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

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,所以是要耗电的,可能系统做了优化来节电。

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

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,请注明来自公众号,我们将优先拜读。

本文分享自微信公众号 - 腾讯音乐技术团队(gh_287053a877e6)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-07-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [Android] Toast问题深度剖析(一)

    伴随着我们开发的深入,Toast 的问题也逐渐暴露出来。本文章就将解释 Toast 这些问题产生的具体原因。

    QQ音乐技术团队
  • Material Design技术分享

      因项目需要接触了近一个月的Material Design,之前只觉得它美丽而神秘,真正接触起来发现确实不错。针对这段时间做个小总结,也给广大战友们分享点踩坑...

    QQ音乐技术团队
  • Lottie : 让动画如此简单

    Lottie是Airbnb开源的一个面向 iOS、Android、React Native 的动画库,可实现非常复杂的动画,使用也及其简单,极大释放人力,值得一...

    QQ音乐技术团队
  • 解决克隆系统网卡名字不是默认eth0的问题

    设备上有2个网卡,在设置也区别了eth0和eth1,直到设置eth0无效时才发现有了问题。 克隆后在ifconfig时候发现网卡名字eth3 或者eth4 而不...

    张琳兮
  • 【Ubuntu快速上手】五、Ubuntu环境下Apache Server安装&配置

    KenTalk
  • 工具类学习-CollectionUtils

    版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。

  • less学习笔记(二)

    1.作用域:基本与javascrip的作用域相似,即先找局部变量,后找全局变量。找变量时遵循就近原则。

    HUC思梦
  • Dapr 将引领云原生时代应用和中间件的未来!!

    Dapr 是由微软发起的云原生开源新项目,在今年 2 月份刚刚发布了 v1.0 正式版本。虽然推出至今不过一年半时间,但 Dapr 发展势头十分迅猛,目前已经在...

    架构师修行之路
  • Python 技术篇-opencv读取中文路径图片报错及解决办法

    我们需要安装和使用 numpy 库,直接 pip install numpy 就好了。 用 numpy 读取处理图片,再对 numpy 处理后的图片数据进行转...

    小蓝枣
  • Hive应用:外部表链接内部表 原

    我们知道,Hive的外部表可以连接HDFS中的任何目录的数据,那么Hive的外部表是否可以连接本身的内部表的数据呢?

    云飞扬

扫码关注云+社区

领取腾讯云代金券