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

本文首发于 BriFuture 的 我的博客html

在个人前一篇文章 使用 Qt 获取 UDP 数据并显示成图片 中,我讲了如何用 Python 模拟发送数据,如何在 Qt 中高效的接收 UDP 数据包并将数据解析出来。然而此前的文章在分别显示 RGB 通道、R 通道、G 通道、B 通道这四组通道的图片时仍然会出现处理速度过慢的问题。python

前面说过编写的程序至少会用到 3 个线程来分别处理 UI、socket 数据、数据解析,由于不这样作无法在时限内处理完接收到的数据,写第一篇博客的时候,我觉得是单纯的使用 new 在堆中分配内存致使程序运行效率低,后来确实经过预分配对象内存解决了部分问题,可是还有一些会影响程序运行速度的问题没有解决,也没有深究,今天从新编写代码的时候,为了分配数据到 4 幅图片上(分别是 RGB 通道、R 通道、G 通道、B 通道),发现运行速度仍是不够,影响运行速度的缘由有几个:数组

  1. 运行程序的模式(Debug 和 Release 两种模式)
  2. Qt 的事件循环机制(以前反复怀疑过,不过最后仍是发现短期内大量调用信号很容易致使处理速度过慢)
  3. 低效的内存复制操做(如 QByteArray 的 assign 赋值操做和过多、过于复杂的程序流程)

接下来看看这几个致使程序运行速度不够的缘由:缓存

1. Qt Debug 模式和 Release 模式的差别

在 QtCreator 中运行程序,若是是以 Debug 模式运行的话,速度是要比 Release 模式低一些的。之前编写 Qt 程序,数据量通常不大,对于性能都没有要求,即便程序代码不够优化,但在用户使用过程当中通常不会感觉到运行卡顿,因此一直都没发现 Debug 模式和 Release 模式的性能有差别。多线程

不过其实也能猜到性能有差别的大概缘由:Debug 模式下会在最终生成的代码里面插入不少额外的代码用于调试,可是 Release 生成的代码是不会插入这些调试用的代码的,最明显的差别就是 Debug 模式生成的可执行文件比 Release 模式生成的可执行文件要大得多。app

Debug 模式下运行程序,实际 FPS 和指望的 FPS 有 6 帧的差距,差距产生的缘由是处理速度不够,致使最终生成图片的速度慢了。socket

Release 模式下即便是原始数据包的指望 FPS 到了 77 帧,实际的 FPS 也能够达到 77 帧,也就是说在处理过程当中没有出现处理速度跟不上接受数据的速度。函数

2. Qt 的事件循环机制

当咱们使用 Qt 程序的时候,常常会在主函数 main 里写出相似下面这样的代码:性能

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow mw;
    mw.show();
    return a.exec();
}

这样咱们的程序应用的生命周期就是 QApplication 所定义的,当咱们使用 QObject::moveToThread 方法将某个 QObject (子类)对象移到其余的子线程中的时候,子线程也有独立于主线程的相应的事件派发机制。优化

QObject 的多线程使用方法很巧妙,利用信号&槽机制或者是 QMetaObject::invokeMethod 方法就可让要执行的耗时函数在子线程中执行,可是若是是直接调用耗时函数,那么就会在当前的线程中执行耗时操做,致使线程阻塞。

在线程之间传递数据,若是是用信号&槽的机制,那么可能你都不须要考虑线程间的数据同步问题,但信号&槽机制是要依靠 Qt 的事件循环机制的,若是事件不能正常分发(dispatch),那么子线程中的槽函数就不会被调用。

关于 Qt 的事件循环机制和线程机制,推荐看一看官方 wiki,《线程、事件与QObject》 或者也有对应的英文原文 Threads Events QObjects

若是频繁的调用信号,在 Qt 的事件循环中,由于前一次耗时的任务没有完成,致使对应的槽函数没法执行,最终致使处理速度跟不上。

所以对于实时性要求高的程序,Qt 的事件循环机制可能不会是你的首选,你更有可能去作的是在 Qt 的一个子线程中运行循环代码,忽略掉该子线程中的事件循环以提升程序的性能。

3. 低效的内存复制操做

在接收到原始字节数据以后,最重要也是最麻烦的就是解析数据。包括识别自定义协议数据的头部信息,将数据包中的图像数据复制到缓冲区,并将缓冲区中的数据以图片的形式显示出来。接下来分享几个高效处理数据的几个小技巧:

  1. 使用 QByteArray 存储原始数据包时,先调用 resize 预分配内存,而后使用 memcpy 直接对内存数据进行操做,这样作效率是最高的,但它也是比较繁琐的。
QByteArray data;
data.resize(PacketSize); // PacketSize 是预先定义好的数据字节数
// 能够简单的认为 rawData 就是从 UDP 端口中接收到的数据,
// ValidDataSize 也是预先定义好的数据字段的长度
memcpy(rawData.data(), data.data(), ValidDataSize);

// 上面的代码要比直接使用赋值操做 = 高效
data = rawData;
  1. 若是接收到的数据能够明确是有序的,能够用数据分别表示相应的序号,再从数组中取数据,我最开始存储 LineDataObj (用于表示图片的一行数据)的时候,用 QMap 存储行号和指针,利用 QMap 的查找功能减小了查找或排序的时间,可是缺点是 QMap 会随着其内部的数据量增大变得缓慢,若是只须要缓存数据,建议直接使用数组存取,这样的运行效率最高。
// 在类中声明一个 map
QMap<int line, LineDataObj *> map;

// 在方法体中使用 map 查找是否有对应的行数据
if(map.contains(line)) {
    // 若是有对应的行数据对象,直接将数据写入到行对象数据上
    ...
} else {
    // 若是没有,则插入一条记录
    map.insert(line, lineDataObj);
}

// 处理完一行数据后,能够将该行数据从 map 中移除掉
map.remove(line);

能够发现,map 就是用来判断是否有对应行数据对象,而后处理结束后移除保存的行号,这并无达到缓存数据的目的,反而再插入和移除的过程当中浪费了过多时间。但若是用一个数组当作缓存区就会快不少,由于咱们减小了查找和移除记录的时间:

QVector<LineDataObj *> linePool;
// LinePoolSize 是预约义的池大小
linePool.reserve(LinePoolSize);
for(int i = 0; i < LinePoolSize; i++) {
    linePool.append(new LineDataObj());
}

// 数组大小是有限的,行号倒是不断增长的,所以要设置一个起始行,保证在长时间执行程序后不会出现数组越界的问题
int diffLine = line - startLine;
// 进行处理
linePool[line].setData(...);
  1. 尽可能保持清晰并且简单的结构。我以前写代码总想着考虑到全部状况,最终却老是无法尽善尽美只有根据状况放低预期,我以为没必要一开始就非要把代码的层次结构划分的特别详细,根据实际状况使用合理的程序结构(固然每一个人可能有不一样的见解,但少便是多的原则确实给了我很大的启发)。

我以前编写程序时,除了有一个 LineDataObj 用来表示行对象,还有一个 RawDataObj 表示原始的数据包对象。处理的流程多:1. 接受原始数据包 => 2. 将数据包填充到 RawDataObj 中并解析数据包的行号,RGB 类型 => 3. 根据 RawDataObj 的属性肯定对应的 LineDataObj => 4. 当 LineDataObj 存储到必定数目时生成图像。

这个流程很直观也很容易想到,可是 RawDataObj 这个数据对象其实不必使用,由于它增长了一次没必要要的内存数据复制。这彻底能够给 LineDataObj 类增长几个静态方法,判断出数据包的行号和 RGB 类型,而后将数据部分写入到 LineDataObj 的数据字段中。这样作不只能够减小内存读写的次数,并且能够在一个对象中申请大段内存,保存整行的数据,最后写入到图片时,只用将这个区域赋值到图片中便可。

4. 高效地显示图片

最后分享一下如何在 Qt 中高效的显示图片。通常用 Qt 显示图片能够用 QLabel:

QLabel label;
QImage image;
// 执行一些读取图片的操做,再显示在 QLabel 上
label.setPixmap(QPixmap::fromImage(image));

可是用 QPixmap::fromImage 会从 image 的内存区域中复制一份数据到 Pixmap 中,这样的操做并不高效。咱们可使用 QImage::scanLine 方法获取它对应的内存区域,直接对内存进行操做,显示的时候不用 QPixmap::fromImage,咱们要直接将内存中的修改显示到界面中,这样咱们要定义一个类(不妨让它继承 QLabel),重写 paintEvent 方法:

void PictureImage::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);
    if(m_index == uchar(-1)) {
        return;
    }
//    this->painter.drawImage(target, *m_image);
    QPainter p(this);
    // target 在构造函数中定义:
    // target = QRectF(0.0, 0.0, PictureImage::ImageWidth, PictureImage::ImageHeight);
    p.drawImage(target, *m_images[m_index]);
}

p.drawImage(target, image) 这样就能够将图片更新到界面中,而且它会被 QPixmap 的 fromImage 方法要高效。

用 Python 发送模拟数据遇到的问题

以前说过,模拟数据是用 Python 代码编写的,这个代码发送模拟数据的效率能够高达 100M/s,下面的截图是我在本身的笔记本(i5 8200U@1.8G)上运行的结果:

可是令我感到特别奇怪的是,有一段时间一样的代码在个人 amd ryzen 1500x@3.5G 台式机上只能达到 50M/s 的速度。我一度怀疑是英特尔和 AMD 的处理器单核性能有差别,但按道理不该该有这么大的速度差别。并且最近几天它又在个人台式机上可以跑到 100M/s 的速度。

参考

线程、事件与QObject

相关文章
相关标签/搜索