iOS延迟执行的原理分析
NSObject和NSTimer
先看两段代码,留下两个问题
1
2
3
4
5
6
7
8
9
10
11
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:0];
NSLog(@"2");
});
}
- (void)test {
NSLog(@"3");
}
// 1 2
问题一:test为什么没有被执行?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CFAbsoluteTime refTime = CFAbsoluteTimeGetCurrent();
NSLog(@"start time 0.000000");
NSTimer *timer = [NSTimer timerWithTimeInterval:5.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fire %f",CFAbsoluteTimeGetCurrent() - refTime);
}];
timer.tolerance = 0.5;
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"before busy %f", CFAbsoluteTimeGetCurrent() - refTime);
NSInteger j;
for (long i = 0; i< 1000000000; i++) {
j = i*3;
}
NSLog(@"after busy %f", CFAbsoluteTimeGetCurrent() - refTime);
});
// start time 0.000000
// before busy 4.001213
// after busy 5.988133
// timer fire 5.988425
// timer fire 10.002296
// timer fire 15.002876
// timer fire 20.003320
问题二:第一个timer fire的执行时间为什么是after busy之后就立马执行了?
问题一 performSelector:afterDelay:
不会执行
问题一很简单,因为子线程里面没有runloop,导致performSelector:afterDelay:
不会执行
1
2
3
4
5
6
7
8
9
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1");
[self performSelector:@selector(testt) withObject:nil afterDelay:0];
// 这行在perform之后执行也没关系
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"2");
});
// 关闭Runloop如下
CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
问题二
NSTimer 其实就是 CFRunLoopTimerRef。他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
所以这里before busy和after busy的任务执行时间大概有2s,4s开始到6s已经错过一个窗口期了,为什么在执行完之后还会立即执行?
因为:如果RunLoop的忙的时间很长,长度达到了好多个timeInteval,则忙的这段时间内的timer回调只会被触发一次。
RunLoop层在timer触发后进行回调的时候,不会对tolerance进行验证。也就是说,因为RunLoop忙导致的timer触发时刻超出了tolerance的情况下,timer并不会取消,而不执行回调。
对于RunLoop忙时很长(或者timeInteval很短)的情况,会导致本该在这段时间内触发的几次回调中,只触发一次,也就是说,这种情况下还是会损失回调的次数。
对于RunLoop比较忙的情况,timer的回调时刻有可能不准,且不会受到tolerance的任何限制。tolerance的作用不是决定timer是否触发的标准,而是一个传递给系统的数值,帮助系统合理的规划GCD Timer的mach-port触发时机。设置了tolerance,一定会损失一定的时间精确度,但是可以显著的降低耗电。
所以有几个延伸的问题:
用NSTimer去计次可不可信?
不太可信。对于timeInteval长的时候基本可信,但是,在timeInteval很短的时候,是有可能导致RunLoop忙时超过1~2个timeInteval,从而丢失某次回调。
用NSTimer获取的时间间隔准不准
不准,如果想获取可靠时间,请配合CFAbsoluteTimeGetCurrent()使用
Toll-free bridgeing
在Core Foundation中Foundation中,有一些类型是可以交换使用的。
比如,NSString
和CFStringRef
就可以交替使用:
1
2
3
4
5
6
7
8
9
10
11
12
// NSString as CFStringRef
NSString * str = @"hello world";
// __bridge __bridge_retained __bridge_transfer 关键词用来管理转换之后的生命周期
// __bridge 进行OC指针和CF指针之间的转换,不涉及对象所有权转换。OC的还是OC管,CF的还是CF管
// __bridge_retained 将一个OC指针转换为一个CF指针,同时移交所有权,意味着你需要手动调用CFRelease来释放这个指针。这个关键字等价于CFBridgingRetain函数。
// __bridge_transfer 将一个CF指针转换为OC指针,同时移交所有权,ARC负责管理这个OC指针的生命周期。这个关键字等价于CFBridgingRelease
NSLog(@"%ld",CFStringGetLength((__bridge CFStringRef)(str)));
// CFStringRef as NSString
CFStringRef cf_str = CFStringCreateWithCString(kCFAllocatorDefault, "hello world", kCFStringEncodingUTF8);
NSLog(@"%zd",[(__bridge NSString *)cf_str length]);
CFRelease(cf_str);
GCD定时器实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 执行一次
double delayTimer = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayTimer * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
//do
});
// 重复执行
NSTimeInterval delayTimer = 1.0;
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), delayTimer * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
//do
});
dispatch_resume(_timer);