学计算机的那个

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

0%

RunLoop之NSTimer

NSTimer 的创建

NSTimer的创建通常有两种方式,一种是以 scheduledTimerWithTimeInterval 为开头的类方法 。这些方法在创建了 NSTimer 之后会将这个 NSTimerNSDefaultRunLoopMode 模式放入当前线程的 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 避坑指南