使用 Qt 获取 UDP 数据并显示成图片 (4)

在编写程序时误以为是 Qt 的事件循环机制过慢导致程序处理不了那么多数据(实际上它的速度足够处理这些数据),因此将程序中使用的 QUdpSocket 对象换成了 [Windows 平台的 Socket 通信代码][winsock demo],并将其改写成类方便调用。实际上是在 QThread 子线程中无限循环地运行 recvfrom(clientSocket, recvedData.data(), recvbuflen, 0, &fromaddr, &addrLen); 这样的接收数据包函数,跳过了 Qt 事件循环机制,然后当接收到包之后再通过回调函数通知数据处理线程进行处理。

但当我写这篇博客,重新用正常的代码进行测试时,发现即便使用 QUdpSocket::readyRead 信号来接收 UDP 数据,只要数据处理进程不堆积数据,就不会出现占用内存越来越多的情况。换句话说,不是 Qt 无法处理实时性的数据,而是自己编写的代码里面有问题。

回想最开始写的程序,在处理 QByteArray 表示的原始数据时,会为每一个接收到的数据包分配地址,而且分配的地址位于堆中。而实际上在堆 heap 中分配回收内存地址相较于在栈 stack 中是慢得多的。为每个到来的数据用 new 构造一个新的 CameraData 对象,然后在处理完后将这个 CameraData delete 掉其实是很慢的,如果你这样做了,并且你在 CameraData 的析构函数中加上 qDebug 语句打印 "CameraData is deleting...",你会发现,当发送方(我们的 Python 模拟发送程序)停止发送数据包后很长一段时间内,Qt 程序在一直打印着 "CameraData is deleting"。

而我最开始就是这么做的,所以发生了 Qt 程序随着数据接收的变多,占用的内存越来越大的情况。当然,这不排除 qDebug 语句输出到控制台上也会占用很多时间。如果每秒钟要调用上万次 qDebug() << "CameraData is deleting",那么建议你使用一个计数变量控制 qDebug 的调用次数,因为这条语句的调用也会让数据处理变得缓慢。

处理接收到的 UDP 包

为了让接收端不丢包,需要快速的处理接收到的 UDP 包,并且在处理的代码中不要调用耗时的函数或者 new 操作。为了避免重复调用 new 和 delete 操作符,我们需要构建一个对象池,以便复用池中的对象,减少 new 操作。池的定义比较简单,封装一个 QList 容器类就好了,为了简化和复用池的代码,我用到了 c++ 的 template 特性,但是这个 DataObjPool 中的容器只能是 DataObj 的子类:

template<class T> class DataObjPool { public: virtual ~DataObjPool() { qDeleteAll(pool); numAvailable = 0; } T *getAvailable() { if( numAvailable == 0 ) { return 0; } for(int i = 0; i < pool.size(); i++) { T *item = pool[i]; if(item->isValid()) { item->setValid(false); numAvailable -= 1; return item; } } return 0; } T *get(int id) { return pool[id]; } inline bool release(T *dobj) { dobj->reset(); numAvailable += 1; return true; } int releaseTimeout(int now, int timeout = 100) { int releaseCount = 0; for(int i = 0; i < pool.size(); i++) { T *item = pool[i]; if(now > item->getGenerateMs() + timeout) { item->reset(); numAvailable += 1; releaseCount += 1; } } return releaseCount; } void releaseAll() { for(int i = 0; i < pool.size(); i++) { T *item = pool[i]; if(item->isValid()) { continue; } item->reset(); numAvailable += 1; } } int getNumAvailable() const { return numAvailable; } template<class T2> operator DataObjPool<T2>(); protected: DataObjPool(int size = 100); private: QList<T *> pool; int numAvailable = 0; }; class RawDataObjPool: public DataObjPool<CameraData> { public: RawDataObjPool(int size = 100); }; class LineDataPool : public DataObjPool<PreProcessData> { public: LineDataPool(int size = 100); };

当然你也可以直接编写两个类 RawDataObjPool 和 LineDataPool,把池的操作分别复制到两个类中,使用模板特化的好处是改动的时候不需要改动两个类了。前面说过,DataObj 类继承自 QObject,就是为了简化在对象池中进行的操作。DataObjPool 会在构造时在内存中预分配一定数量的对象,以 RawDataObjPool 为例,构造时传入 size 参数,便会预先在内存中创建 size 个 CameraData,在程序运行过程中,这些对象都会被我们这个 Qt 程序循环利用,直到关闭程序才会释放掉这些 CameraData(如果操作系统的内存不足,过多的对象占用的内存还是会被释放)。

对象池的主要接口有两个:getAvailable 和 release 分别用于获取可用的对象或释放掉池中的对象,注意这里的释放是让对象池对该对象进行标记,以便重复使用,而不是释放掉该对象占用的内存空间或 delete 掉。当对象池中无可用对象时,可以根据需要释放掉超时的对象或者释放掉全部对象。

使用对象池减少 new 操作符的使用后,处理数据的子线程的速度明显加快。正常情况下就可以看到如下的图片:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsxfdy.html