文章目录

  • 一、NSTimer
    • 1. 工作原理
    • 2. 初始化方法的区别
    • 3. 8种初始化方法:
    • 4. 不work的原因
    • 5. 内存泄露
    • 6. 对self的强引用的解决方案
      • 6.1. target 使用类对象
      • 6.2. 代理类(NSProxy)弱引用self + 消息转发
      • 6.3. CoreFundation
  • 二、GCD定时器
  • 三、CADisplayLink定时器
    • 1. 初始化:
    • 2. 频率:
    • 3. 回调方法:
    • 4. 控制+销毁:
  • 四、对比总结

在iOS里用个 Timer(定时器)真的是太麻烦了,一不小心就不work了,一不小心又导致内存泄露了~

反正就是得非常注意,下面就来聊聊定时器:

一、NSTimer

1. 工作原理

首先我们得了解Timer是怎么工作的:

首先它需要加到RunLoop中,RunLoop会在固定时间触发Timer的回调。这个Timer是被存放在RunLoopModel_timers数组里,是强引用的。(之前的文章有介绍RunLoop的结构)

因此我们需要在持有Timer的对象(如:ViewController,本文就以ViewControllerTimer的持有对象举例说明,下文用self表示)的dealloc方法里销毁Timer:调用其invalidate方法。(所以持有仅仅是为了销毁)

invalidate方法:会将TimerRunLoop中移除;并释放Timer持有的资源(targetuserInfoBlock)

2. 初始化方法的区别

NSTimer的初始化方法中只有scheduled开头的,会自动把Timer添加到当前的RunLoopDefaultMode里。而其他初始化方法则需要我们手动加入RunLoop

但为了让其能在ScrollView滑动的时候work,所以不管什么方式创建的我们都需要操作如下:
方法1:添加到CommonMode
方法2:添加到DefaultTracking 的Mode中
(app启动后系统默认将DefaultTracking声明为common属性了)(之前RunLoop的文章有介绍)

3. 8种初始化方法:

8种初始化方法,不带block的都会导致内存泄露,需要进行处理:

self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerOne) userInfo:nil repeats:YES];
self.timer2 = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {NSLog(@"timer 2");
}]; // iOS 10self.timer3 = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerThree) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer3 forMode:NSRunLoopCommonModes];
self.timer4 = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {NSLog(@"timer 4");
}]; // iOS 10
[[NSRunLoop currentRunLoop] addTimer:self.timer4 forMode:NSRunLoopCommonModes];self.timer5 = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:2] interval:1 target:self selector:@selector(timerFive) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer5 forMode:NSRunLoopCommonModes];self.timer6 = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:2] interval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {NSLog(@"timer 6");
}]; // iOS 10
[[NSRunLoop currentRunLoop] addTimer:self.timer6 forMode:NSRunLoopCommonModes];NSMethodSignature *signature7 = [self methodSignatureForSelector:@selector(timerSeven:)];
self.invocation7 = [NSInvocation invocationWithMethodSignature:signature7]; // 必须持有
[self.invocation7 setSelector:@selector(timerSeven:)];
[self.invocation7 setTarget:self];
NSString *name7 = @"moxiaoyan7";
[self.invocation7 setArgument:&name7 atIndex:2]; // 前面有两个隐藏参数
self.timer7 = [NSTimer scheduledTimerWithTimeInterval:1 invocation:self.invocation7 repeats:YES];NSMethodSignature *signature8 = [self methodSignatureForSelector:@selector(timerEight:)];
self.invocation8 = [NSInvocation invocationWithMethodSignature:signature8];
[self.invocation8 setSelector:@selector(timerEight:)];
[self.invocation8 setTarget:self];
NSString *name8 = @"moxiaoyan8";
[self.invocation8 setArgument:&name8 atIndex:2];
self.timer8 = [NSTimer timerWithTimeInterval:1 invocation:self.invocation8 repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer8 forMode:NSRunLoopCommonModes];

4. 不work的原因

  1. 滑动时切换到Tracking Mode
// 解决方案一:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 解决方案二:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
  1. 子线程的RunLoop没有创建
// 不获取就不会主动创建
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 保持线程常驻
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];

5. 内存泄露

NSTimer 为什么这么容易导致内存泄露,很重要的一点是因为 RunLoop 会强引用 NSTimer,(系统实现的无法做修改)。
所以开发者必须在恰当的时机将NSTimer释放掉。
而一般最佳释放时机为持有 NSTimerselfdealloc 方法里:

- (void)dealloc {[self.timer invalidate];self.timer = nil;
}

iOS10之前的方法,需要传入target(一般我们用self)作为代理,执行需要定时触发的方法。
因为NSTimer会强引用传入的target(这也是系统实现的无法修改)。
当开发者直接传入 self 时,就导致了 self 无法被释放,开发者在 dealloc 里释放 NSTimer 的代码也不会执行,从而导致了内存泄露:RunLoop -> NSTimer -> self (不是引用环,但是无法释放)

iOS10苹果新出了3个方法,采用block的形式实现代理方法,不需要传入self(block中还是需要用weakSelf),从而保证了selfdealloc的执行


6. 对self的强引用的解决方案

6.1. target 使用类对象

为NSTimer创建分类,实现block的方式传入代理方法

#import "NSTimer+MOBlock.h"
@implementation NSTimer (MOBlock)
+ (NSTimer *)mo_scheduledTimerWithTimeInterval:(NSTimeInterval)ti repeats:(BOOL)yesOrNo block:(void(^)(NSTimer *timer))block {return [self scheduledTimerWithTimeInterval:titarget:self // 类对象无需回收,所以不用担心selector:@selector(blockInvoke:)userInfo:[block copy] // 需要copy到堆中,否则会被释放repeats:yesOrNo];
}+ (void)blockInvoke:(NSTimer *)timer {void(^block)(NSTimer *timer) = timer.userInfo;if (block) {block(timer);}
}
@end
// 使用如下:
self.timerFirst = [NSTimer mo_scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {NSLog(@"优化后的 first timer");
}];

6.2. 代理类(NSProxy)弱引用self + 消息转发

创建一个类继承自NSProxy,弱引用 target,并将消息转发给 target:

@interface MOProxy : NSProxy
@property (nonatomic, weak) id target;
@end@implementation MOProxy
- (id)forwardingTargetForSelector:(SEL)aSelector {return _target;
}
@end

使用时,创建该类对象,并将需要响应消息的对象赋值给target:

MOProxy *proxy = [[MOProxy alloc] init];
proxy.target = self;
self.timerSecond = [NSTimer scheduledTimerWithTimeInterval:1target:proxyselector:@selector(timerSecond:)userInfo:@{@"name": @"cool"}repeats:YES];

另外YYKit库的Utility有一个YYWeakProxy的弱代理类,(具体实现可以点链接过去看)同理,使用如下:

  YYWeakProxy *weakObj = [YYWeakProxy proxyWithTarget:self];self.timerSecond = [NSTimer scheduledTimerWithTimeInterval:1target:weakObjselector:@selector(timerSecond:)userInfo:@{@"name": @"cool"}repeats:YES];

6.3. CoreFundation

开源库 BlocksKit 的NSTimer+BlocksKit.m
使用 CFRunLoopTimerCreateWithHandler 实现了 Timer,也可以规避 Timer 持有 target 的问题:

+ (instancetype)bk_timerWithTimeInterval:(NSTimeInterval)inSeconds repeats:(BOOL)repeats usingBlock:(void (^)(NSTimer *timer))block {NSParameterAssert(block != nil);CFAbsoluteTime seconds = fmax(inSeconds, 0.0001);CFAbsoluteTime interval = repeats ? seconds : 0;CFAbsoluteTime fireDate = CFAbsoluteTimeGetCurrent() + seconds;return (__bridge_transfer NSTimer *)CFRunLoopTimerCreateWithHandler(NULL, fireDate, interval, 0, 0, (void(^)(CFRunLoopTimerRef))block);
}

最后不要忘了在selfdealloc中释放NSTimer !!!


二、GCD定时器

GCDTimer完美避过NSTimer的3大缺陷:RunLoop、Thread、Leaks
因为NSTimer依赖RunLoop实现的,所以:
1.默认在RunLoop的DefaultMode下计时 (导致scrollView滑动不work)
2.RunLoop对NSTimer保持强引用 (容易导致内存泄露问题)
3.子线程中默认不创建RunLoop,导致NSTimer失效
4.NSTimer的创建和撤销必须在同一个线程操作,不能跨线程操作

// GCD 定时器(不会被RunLoop强引用)
// GCD 一次性定时器
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"GCD 一次性计时器");
});
// GCD 重复性定时器
@property (nonatomic, strong) dispatch_source_t gcdTimer;self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)2 * NSEC_PER_SEC);
uint64_t duration = (uint64_t)(2.0 * NSEC_PER_SEC);
// 参数:sourceTimer 开始时间 循环间隔 精确度(这里写的0.1s)
dispatch_source_set_timer(self.gcdTimer, start, duration, 0.1 * NSEC_PER_SEC);
__weak typeof(self) weakSelf = self;
dispatch_source_set_event_handler(self.gcdTimer, ^{NSLog(@"GCD 重复性计时器");dispatch_suspend(weakSelf.gcdTimer); // 暂停sleep(1);dispatch_resume(weakSelf.gcdTimer); // 恢复
});
dispatch_resume(self.gcdTimer);
// cancel 销毁,不可再使用
//  dispatch_source_cancel(self.gcdTimer);

三、CADisplayLink定时器

适用于界面的不停重绘,如:视频播放的时候需要不停的获取下一帧的数据用于界面渲染。不能被继承。

1. 初始化:

@property (nonatomic, strong) CADisplayLink *link;
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLink:)];
[self.link setPaused:YES]; // 先暂停,需要的时候再开启
// 依赖runloop的循环工作
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

2. 频率:

这里需要了解一个概念:

FPS:帧率,每秒刷新的最大次数。于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约10至12帧的时候,就会认为是连贯的,此现象称之为视觉暂留。
iOS现存的设备是60HZ,即60次每秒,可以通过[UIScreen mainScreen].maximumFramesPerSecond获得

所以这里selector被调用的频率是:FPS/s,(如:目前的60次/s)

控制selector触发频率的属性

  • iOS10之前用frameInterval,默认1
self.link.frameInterval = 2; // 30次/s

即:每次时间间隔 duration = FPS / frameInterval
如:按目前设备的FPS算: 60/1 = 0.016667s 刷新1次
此为最理想的状态, 如果CPU忙碌会跳过若干次回调

当值小于1时,结果不可预测
(大概是频率已经大于屏幕刷新频率了, 能否及时绘制每次计算的数值得看CPU的负载情况, 此时就会出现严重的丢帧现象)

iOS10之后已被弃用, 因为每次的时间间隔会根据FPS的不同而不用, 以后某台设备提升了FPS, 此时duration在不同设备上的值就不一样了

  • iOS10之后用preferredFramesPerSecond,默认0,跟设备FPS一样
self.link.preferredFramesPerSecond = 30; // 30次/s

这个就比较好理解了,也不会因为FPS的不同,导致不同的频率

3. 回调方法:

- (void)displayLink:(CADisplayLink *)link {link.duration // 最大屏幕刷新时间间隔, 在selector首次被调用后才会被赋值link.timestamp // 上一帧时间戳link.targetTimestamp // 下一帧时间戳// targetTimestamp - timestamp: 实际刷新时间间隔 (据此确定下一次需要display的内容)
}

4. 控制+销毁:

- (void)startLink {[self.link setPaused:NO]; // 恢复
}
- (void)pauseLink {[self.link setPaused:YES]; // 暂停
}
- (void)stopLink {[self.link invalidate]; // removeFromRunLoop, 释放target
}

它跟NSTimer一样:依赖RunLoop,会对target造成强引用
解决的办法也可以跟NStimer一样


四、对比总结

以上说了iOS的3中计时器,各有优缺点:

NSTimer:适用于各种计时/循环处理的事件,频率计算可以按秒计

CADisplayLink:精确度比较高,频率计算相对于每秒而言,适用情况比较单一,一般用于界面的不停重绘,需要保证刷新效率的事情。如:视频播放的时候需要不停的获取下一帧的数据用于界面渲染

以上两者原理都差不多,需要依赖RunLoop,并指定Mode实现;只是频率的计算方式不同;还有就是精确度,iOS10后为了尽量避免在NSTimer触发时间到了而去中断当前处理的任务,NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间范围。可以看得出NSTimer不太强调多高的精确度。

GCD 比较精准,不依赖于RunLoop,用dispatch_source_t实现,代码较多可控性强

github Demo 地址

参考:
NSTimer使用解析
NSTimer定时器进阶——详细介绍,循环引用分析与解决
GCD实现多个定时器,完美避过NSTimer的三大缺陷(RunLoop、Thread、Leaks)

CADisplayLink官方文档
CADisplayLink
CADisplayLink的使用(一)
CADisplayLink学习笔记

iOS_定时器:NSTimer、GCDTimer、DisplayLink相关推荐

  1. iOS定时器-- NSTimer 和CADisplaylink

    iOS定时器-- NSTimer 和CADisplaylink 一.iOS中有两种不同的定时器: 1.  NSTimer(时间间隔可以任意设定,最小0.1ms)// If seconds is les ...

  2. iOS中定时器NSTimer的开启与关闭

    调用一次计时器方法: [cpp] view plain copy   myTimer = [NSTimer scheduledTimerWithTimeInterval:1.5 target:self ...

  3. iOS中定时器NSTimer的使用

    1.初始化 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelect ...

  4. IOS 定时器 NSTimer

    定时器对象,在OC中,定时器对象是NSTimer类型 //ViewController.h #import <UIKit/UIKit> @interface ViewController: ...

  5. ios nstimer实现延时_iOS中定时器NSTimer的使用

    1.初始化 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelect ...

  6. IOS中定时器NSTimer

    调用一次计时器方法: [cpp]  view plain copy   myTimer = [NSTimer scheduledTimerWithTimeInterval:1.5 target:sel ...

  7. iOS中的三大定时器

    iOS开发中定时器经常会用到,iOS中常用的定时器有三种,分别是NSTime,CADisplayLink和GCD. NSTimer 方式1 // 创建定时器NSTimer *timer = [NSTi ...

  8. 【整理】NSTimer使用及注意事项

    一.NSTimer的创建 // 创建一个定时器,但是么有添加到运行循环,我们需要在创建定时器后手动的调用 NSRunLoop 对象的 addTimer:forMode: 方法. + (NSTimer ...

  9. iOS中关于NSTimer使用知多少

    看到这个标题,你可能会想NSTimer不就是计时器吗,谁不会用,不就是一个能够定时的完成任务的东西吗? 我想说你知道NSTimer会retain你添加调用方法的对象吗?你知道NSTimer是要加到ru ...

最新文章

  1. Day 20: 斯坦福CoreNLP —— 用Java给Twitter进行情感分析
  2. python服务器稳定性,一种基于Python服务器稳定性测试的方法技术
  3. CodeForces - 1354E Graph Coloring(dfs判断二分图+dp)
  4. spring boot缓存_Spring Boot和缓存抽象
  5. LeetCode 1955. 统计特殊子序列的数目
  6. 30岁以上的女人应选择什么品牌的眼霜?
  7. 第二模块:函数编程 第1章·文件处理、函数、装饰器、迭代器、内置方法
  8. 链表简介(二)——在单向链表中插入节点
  9. MOS管防倒灌电路设计及其过程分析
  10. JS基础-百度换肤案例
  11. 【Python案例】用某度AI接口实现抠图并改图片底色
  12. 广告联盟中CPC CPA CPM CPS CPV分别是什么意思
  13. 微信二次开发-windows版微信Hook开发SDK之C#版
  14. Ubuntu 卸载程序
  15. OpenFoam编程笔记——starccm网格转openfoam格式
  16. 造车新势力平均月薪 15367 元,自动驾驶算法岗年薪百万
  17. 佳能eosr控制环能否计算机控制,镜头不够EF口来凑 佳能EOS R转接性能测试
  18. 有关于论文投稿的问题
  19. 所在地区级别_在人所在的地方
  20. 激光打印机的粉盒装粉

热门文章

  1. 二、索引的创建和设计原则
  2. 2015的读书计划和读书心得
  3. 【报告分享】2021-2022中国男女婚恋观报告-百合世纪佳缘(附下载)
  4. 【报告分享】2020年Q4抖音广告投放分析-AppGrowing(附下载)
  5. Ubuntu下代理工具electron的安装
  6. 记录在小程序中前端调用百度 Ocr 识别身份证信息
  7. 对UI自动化测试的一些感悟
  8. 数字化养老赋能,智慧养老落实——苏州经贸职业技术学院来新导智能社会实践
  9. 三星联合独立调研机构调查 Note7事件终水落石出
  10. 保密计算机能用旧显示器,旧液晶电视机别扔,可作电脑显示器用