文章目录
代码语言:txt复制- [一、NSTimer](https://cloud.tencent.com/developer)
- [1. 工作原理](https://cloud.tencent.com/developer)
- [2. 初始化方法的区别](https://cloud.tencent.com/developer)
- [3. 8种初始化方法:](https://cloud.tencent.com/developer)
- [4. 不work的原因](https://cloud.tencent.com/developer)
- [5. 循环引用](https://cloud.tencent.com/developer)
- [6. 对self的强引用的解决方案](https://cloud.tencent.com/developer)
- [二、GCD定时器](https://cloud.tencent.com/developer)
- [三、CADisplayLink定时器](https://cloud.tencent.com/developer)
- [1. 初始化:](https://cloud.tencent.com/developer)
- [2. 频率:](https://cloud.tencent.com/developer)
- [3. 回调方法:](https://cloud.tencent.com/developer)
- [4. 控制 销毁:](https://cloud.tencent.com/developer)
- [四、对比总结](https://cloud.tencent.com/developer)
在iOS里用个 Timer
(定时器)真的是太麻烦了,一不小心就不work了,一不小心又导致内存泄露了~
反正就是得非常注意,下面就来聊聊定时器:
一、NSTimer
1. 工作原理
首先我们得了解Timer
是怎么工作的:
首先它需要加到RunLoop
中,RunLoop
会在固定时间触发Timer
的回调。这个Timer
是被存放在RunLoop
的Model
的_timers
数组里,是强引用的。(之前的文章有介绍RunLoop的结构)
因此我们需要在持有Timer
的对象(如:ViewController
,本文就以ViewController
为Timer
的持有对象举例说明,下文用self
表示)的dealloc
方法里销毁Timer
:调用其invalidate
方法。(所以持有仅仅是为了销毁)
invalidate
方法:会将Timer
从RunLoop
中移除;并释放Timer
持有的资源(target
、userInfo
、Block
)
2. 初始化方法的区别
NSTimer
的初始化方法中只有scheduled
开头的,会自动把Timer
添加到当前的RunLoop
的DefaultMode
里。而其他初始化方法则需要我们手动加入RunLoop
但为了让其能在ScrollView
滑动的时候work,所以不管什么方式创建的我们都需要操作如下:
方法1:添加到CommonMode
中
方法2:添加到Default
和Tracking
的Mode中
(app启动后系统默认将Default
和Tracking
声明为common
属性了)(之前RunLoop的文章有介绍)
3. 8种初始化方法:
8种初始化方法,不带block的都会导致内存泄露,需要进行处理:
代码语言:javascript复制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 10
self.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的原因
- 滑动时切换到
Tracking Mode
:
// 解决方案一:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
代码语言:javascript复制// 解决方案二:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
- 子线程的
RunLoop
没有创建
// 不获取就不会主动创建
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 保持线程常驻
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
5. 循环引用
iOS10苹果新出了3个方法,采用block
的形式实现代理方法,不需要传入self
(block中还是需要用weakSelf
),从而保证了self
的dealloc
的执行
iOS10之前的方法,需要传入target
(一般我们用self
)作为代理,执行需要定时触发的方法。
因而target
对self
有强引用,进而导致self
的dealloc
方法无法触发,从而导致Timer
的invalidate
也无法执行,就内存泄露了。
虽说这里有个强引用环,但造成泄露的主要原因是:
RunLoop
对Timer
的强引用,导致Timer
需要我们手动释放,释放最适宜的时机又是self
的dealloc
方法。所以我们必须要保证self
的正常释放,因而最关键的强引用是target
对self
的这条!!! (因为不管self
对Timer
有木有强引用,Timer
都不会被释放,因为RunLoop
的关系)
6. 对self的强引用的解决方案
- block方式添加Target-Action 为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:ti
target: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");
}];
- 代理类(NSProxy)弱引用self 消息转发 创建一个类继承自NSProxy,并声明一个weak属性target:
// MOProxy.h
@interface MOProxy : NSProxy
@property (nonatomic, weak) id target;
@end
实现文件里,将消息转发给target:
代码语言:javascript复制// MOProxy.m
@implementation MOProxy
// 消息转发的第二次机会
- (id)forwardingTargetForSelector:(SEL)aSelector {
return _target; // 如果target为nil,则触发第三次机会
}
// 消息转发的第三次机会
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [NSObject instanceMethodSignatureForSelector:@selector(init)]; // 获取了init方法的方法签名
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null]; // 给nil发送init方法,不会crash
}
@end
使用时,创建该类对象,并将需要响应消息的对象赋值给target:
代码语言:javascript复制MOProxy *proxy = [MOProxy alloc];
proxy.target = self;
self.timerSecond = [NSTimer scheduledTimerWithTimeInterval:1
target:proxy
selector:@selector(timerSecond:)
userInfo:@{@"name": @"cool"}
repeats:YES];
最后不要忘了在self
的dealloc
中调用Timer
的invalidate
方法~
另外YYKit库的Utility有一个YYWeakProxy的弱代理类,(具体实现可以点链接过去看)同理,使用如下:
代码语言:javascript复制 YYWeakProxy *weakObj = [YYWeakProxy proxyWithTarget:self];
self.timerSecond = [NSTimer scheduledTimerWithTimeInterval:1
target:weakObj
selector:@selector(timerSecond:)
userInfo:@{@"name": @"cool"}
repeats:YES];
二、GCD定时器
GCDTimer完美避过NSTimer的3大缺陷:RunLoop、Thread、Leaks
因为NSTimer依赖RunLoop实现的,所以:
1.默认在RunLoop的DefaultMode下计时 (导致scrollView滑动不work)
2.RunLoop对NSTimer保持强引用 (容易导致内存泄露问题)
3.子线程中默认不创建RunLoop,导致NSTimer失效
4.NSTimer的创建和撤销必须在同一个线程操作,不能跨线程操作
代码语言:javascript复制// 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. 初始化:
代码语言:javascript复制@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. 回调方法:
代码语言:javascript复制- (void)displayLink:(CADisplayLink *)link {
link.duration // 最大屏幕刷新时间间隔, 在selector首次被调用后才会被赋值
link.timestamp // 上一帧时间戳
link.targetTimestamp // 下一帧时间戳
// targetTimestamp - timestamp: 实际刷新时间间隔 (据此确定下一次需要display的内容)
}
4. 控制 销毁:
代码语言:javascript复制- (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学习笔记