Qt-计时器实现探讨

引入

在查看关于processEvent时对于其第二个参数产生疑惑,Qt是如何保持高精度的计时的呢?

而在需要一个计时器时,通常会想到QTimer,通常对其的使用方式有两种:

  • 重复可用的计时器:创建一个QTimer对象,每过一段时间进行一个操作,即将一个QTimer对象和信号连接到槽函数
  • 单次触发的计时器:使用静态函数QTimer::singleShot,触发某些需要延迟触发的操作或需要在当前当不是现在(即想要执行,但可能立即执行会产生问题,使用interval0singleShot可以实现将其加入函数执行队列中);

废话结束,看源码吧

解析

重复可用的计时器解析

表层解析

首先看看浮于表面的QTimer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// qtimer.h
class QTimer
: public QObject
{
...
public Q_SLOTS:
void start(int msec);
void start();
void stop();
Q_SIGNALS:
void timeout();
protected:
void timerEvent(QTimerEvent *);
...
};
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
// qtimer.cpp
void QTimer::start(int msec)
{
inter = msec;
// 调用下方的函数
start();
}

void QTimer::start()
{
// 如果有则停止正在运行的计时器
if (id != INV_TIMER)
stop();
nulltimer = (!inter && single);
id = QObject::startTimer(inter, Qt::TimerType(type));
}

void QTimer::stop()
{
if (id != INV_TIMER)
{
QObject::killTimer(id);
id = INV_TIMER;
}
}

void QTimer::timerEvent(QTimerEvent *e)
{
if (e->timerId() == id)
{
if (single)
stop();
emit timeout(QPrivateSignal());
}
}

从这里可以看出,QTimer的实现就是围绕QObject中的三个函数:

  • public static int startTimer(int interval, Qt::TimerType timerType)
  • public static void killTimer(int id)
  • protected virtual void timerEvent(QTimerEvent *)

内核解析

当然,这只是表面的,现在进到QObject中查看更底层的实现:

startTimer

首先来看看startTimer

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
// qobject.cpp
int QObject::startTimer(int interval, Qt::TimerType timerType)
{
Q_D(QObject);
// 控制台发出警告,Q_UNLIKELY宏此处为向编译器提示,interval < 0 的可能为false
// 之后的几处表达式为相似用途
if (Q_UNLIKELY(interval < 0)) {
qWarning("QObject::startTimer: Timers cannot have negative intervals");
return 0;
}
auto thisThreadData = d->threadData.loadRelaxed();
if (Q_UNLIKELY(!thisThreadData->hasEventDispatcher())) {
qWarning("QObject::startTimer: Timers can only be used with threads started with QThread");
return 0;
}
if (Q_UNLIKELY(thread() != QThread::currentThread())) {
qWarning("QObject::startTimer: Timers cannot be started from another thread");
return 0;
}
// 此处为实现关键
int timerId = thisThreadData->eventDispatcher.loadRelaxed()->registerTimer(interval, timerType, this);
if (!d->extraData)
d->extraData = new QObjectPrivate::ExtraData;
// 添加timer Id到正在运行的timerId数组中
d->extraData->runningTimers.append(timerId);
// 返回注册的Timer Id
return timerId;
}

从上面可以看到,QObject调用了QAbstractEventDispatcher::registerTimer这个函数,我们层层深入:

1
2
3
4
5
6
int QAbstractEventDispatcher::registerTimer(int interval, Qt::TimerType timerType, QObject *object)
{
int id = QAbstractEventDispatcherPrivate::allocateTimerId();
registerTimer(id, interval, timerType, object);
return id;
}

此函数总共有3行,我们每一行进行解析:

第一行

此处调用了QAbstractEventDispatcherPrivateallocateTimerId去获取一个Id,此函数很简单:

1
2
3
4
int QAbstractEventDispatcherPrivate::allocateTimerId()
{
return timerIdFreeList()->next();
}

这里我们在深入了解一下QFreeList;

如果不想深入可以到直接下一个小节然后前翻一点看结论;

这里又涉及到一个新的东西:timerIdFreeList,这是个什么呢?源码中这个函数的前几行有其定义:

1
2
3
typedef QFreeList<void, QtTimerIdFreeListConstants> QtTimerIdFreeList;
// Q_GLOBAL_STATIC(TYPE, NAME)宏创建全局对象
Q_GLOBAL_STATIC(QtTimerIdFreeList, timerIdFreeList)

越来越多东西了,不急,我们慢慢进行分析,首先此处简单讲一下QFreeList

1
2
3
4
5
6
7
8
9
template <typename T, typename ConstantsType = QFreeListDefaultConstants>
class QFreeList
{
...
public:
inline int next();
inline void release(int id);
...
}

注释中对其的解释翻译后为:

这是一个无锁自由列表的通用实现。使用 next() 获取列表中的下一个自由条目,并在使用完成id后 release(id)。

此版本是模板化的,允许使用 next() 返回的id访问T类型的有效负载。
有效负载由自由列表自动分配和解除分配,但在调用 next()/release() 时不会。
初始化应该在 next() 返回后由需要它的代码完成。
同样,cleanup() 应该在调用 release() 之前发生。
可以使用“void”作为有效载荷类型,在这种情况下,自由列表只包含下一个自由项的索引。

ConstantsType类型默认为上述 QFreeListDefaultConstants。您可以定义自定义的 ConstantsType,请参阅上面的内容了解需要提供的详细信息

总结一下就是调用QFreeList::next获取一个唯一的id,在使用完毕后调用QFreeList::release释放

第二行

此处调用的registerTimer(id, interval, timerType, object);为此类的一个纯虚函数,通过项目中搜索public QAbstractEventDispatcher我们找到了QEventDispatcherWin32(源码版本5.7.1,其他版本可能有所不同);

此处我们不再深入聊这个类,只说其重写的registerTimer函数:

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
void QEventDispatcherWin32::registerTimer(int timerId, int interval, Qt::TimerType timerType, QObject *object)
{
...
// 判断参数非法和跨线程调用
...

// 转换私有指针
Q_D(QEventDispatcherWin32)

// 退出时不再注册新的计时器
// 当QCoreApplication::closingDown() 在这里被使用太晚了?(is set too late to be used here)
if(d->closingDown)
return;

// 创建一个计时器信息指针
WinTimerInfo *t = new WinTimerInfo;
t->dispatcher = this;
t->timerId = timerId;
t->interval = interval;
t->timerTpye = timerType;
t->obj = object;
t->inTimerEvent = false;
t->fastTimerId = 0;

// 内部窗口句柄存在
if(d->internalHwnd)
d->registerTimer(t);

// 添加到timer数组和字典中
d->timerVec.append(t);
d->timerDict.insert(t->timerId, t);
}

此处的作用即将上层传入的数据封装到结构体,并在内部窗口存在时注册此结构体,并添加到内部数组和字典当中。

其中注册结构体调用的QEventDispatcherWin32Private::registerTimer源码如下:

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
void QEventDispatcherWin32Private::registerTimer(WinTimerInfo *t)
{
// 判断句柄是否存在
Q_ASSERT(internalHwnd);

Q_Q(QEventDispatcherWin32);

int ok = 0;
// 计算下次超时间隔和时间,不过多赘述
calculateNextTimeout(t, qt_msectime());
uint interval = t->interval;
if(interval == 0u)
{
// 优化单词触发0间隔计时器,即向event队列添加一个事件
QCoreApplication::postEvent(q, new QZeroTimerEvent(t->timerId));
ok = 1;
} else if((interval < 20u || t->timerType == Qt::PreciseTimer) && qtimeSetEvent)
{
ok = t->fastTimerId = qtimeSetEvent(interval, 1, qt_fast_timer_proc, (DWORD_PTR)t, TIME_CALLBACK_FUNCTION | TIME_PERIODIC | TIME_KILL_SYNCHRONOUS);
}

if(ok == 0)
{
// 为(very)CoarseTimers使用普通计时器,或没有更多的多媒体计时器可用时
ok = SetTimer(internalHwnd, t->timerId, interval, 0);
}

// 创建失败
if(ok == 0)
qErrorWarning("QEventDispatcherWin32::registerTimer: Failed to create a timer");
}

对于间隔为0的计时器,实际是创建了一个QZeroTimerEvent事件;

此函数的关键位置在else if(...)包含的代码块以及之后如果ok0(即之前操作失败)的情况下的处理;

首先是else if代码块,当触发间隔小于20且计时器类型为PreciseTimer(精确到毫秒)且qtimeSetEvent存在时,进入代码块;前两个判断条件显而易见,第三个qtimeSetEvent是什么呢?我们来看一下它的定义和赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// qeventdispatcher_win.cpp
typedef MMRESULT(WINAPI *ptimeSetEvent)(UINT, UINT, LPTIMECALLBACK, DWORD_PTR, UINT);

static ptimeSetEvent qtimeSetEvent = 0;

static void resoloveTimerApi()
{
static bool triedResolve = false;
if(!triedResolve){
...
#if !defined(Q_OS_WINCE)
qtimeSetEvent = (ptimeSetEvent)QSystemLibrary::resolve(QLatin1String("winmm"), "timeSetEvent");
...
#else
...
#endif
}
}

上面的操作将系统库中的timeSetEvent函数指针保存;

timeSetEventMSDN中解释为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 如果成功,则返回计时器事件的标识符,否则返回错误。如果失败且未创建计时器事件,则此函数返回NULL。(此标识符也传递给回调函数。)
MMRESULT timeSetEvent(
// 事件延迟,以毫秒为单位。如果这个值不在定时器支持的最小和最大事件延迟的范围内,函数将返回一个错误。
UINT uDelay,
// 计时器事件的分辨率,以毫秒为单位。数值越小,分辨率越高;分辨率为0表示周期性事件应该以尽可能高的精度发生。然而,为了减少系统开销,您应该使用适合您的应用程序的最大值。
UINT uResolution,
// 指向回调函数的指针,该函数在单个事件到期时调用一次,或在周期性事件到期时周期性调用。
// 如果fuEvent指定TIME_CALLBACK_EVENT_SET或TIME_CALLBACK_EVENT_PULSE标志,则lpTimeProc参数被解释为事件对象的句柄。事件将在单个事件完成时设置或脉冲化,或在周期性事件完成时周期性设置或脉冲。
// 对于fuEvent的任何其他值,lpTimeProc参数是指向LPTIMECALLBACK类型回调函数的指针。
LPTIMECALLBACK lpTimeProc,
// 用户提供的回调数据。
DWORD_PTR dwUser,
// 定时器事件类型。
UINT fuEvent
);

此代码块中对于qtimeSetEvent调用时的uResolution参数为qt_fast_timer_proc,其代码为:

1
2
3
4
5
6
7
8
9
10
11
12
// 
void WINAPI QT_WIN_CALLBACK qt_fast_timer_proc(uint timer, uint /*reserved*/, DWORD_PTR user, DWORD_PTR /*reserved*/, DWORD_PTR /*reserved*/)
{
// 完整性检测
if(!timerId)
return;
// 将之前传入的dwUser参数转换回原本类型
WinTimerInfo *t = (WinTimerInfo*)user;
Q_ASSERT(t);
// 异步通知t的调度者t已经超时
QCoreApplication::postEvent(t->dispatcher, new QTimerEvent(t->timerId));
}

简单来说else if代码块的作用为创建一个系统提供的高精度定时器;此计时器原理为:通过超时后调用回调函数,回调函数会对回调的参数中所属调度者发出QTimerEvent(超时)事件

else if代码块解析完毕,再来看看第一个if(ok == 0)的代码块,此函数只有一行,作用为在前面创建失败后,调用Windows系统函数SetTimer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// WinUser.h
UINT_PTR SetTimer(
// 要与计时器关联的窗口的句柄。 此窗口必须由调用线程拥有。 如果 hWnd 的 NULL 值与现有计时器的 nIDEvent 一起传入,则将以现有非 NULL hWnd 计时器相同的方式替换该计时器。
_In_opt_ HWND hWnd,
// 非零计时器标识符。 如果 hWnd 参数为 NULL, 且 nIDEvent 与现有计时器不匹配,则忽略它并生成新的计时器 ID。 如果 hWnd 参数不是 NULL , 并且 hWnd 指定的窗口已有一个具有值 nIDEvent 的计时器,则现有计时器将被新计时器替换。
// SetTimer 替换计时器时,将重置计时器。 因此,将在当前超时值过后发送一条消息,但以前设置的超时值将被忽略。 如果调用不打算替换现有计时器,则如果 hWnd 为 NULL,则 nIDEvent 应为 0。
_In_ UINT_PTR nIDEvent,
// 超时值(以毫秒为单位)
// 如果 uElapse 小于 USER_TIMER_MINIMUM (0x0000000A) ,则超时设置为 USER_TIMER_MINIMUM。
// 如果 uElapse 大于 USER_TIMER_MAXIMUM (0x7FFFFFFF) ,则超时设置为 USER_TIMER_MAXIMUM。
_In_ UINT uElapse,
// 指向在超时值用完时要通知的函数的指针。 有关函数的详细信息,请参阅 TimerProc。
// 如果 lpTimerFunc 为 NULL,系统会将 WM_TIMER 消息发布到应用程序队列。 消息 MSG 结构的 hwnd 成员包含 hWnd 参数的值。
_In_opt_ TIMERPROC lpTimerFunc
);

从上方函数调用可见,此处lpTimerFunc为空,系统会将WM_TIMER消息发布到应用程序队列;

至此,第二行解析完毕。

第三行

第三行就很简单了,将前一步获取的timerId返回;

至此,有关startTimer解析完毕。

killTimer

解析完startTimer后,来看killTimer就简单多了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// qobject.cpp
void QObject::killTimer(int id)
{
Q_D(QObject);
// 判断线程一致
...

if(id)
{
int at = d->extraData ? d->extraData->runningTimers.indexOf(id) : -1;
if(at == -1)
{
// 发出警告,timerId不存在
...
return;
}
if(d->threadData->eventDispatcher.load())
d->threadData->eventDispatcher.load()->unregisterTimer(id);
d->extraData->runningTimers.remove(at);
QAbstractEventDispatcherPrivate::releaseTimerId(id);
}
}

按照之前一样的分析,首先判断id存在后,获取索引值;当索引值存在,调用QAbstractEventDispatcher::unregisterTimer,并移除当前运行计时器数组中对应id,最后调用QAbstractEventDispatcherPrivate::releaseTimerId移除自由队列中的id

这里重点解析QAbstractEventDispatcher::unregisterTimer

QAbstractEventDispatcher::unregisterTimer与之前的registerTimer一样,都是纯虚函数,通过找到其在QEventDispatcherWin32中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool QEventDispatcherWin32::unregisterTimer(int timerId)
{
...
// 判断参数非法和跨线程调用
...

Q_D(QEventDispatcherWin32);
if(d->timerVec.isEmpty() || timerId <= 0)
return false;

WinTimerInfo *t = d->timerDict.value(timerId);
if(!t)
return false;

// 从字典和数组中移除对应计时器信息
d->tiemrDict.remove(t->timerId);
t->timerVec.removeAll(t);
d->unregisterTimer(t);
return true;
}

可以看到,关键的还是在d指针的unregisterTimer函数,通过查看其源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void QEventDispatcherWin32Private::unregisterTimer(WinTimerInfo *t)
{
if(t->interval == 0)
{
QCoreApplicationPrivate::removePostedTimerEvent(t->dispatcher, t->timerId);
}
else if(t->fastTimerId != 0)
{
qtimeKillEvent(t->fastTimerId);
QCoreApplicationPrivate::removePostedTimerEvent(t->dispatcher, t->timerId);
}
else if(internalHwnd)
{
killTimer(internamHwnd, t->timerId);
}
delete t;
}

首先,这个函数中出现了三个函数,我们逐个解析:

  1. QCoreApplicationPrivate::removePostedTimerEvent通过查看其在qcoreapplication_win.cpp中的实现,其函数作用为删除对应调度者下对应timerId的计时器,此处不过多赘述;
  2. qtimeKillEvent与之前qtimeSetEvent类似,也是通过加载系统库中的timeKillEvent函数指针,并在此处调用取消指定的定时器计时;
  3. KillTimer与之前SetTimer对应,删除SetTimer设置的计时器;

至此,重复可用的计时器解析完成

单次触发的计时器

表层解析

按着之前的方法,由表面层层深入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// qtimer.h
class QTimer
: public QObject
{
public:
static void singleShot(int mesc, const QObject *receiver, const char *member);
static void singleShot(int mesc, Qt::TimerType timerType, const QObject *receiver, const char *member);
...
// 模板函数,以单次触发槽函数
template<typename Func1>
static inline void singleShot(int msec, const typename QtPrivate::FunctionPointer<Func1>::Object *receiver, Func1 slot)
{
singleShot(msec, msec >= 2000 ? Qt::CoarseTimer : Qt::PreciseTimer, receiver, slot)
};
// 更多模板函数不过多赘述
...
}
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
// qtimer.cpp
oid singleShot(int mesc, const QObject *receiver, const char *member)
{
singleShot(msec, msec >= 2000 ? Qt::CoarseTimer : Qt::PreciseTimer, receiver, member);
}

void singleShot(int mesc, Qt::TimerType timerType, const QObject *receiver, const char *member)
{
if(Q_UNLIKELY(msec < 0)
{
// 警告
...
return;
}
if(receiver && member)
{
// 立即触发
if(msec == 0)
{
// 获取函数名
...
QMetaObject::invokeMethod(const_cast<QObject *>(receiver), methodNmae.constData(), Qt::QueuedConnection);
return;
}
}
(void) new QSingleShotTimer(mesc, timerType, receiver, member);
}

这里又引出了一个类:QSingleShotTimer,通过查看此类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// qtimer.cpp
class QSingleShotTimer
: public QObject
{
...
public:
QSingleShotTimer(int msec, Qt::TimerType timerType, const QObject *r, const char *m);
QSingleShotTimer(int msec, Qt::TimerType timerType, const QObject *r, QtPrivate::QSlotObjectBase *slotObj);
~QSingleShotTimer();
...
Q_SIGNALS:
void timeout();
protected:
void timerEvent(QTimerEvent *) Q_DECL_OVERRIDE;
}

内核解析

这里可以看出来,这个类与QTimer很相似,同样有timeout信号和timerEvent函数:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
QSingleShotTimer(int msec, Qt::TimerType timerType, const QObject *r, const char *m)
: QObject(QAbstractEventDispatcher::instance())
, hasValidReceiver(false)
, slotObj(0)
{
timerId = startTimer(msec, timerType);
connect(this, SIGNAL(timeout), r, member);
}

QSingleShotTimer(int msec, Qt::TimerType timerType, const QObject *r, QtPrivate::QSlotObjectBase *slotObj)
: QObject(QAbstractEventDispatcher::instance())
, hasValidReceiver(false)
, receiver(r)
, slotObj(slotObj)
{
timerId = startTimer(mesc, timerType);
if(r && thread != r->thread())
{
// 避免在计时器触发前泄露QSingleShotTimer实例
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &QObject::deleteLater);
setParent(0);
moveToThread(r->thread);
}
}

~QSingleShotTimer()
{
if(timerId > 0)
killTimer(timerId);
if(slotObj)
slotObj->destoryIfLastRef();
}

void QSingleShotTimer::timerEvent(QTimerEvent *)
{
if(timerId > 0)
killTimer(timerId);
timerId = -1;

if(slotObj)
{
// Q_LIKELY宏与之前Q_UNLIKELY相似,但建议编译器此表达式通常为真
if(Q_LIKELY(!receiver.isNull() || !hasValidReceiver)
{
void *args[1] = {0};
slotObj->call(const_cast<QObject *>(receiver.data()), args);
}
}
else
{
emit timeout();
}

// 官方注释:我们想在此使用deletelater,但是发布一个新的事件来处理这个事件感觉很浪费,所以我们只是取消了标记并显式删除...
qDeleteInEventHandler(this);
}

两个构造函数都调用了QObject::startTimer创建计时器,有关这一个不过多赘述,请看上面关于startTimer的解析

有关析构函数,依旧是调用了QObject::killTimer,也不过多赘述,请看上面关于killTimer的解析

再来看事件处理,首先依旧是删除对应计时器;再进行slotObj的判断:

  • 如果slotObj存在,则为第二种构造函数,传入的为函数指针,直接通过QSlotObjectBase::call调用目标函数;
  • 如果不存在,则为第一种构造函数,因为之前进行了槽链接,直接发出timeout信号即可;

完成上述操作后删除自身。

至此,单次触发的计时器解析完毕

总结

Qt的计时器QTimer的底层实现原理是根据触发间隔分类:

  • 触发间隔为0
    • 使用singleShot会将函数加入调用队列(QueuedConnection方式的QObject::invokeMethod),用Qt底层机制去实现;
    • 使用普通QTimer对象时,会使用postEvent派发一个QZeroTimerEventQAbstractEventDispatcher自身,再通过事件处理调用QObject::SendEvent派发到QTimerEvent对应的对象当中;
  • 触发间隔不为0时调用操作系统计时器,且再Windows平台会根据触发间隔的长短选择性能不一的计时器。

我们在使用时往往不会去在意其实现,但往往了解其实现后会更容易去使用它。

学吧,学无止境!