浅析 NSTimer 和 CADisplayLink 内存泄漏

谈论 NSTimer & CADisplayLink 内存泄漏,要理解 NSTimer & CADisplayLink 的基础概念,下面通过一个倒计时的实现的 demo 进入正题。

  • 第一种就是直接在 TableViewCell 上使用 NSTimer,然后添加到当前线程所对应的 RunLoop 中的 commonModes 中。
  • 第二种是通过 Dispatch 中的 TimerSource 来实现定时器。
  • 第三种是使用 CADisplayLink 来实现。

UITableViewCell 为例:

一、在 Cell 中直接使用 NSTimer

首先我们按照常规做法,直接在 UITableViewCell 上添加相应的 NSTimer, 并使用 scheduledTimer 执行相应的代码块。
代码如下所示:

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier]) {
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(countDown:) userInfo:nil repeats:YES];
    }
    return self;
}

- (void)countDown:(NSTimer *)timer {
    NSDateFormatter *dateformatter = [[NSDateFormatter alloc] init];
    dateformatter.dateFormat = @"HH:mm:ss";
    self.textLabel.text = [NSString stringWithFormat:@"倒计时:%@", [dateformatter stringFromDate:[NSDate date]]];
}

- (void)dealloc {
    [self.timer invalidate];
    NSLog(@"%@_%s", self.class, __func__);
}

2017-06-07 13:36:11.981473+0800 NSTimer&CADisplayLink[24050:457782] MYNSTimerBlockViewController_-[MYNSTimerBlockViewController dealloc]

二、DispatchTimerSource

接下来我们就在 TableViewCell 上添加 DispatchTimerSource,然后看一下运行效果。当然下方代码片段我们是在全局队列中添加的 DispatchTimerSource,在主线程中进行更新。当然我们也可以在 mainQueue 中添加 DispatchTimerSource,这样也是可以正常工作的。当然我们不建议在 MainQueue 中做,因为在编程时尽量的把一些和主线程关联不太大的操作放到子线程中去做。
代码如下所示:

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier]) {
        [self countDown];
    }
    return self;
}

- (void)countDown {
    // 倒计时时间
    __block NSInteger timeOut = 60.0f;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 每秒执行一次
    dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(_timer, ^{
        
        // 倒计时结束,关闭
        if (timeOut <= 0) {
            dispatch_source_cancel(_timer);
            dispatch_async(dispatch_get_main_queue(), ^{
                self.detailTextLabel.text = @"倒计时结束";
            });
        } else {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSDateFormatter *dateformatter = [[NSDateFormatter alloc] init];
                dateformatter.dateFormat = @"HH:mm:ss";
                self.detailTextLabel.text = [NSString stringWithFormat:@"倒计时%@", [dateformatter stringFromDate:[NSDate date]]];
            });
            timeOut--;
        }
    });
    dispatch_resume(_timer);
}

- (void)dealloc {
    NSLog(@"%@_%s", self.class, __func__);
}

2017-06-07 13:49:43.630398+0800 NSTimer&CADisplayLink[24317:476977] MYNSTimerBlockViewController_-[MYNSTimerBlockViewController dealloc]

三、CADisplayLink

接下来我们来使用 CADisplayLink 来实现定时器功能,CADisplayLink 可以添加到 RunLoop 中,每当屏幕需要刷新的时候,runloop 就会调用 CADisplayLink 绑定的 target 上的 selector,这时 target 可以读到 CADisplayLink 的每次调用的时间戳,用来准备下一帧显示需要的数据。例如一个视频应用使用时间戳来计算下一帧要显示的视频数据。在UI做动画的过程中,需要通过时间戳来计算UI对象在动画的下一帧要更新的大小等等。

可以设想一下,我们在动画的过程中,runloop 被添加进来了一个高优先级的任务,那么,下一次的调用就会被暂停转而先去执行高优先级的任务,然后在接着执行 CADisplayLink 的调用,从而造成动画过程的卡顿,使动画不流畅。

下方代码,为了不让屏幕的卡顿等引起的主线程所对应的 RunLoop 阻塞所造成的定时器不精确的问题。我们开启了一个新的线程,并且将 CADisplayLink 对象添加到这个子线程的 RunLoop 中,然后在主线程中更新UI即可。
具体代码如下:

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier]) {
        dispatch_queue_t disqueue =  dispatch_queue_create("com.countdown", DISPATCH_QUEUE_CONCURRENT);
        dispatch_group_t disgroup = dispatch_group_create();
        dispatch_group_async(disgroup, disqueue, ^{
            self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(countDown)];
            [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        });
    }
    return self;
}

- (void)countDown {
    NSDateFormatter *dateformatter = [[NSDateFormatter alloc] init];
    dateformatter.dateFormat = @"HH:mm:ss";
    self.detailTextLabel.text = [NSString stringWithFormat:@"倒计时%@", [dateformatter stringFromDate:[NSDate date]]];
}

- (void)dealloc {
    [self.link invalidate];
    NSLog(@"%@_%s", self.class, __func__);
}

2017-06-07 13:49:43.630398+0800 NSTimer&CADisplayLink[24317:476977] MYNSTimerBlockViewController-[MYNSTimerBlockViewController dealloc]

得出结论

从上面的三种 demo 可以看出 UITableViewCell 没有被释放,由此得出结论,当 UITableViewCell 里面强引用了定时器,定时器又强引用了 UITableViewCell,这样两者的 retainCount 值一直都无法为0,于是内存始终无法释放,导致内存泄露。所谓的内存泄露就是本应该释放的对象,在其生命周期结束之后依旧存在。

原因

定时器的运行需要结合一个 NSRunLoop,同时 NSRunLoop 对该定时器会有一个强引用,这也是为什么我们不能对 NSRunLoop 中的定时器进行强引的原因。

由于 NSRunLoop 对定时器有引用,定时器怎样才能被释放掉。

Removes the object from all runloop modes (releasing the receiver if it has been implicitly retained) and releases the target object.

据官方介绍可知,- invalidate 做了两件事,首先是把本身(定时器)从 NSRunLoop 中移除,然后就是释放对 target 对象的强引用,从而解决定时器带来的内存泄漏问题。

从上面的 demo 中看出,在 UITableViewCelldealloc 方法中调用 invalidate 方法,并没有解决问题。

分析

这里使用下 Xcode8 调试黑科技 Memory Graph 来检测下内存泄漏:

image.png

从图中可以看出:

NSRunLoop  ---> 定时器 ---> UITableViewCell

导致 UITableViewCell 中没有释放掉定时器。

self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(countDown)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

从代码中看出,Target 直接设置成 self 会造成内存泄, CADisplayLink 会强引用 Target。当 CADisplayLink 添加到 NSRunLoop 中,NSRunLoop 会强引用 CADisplayLink。如果仅仅在 dealloc 中调用 CADisplayLinkinvalidate 方法是没用的,因为 NSRunLoop 的存在 CADisplayLink 不会被释放,Target 被强引用,Targetdealloc 方法不会被调用,CADisplayLinkinvalidate 方法也不被调用,CADisplayLink 不会从 NSRunLoop 中移除,从而导致内存泄漏。

NSRunLoop 的问题请查看这里

解决方案

1、Target

为了解决定时器与 Target 之间类似死锁的问题,我们会将定时器中的 target 对象替换成定时器自己,采用分类实现。

#import "NSTimer+TimerTarget.h"

@implementation NSTimer (TimerTarget)

+ (NSTimer *)my_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                    repeat:(BOOL)yesOrNo 
                     block:(void (^)(NSTimer *))block {
    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(startTimer:) userInfo:[block copy] repeats:yesOrNo];
}

+ (void)startTimer:(NSTimer *)timer {
    void (^block)(NSTimer *timer) = timer.userInfo;
    if (block) {
        block(timer);
    }
}
@end

2、Proxy

这种方式就是创建一个 NSProxy 子类 TimerProxyTimerProxy 的作用是什么呢?就是什么也不做,可以说只会重载消息转发机制,如果创建一个 TimerProxy 对象将其作为定时器的 target,专门用于转发定时器消息至 Target 对象,那么问题是不是就解决了呢。

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.25 target:[TimerProxy timerProxyWithTarget:self] selector:@selector(startTimer) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

self.timer = timer;

3、NSTimer Block

还有一种方式就是采用Block,iOS 10增加的API。

+ scheduledTimerWithTimeInterval:repeats:block:

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.25 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"MYNSTimerTargetController timer start");
}];

[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.timer = timer;

Example

浅析NSTimer & CADisplayLink内存泄漏

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 偶得前言 NSRunLoop与定时器 [- invalidate的作用](#- invalidate的作用) 我们...
    tingxins阅读 889评论 0 11
  • 概述 RunLoop作为iOS中一个基础组件和线程有着千丝万缕的关系,同时也是很多常见技术的幕后功臣。尽管在平时多...
    阳明先生_x阅读 1,088评论 0 17
  • iOS刨根问底-深入理解RunLoop 概述 RunLoop作为iOS中一个基础组件和线程有着千丝万缕的关系,同时...
    reallychao阅读 820评论 0 6
  • iOS面试题目100道 1.线程和进程的区别。 进程是系统进行资源分配和调度的一个独立单位,线程是进程的一个实体,...
    有度YouDo阅读 29,872评论 8 137
  • 概述RunLoop作为iOS中一个基础组件和线程有着千丝万缕的关系,同时也是很多常见技术的幕后功臣。尽管在平时多数...
    飞天猪Pony阅读 514评论 0 7