文章

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:不会执行

参考文章 - afterDelay为什么不会执行

解决方式参考文章-在子线程开启和关闭Runloop

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回调只会被触发一次。

20220210-1

RunLoop层在timer触发后进行回调的时候,不会对tolerance进行验证。也就是说,因为RunLoop忙导致的timer触发时刻超出了tolerance的情况下,timer并不会取消,而不执行回调。

对于RunLoop忙时很长(或者timeInteval很短)的情况,会导致本该在这段时间内触发的几次回调中,只触发一次,也就是说,这种情况下还是会损失回调的次数。

对于RunLoop比较忙的情况,timer的回调时刻有可能不准,且不会受到tolerance的任何限制。tolerance的作用不是决定timer是否触发的标准,而是一个传递给系统的数值,帮助系统合理的规划GCD Timer的mach-port触发时机。设置了tolerance,一定会损失一定的时间精确度,但是可以显著的降低耗电。

所以有几个延伸的问题:

  1. 用NSTimer去计次可不可信?

    不太可信。对于timeInteval长的时候基本可信,但是,在timeInteval很短的时候,是有可能导致RunLoop忙时超过1~2个timeInteval,从而丢失某次回调。

  2. 用NSTimer获取的时间间隔准不准

    不准,如果想获取可靠时间,请配合CFAbsoluteTimeGetCurrent()使用

参考文章-从Runtime探讨NSTimer原理

参考文章-深入学习iOS定时器

Toll-free bridgeing

在Core Foundation中Foundation中,有一些类型是可以交换使用的。

比如,NSStringCFStringRef就可以交替使用:

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);
本文由作者按照 CC BY 4.0 进行授权