学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

RunLoop之NSTimer

NSTimer 的创建

NSTimer的创建通常有两种方式,一种是以 scheduledTimerWithTimeInterval 为开头的类方法 。这些方法在创建了 NSTimer 之后会将这个 NSTimer 以 NSDefaultRunLoopMode 模式放入当前线程的 RunLoop。

1
2
+ ( NSTimer *) scheduledTimerWithTimeInterval:invocation:repeats: 
+ ( NSTimer *) scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:

另一种是以 timerWithTimeInterval 为开头的类方法。这些方法创建的 NSTimer 并不能马上使用,还需要调用 RunLoop 的 addTimer:forMode: 方法将 NSTimer 放入 RunLoop,这样 NSTimer 才能正常工作。

1
2
+ ( NSTimer *) timerWithTimeInterval:invocation:repeats:
+ ( NSTimer *) timerWithTimeInterval:target:selector:userInfo:repeats:

Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

从 NSTimer 的官方文档可以得知,RunLoop 对加入其中的 NSTimer 会添加一个强引用。

以 timerWithTimeInterval 为开头的类方法创建出来的 NSTimer 需要手动加入 RunLoop, 这样 RunLoop 才会对这个 NSTimer 有强引用。若是我们使用 weak 修饰 NSTimer 变量,在 NSTimer 创建之后加入 RunLoop 之前,将 NSTimer 对象赋值给 weak 修饰的变量,那么对导致 NSTimer 对象被释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#import "TimerViewController.h"
@interface TimerViewController ()
// 使用 weak
@property (nonatomic,weak) NSTimer *timer;

@end

@implementation TimerViewController

- (void)viewDidLoad {
[super viewDidLoad];
// NSTimer 创建之后没有被自动加入 RunLoop
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(outputLog:) userInfo:nil repeats:YES];

if (self.timer == nil) {
NSLog(@"timer 被释放了");
}
}

- (void)outputLog:(NSTimer *)timer{
NSLog(@"it is log!");
}

@end

解决这个问题的方法也很简单, NSTimer 对象创建之后先加入 RunLoop 再赋值给变量。

1
2
3
4
5
6
7
8
9
10
11
12
// ...... 省略代码
- (void)viewDidLoad {
[super viewDidLoad];
// 创建 NSTimer
NSTimer *doNotWorkTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(outputLog:) userInfo:nil repeats:YES];
// NSTimer 加入 NSRunLoop
[[NSRunLoop currentRunLoop] addTimer:doNotWorkTimer forMode:NSDefaultRunLoopMode];
// 赋值给 weak 变量
self.timer = doNotWorkTimer;

}
// ...... 省略代码

NSTimer保留环

在使用NSTimer的时候,NSTimer会生成指向其使用者的引用,而其使用者如果也引用了NSTimer,那么就会生成保留环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#import <Foundation/Foundation.h>

@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end


@implementation EOCClass {
NSTimer *_pollTimer;
}


- (id)init {
return [super init];
}


- (void)dealloc {
[_pollTimer invalidate];
}

- (void)stopPolling {

[_pollTimer invalidate];
_pollTimer = nil;
}


- (void)startPolling {
_pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:@selector(p_doPoll)
userInfo:nil
repeats:YES];
}

- (void)p_doPoll {
// Poll the resource
}
@end

在EOCClass和_pollTimer之间形成了保留环,如果不主动调用stopPolling方法就无法打破这个保留环。通过回收该类的方法来打破此保留环也是行不通的,因为会将该类和NSTimer孤立出来,形成“孤岛”

通过“块”来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#import <Foundation/Foundation.h>
@interface NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;
@end

@implementation NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats
{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(eoc_blockInvoke:)
userInfo:[block copy]
repeats:repeats];

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

在NSTimer类里添加了方法,我们来看一下如何使用它:

1
2
3
4
5
6
7
8
9
- (void)startPolling {
__weak EOCClass *weakSelf = self;
_pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{

EOCClass *strongSelf = weakSelf;
[strongSelf p_doPoll];
}
repeats:YES];
}

这里,创建了一个self的弱引用,然后让块捕获了这个self变量,让其在执行期间存活。
一旦外界指向EOC类的最后一个引用消失,该类就会被释放,被释放的同时,也会向NSTimer发送invalidate消息(因为在该类的dealloc方法中向NSTimer发送了invalidate消息)。
而且,即使在dealloc方法里没有发送invalidate消息,因为块里的weakSelf会变成nil,所以NSTimer同样会失效。

RunLoop与NSTimer

NSTimer是由RunLoop来管理的,NSTImer其实就是CFRunLoopTimerRef,他们之间是toll-free bridged的,可以相互转换;

如果我们在子线程上使用NSTimer,就必须开启子线程的RunLoop,否则定时器无法生效

tableview滑动时NSTimer失效的问题

RunLoop同一时间只能运行在一种模式下,当我们滑动tableView/scrollview的时候,Runloop会切换到UITrackingRunLoopMode界面追踪模式下,如果我们的NSTimer是添加到RunLoop的defauleMode默认模式下的话,此时是会失效的。

解决:可以将NSTimer添加到RunLoop的ComonModes通用模式下,来保证无论在默认模式还是界面追踪模式下NSTimer都可以执行。

NSTimer的创建方式

自动添加到RunLoop的默认模式下

1
2
3
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];

自定义添加到RunLoop的某种模式下

1
2
3
4
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

注意: 如果是通过timerxxx开头方法创建的NSTimer是不会自动添加到RunLoop中的,所以一定要记得手动添加,否则NSTimer不生效

CFRunLoopAddTimer函数实现

CFRunLoopAddTimer()函数中会判断传入的modeName模式名称是不是
kCFRunLoopCommonModes通用模式,是的话就会将timer添加到RunLoop的 _commonModeItems 集合中,并同步该timer到 _commonModes 里的所有模式中,这样无论在默认模式还是界面追踪模式下NSTimer都可以执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {    
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
__CFRunLoopLock(rl);
if (modeName == kCFRunLoopCommonModes) { // 判断 modeName 是不是 kCFRunLoopCommonModes
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) { // 懒加载,判断 _commonModeItems 是否为空,是的话创建
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
CFSetAddValue(rl->_commonModeItems, rlt); // 将 timer 添加到 _commonModeItems 中
if (NULL != set) {
CFTypeRef context[2] = {rl, rlt}; // 将 timer 和 RunLoop 封装到 context 中
/* add new item to all common-modes */
// 遍历 commonModes,将 timer 添加到 commonModes 的所有模式下
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
......
}
}


static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
CFStringRef modeName = (CFStringRef)value;
CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
}
}

NSTImer和CADisplayLink存在的问题

不准时:NSTime和CADisplayLink底层都是基于RunLoop的CFRunLoopTimerRef的实现的,也就是说它们都依赖于RunLoop。如果RunLoop的任务过于繁重,会导致它们不准时

比如NSTimer每1.0秒就会执行一次任务,Runloop每进行一次循环,就会看一下NSTimer的时间是否达到1.0秒,是的话就执行任务。但是由于Runloop每一次循环的任务不一样,所花费的时间就不固定。假设第一次循环所花时间为 0.2s,第二次 0.3s,第三次 0.3s,则再过 0.2s 就会执行NSTimer的任务,这时候可能Runloop的任务过于繁重,第四次花了0.5s,那加起来时间就是 1.3s,导致NSTimer不准时。

CADisplayLink 是用于同步屏幕刷新的定时器,如果任务繁重的话,会出现丢帧现象的

解决方法:使用GCD的定时器,GCD 的定时器是直接跟系统内核挂钩的,而且它不依赖于RunLoop,所以它非常的准时。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);

//创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置时间(start:几s后开始执行; interval:时间间隔)
uint64_t start = 2.0; //2s后开始执行
uint64_t interval = 1.0; //每隔1s执行
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
//设置回调
dispatch_source_set_event_handler(timer, ^{
NSLog(@"%@",[NSThread currentThread]);
});
//启动定时器
dispatch_resume(timer);
NSLog(@"%@",[NSThread currentThread]);

self.timer = timer;
/*
2020-02-01 21:34:23.036474+0800 多线程[7309:1327653] <NSThread: 0x600001a5cfc0>{number = 1, name = main}
2020-02-01 21:34:25.036832+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:26.036977+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:27.036609+0800 多线程[7309:1327707] <NSThread: 0x600001a1e5c0>{number = 4, name = (null)}
*/

参考

NSTimer 避坑指南