背景

最近自己业余时间在开发一个录音软件,要画一个声音的音频图,大概类似下面的样子:

在一秒钟内要取大概20次当前声音的大小,用以绘制成一幅图,那么首先,想当然的当然是用NSTimer了,写了代码如下:

1
2
3
4
5
6
// 当然这里把每秒钟需要的次数设置成一个变量会更好。
[NSTimer scheduledTimerWithTimeInterval:1 / 20
repeats:YES
block:^(NSTimer * _Nonnull timer) {
// do somting.
}];

block里面去取了一下当前音量,绘制成了一个图,代码运行起来没问题,音量正确得取到了,并且也绘制在了图上面,乍一看发现没什么问题,等了十几秒再回头看,发现貌似时间过得很慢,明明感觉已经起码过了十秒,但是录音图上停留在6秒7秒的样子,很疑惑,在block中,加了一个Log,就看出缘由了。

设置的明明是0.05秒执行一次,但是从图中来看,基本都是隔了0.1秒才执行一次,很少有0.5秒才执行一次,NSTimerrunloop运行的时候执行,runloop中有其他任务时势必会影响到NSTimer,但是没想到影响会那么大,查阅了官方文档后发现,iOS设备中,NSTimer的精度在0.1秒左右,看来是需要另寻他法了。

平均误差:18.11

while循环

具体思路就是新开一个线程,然后跑一个while循环去检测,代码如下:

1
2
3
4
5
6
7
8
dispatch_queue_t timerQueue = dispatch_queue_create("timer", DISPATCH_QUEUE_SERIAL);

dispatch_async(timerQueue, ^{
while (YES) {
usleep( 50000 );
NSLog(@"timer");
};
});

平均误差:65.33毫秒

貌似比NSTimer还不靠谱,经过测试,发现精度基本上在200MS左右。

CADisplayLink的调用是根据屏幕刷新率来的,通常iOS设备的刷新率在60HZ,那么一秒钟就能够被调60次。

1
2
3
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(testDisplayLink)];
link.paused = NO;
[link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

平均误差:0.002(在每秒60次调用的情况下)

相当精确,在固定刷新率下,精度基本在0.001毫秒的样子,但是测试发现CADisplayLink有以下问题:

  1. 时间间隔只能是 1 / 60,没法自定义,可以设置 1 / 60的倍数。
  2. 首先精度在 1 / 60秒的样子,无法再精确了,在此基础上,调用间隔非常精确。
  3. 全局只能有一个CADisplayLink实例在运行。
  4. 屏幕在熄灭状态下无法调用此方法。

dispatch_after

通过递归得调用这个函数来实现

1
2
3
4
5
6
7
8
9
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
(int)(0.05 * (double)NSEC_PER_SEC)
),
dispatch_get_main_queue(), ^{
NSLog(@"dispatch after");
[self after];
});

平均误差:12.17毫秒。

目前看来是误差最小的了,如果不算CADisplayLink的话。

官方提供的高精度解决方案

官网地址:https://developer.apple.com/library/content/technotes/tn2169/_index.html

平均误差:0.34毫秒

大概测试了一下,间隔在1毫秒的时候,误差在0.7毫秒,间隔在5毫秒的时候误差0.36,间隔在2毫秒的时候误差在0.7毫秒,间隔在4毫秒的时候误差为0.3毫秒,间隔在3毫秒的时候误差在0.49

间隔 误差
5ms 0.36ms
4ms 0.3ms
3ms 0.49ms
2ms 0.7ms
1ms 0.7ms

总体来说,这种方式的精度在5毫秒左右,5毫秒已经能满足绝大多数需求了。

iOS