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

前言

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

先说下该动画: 进入 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)

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

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏c#开发者

datagrid资料+ by iCeSnaker - Program rhapsody

datagrid资料+ by iCeSnaker - Program rhapsody 关于datagrid的打印 http://www.chinaaspx.c...

3659
来自专栏葡萄城控件技术团队

如何将GridViewEX升级到UWP(Universal Windows Platform)平台

引言 上一篇文章中,我们主要讲解了如何在保证GridView控件的用户体验基础上,扩展GridView生成GridViewEx控件,增加动态添加新分组功能等,本...

2288
来自专栏Jaycekon

Phantomjs+Nodejs+Mysql数据抓取(2.抓取图片)

概要 这篇博客是在上一篇博客Phantomjs+Nodejs+Mysql数据抓取(1.抓取数据) http://blog.csdn.net/jokerko...

3705
来自专栏程序员的SOD蜜

“老坛泡新菜”:SOD MVVM框架,让WinForms焕发新春

火热的MVVM框架 最近几年最热门的技术之一就是前端技术了,各种前端框架,前端标准和前端设计风格层出不穷,而在众多前端框架中具有MVC,MVVM功能的框架成为耀...

4156
来自专栏林德熙的博客

win10 uwp 萤火虫效果 安装 win2d创建界面后台的方法核心代码

本文在Nukepayload2指导下,使用他的思想用C#写出来。 本文告诉大家,如何使用 win2d 做出萤火虫效果。

1281
来自专栏向治洪

iOS开发入门笔记

iOS开发入门笔记 本文面向已有其它语言(如Java,C,PHP,Javascript)编程经验的iOS开发初学者,初衷在于让我的同事一小时内了解如何开始开发i...

4606
来自专栏pangguoming

c#以POST方式模拟提交表单

这是我一年前写的一个用C#模拟以POST方式提交表单的代码,现在记录在下面,以免忘记咯。那时候刚学C#~忽忽。。很生疏。。代码看上去也很幼稚 臃肿不堪 #reg...

3469
来自专栏xx_Cc的学习总结专栏

iOS-世界那么大,CoreLocation带你去看看

3009
来自专栏流媒体人生

内嵌Activex的Activex插件开发

介绍:   如今在许多流媒体视频网站(youku,tudou......)我们都会发现,观看视频之前都会有一段时间的广告,甚至在观看视频途中也会插入一些 广...

993
来自专栏跟着阿笨一起玩NET

C#自定义开关按钮控件--附带第一个私活项目截图

进入智能手机时代以来,各种各样的APP大行其道,手机上面的APP有很多流行的元素,开关按钮个人非常喜欢,手机QQ、360卫士、金山毒霸等,都有很多开关控制一些操...

2581

扫码关注云+社区

领取腾讯云代金券