一個(gè)項(xiàng)目,要接收 UDP 數(shù)據(jù)包,解析并獲取其中的數(shù)據(jù),主要根據(jù)解析出來(lái)的行號(hào)和序號(hào)將數(shù)據(jù)拼接起來(lái),然后將拼接起來(lái)的數(shù)據(jù)(最重要的數(shù)據(jù)是 R、G、B 三個(gè)通道的像素值)顯示在窗口中??紤]到每秒鐘要接收的數(shù)據(jù)包的數(shù)量較大,Python 的處理速度可能沒(méi)有那么快,而且之前對(duì) Qt 也比較熟悉了,所以用Qt 作為客戶端接收處理數(shù)據(jù)包,用近期學(xué)習(xí)的 Python 模擬發(fā)送數(shù)據(jù)包。
在 TCP/IP 協(xié)議中,UDP 數(shù)據(jù)包的大小是由限制的,因此用 UDP 傳輸數(shù)據(jù)時(shí),還要在 UDP 層上再封裝一層自定義的協(xié)議。這個(gè)自定義的協(xié)議比較簡(jiǎn)單,每個(gè) UDP 包的大小為 1432 個(gè)字節(jié),分為幾個(gè)部分:
部分 | 起始字節(jié) | 字節(jié)長(zhǎng)度 | 說(shuō)明 |
---|---|---|---|
Start | 0 | 4 | 包頭部的 Magic Number,設(shè)為 0x53746172 |
PartialCnt | 4 | 1 | 分包總數(shù),一個(gè)字節(jié)(0-255)以內(nèi) |
PartialIdx | 5 | 1 | 分包序號(hào) |
SampleLine | 6 | 1 | 采樣率 |
RGB | 7 | 1 | rgb 通道標(biāo)識(shí)符 |
LineIdx | 8 | 4 | 行號(hào),每一行可以包含 RGB 三個(gè)通道的數(shù)據(jù),每個(gè)通道由多個(gè)分包組成 |
ValidDataLen | 12 | 4 | 數(shù)據(jù)部分有效字節(jié)數(shù) |
LineBytes | 16 | 4 | 每行數(shù)據(jù)包含的字節(jié)總數(shù) |
Reserve | 20 | 128 | 保留部分 |
Data | 148 | 1280 | 數(shù)據(jù)部分 |
end | 1428 | 4 | 包尾部的 Magic Number,設(shè)為 0x54456e64 |
上述表格描述的就是一個(gè)完整的 UDP 包。這里的一個(gè) UDP 數(shù)據(jù)包包含的是 RGB 某個(gè)通道的某一部分的數(shù)據(jù)。換種說(shuō)法:
一行數(shù)據(jù)
R 通道數(shù)據(jù)(若干個(gè)分包組成)
G 通道數(shù)據(jù)(若干個(gè)分包組成)
B 通道數(shù)據(jù)(若干個(gè)分包組成)
所以要生成/解析 UDP 包,最重要的是 PartialCnt、PartialIdx、RGB、LineIdx、Data 這幾個(gè)部分。清楚了自定義協(xié)議就可以開(kāi)始編寫(xiě)模擬包的生成和相應(yīng)的接收邏輯了。
由于本地開(kāi)發(fā)的時(shí)候缺少必要的硬件環(huán)境,為了方便開(kāi)發(fā),用 Python 編寫(xiě)一個(gè)簡(jiǎn)單的 UDPServer,發(fā)送模擬生成的數(shù)據(jù)包。根據(jù)上述協(xié)議,可以寫(xiě)出如下的 CameraData 類(lèi)來(lái)表示 UDP 數(shù)據(jù)包:
# -*- coding: utf-8 -*-DATA_START_MAGIC = bytearray(4)DATA_START_MAGIC[0] = 0x53 # SDATA_START_MAGIC[1] = 0x74 # tDATA_START_MAGIC[2] = 0x61 # aDATA_START_MAGIC[3] = 0x72 # rDATA_END_MAGIC = bytearray(4)DATA_END_MAGIC[0] = 0x54 # TDATA_END_MAGIC[1] = 0x45 # EDATA_END_MAGIC[2] = 0x6e # nDATA_END_MAGIC[3] = 0x64 # dslice_start_magic = slice(0, 4)slice_partial_cnt = 4slice_partial_idx = 5slice_sample_line = 6slice_rgb_extern = 7slice_line_idx = slice(8, 12)slice_valid_data_len = slice(12, 16)slice_line_bytes = slice(16, 20)slice_resv = slice(20, 148)slice_data = slice(148, 1428)slice_end_magic = slice(1428, 1432)import numpy as npclass CameraData(object):def __init__(self):# self.new()# self.rawdata = rawdataself.dataLow = 10self.dataHigh = 20self.new() def genRandomByte(self, by=4):r = bytearray(by) for i in range(by): r[i] = np.random.randint(0, 255) def setPackageIdx(self, i = 0):self.rawdata[slice_partial_idx] = i def setRGB(self, c = 1):self.rawdata[slice_rgb_extern] = c def setLineIdx(self, line):start = slice_line_idx.start self.rawdata[start+3] = 0x000000ff & line self.rawdata[start+2] = (0x0000ff00 & line) >> 8self.rawdata[start+1] = (0x00ff0000 & line) >> 16self.rawdata[start+0] = (0xff000000 & line) >> 24def setValidDataLen(self, len):start = slice_valid_data_len.start self.rawdata[start+3] = 0x000000ff & len self.rawdata[start+2] = (0x0000ff00 & len) >> 8self.rawdata[start+1] = (0x00ff0000 & len) >> 16self.rawdata[start+0] = (0xff000000 & len) >> 24def setLineBytes(self, len):start = slice_line_bytes.start self.rawdata[start+3] = 0x000000ff & len self.rawdata[start+2] = (0x0000ff00 & len) >> 8self.rawdata[start+1] = (0x00ff0000 & len) >> 16self.rawdata[start+0] = (0xff000000 & len) >> 24def randomData(self):size = slice_data.stop - slice_data.start arr = np.random.randint(self.dataLow, self.dataHigh, size, dtype=np.uint8) self.rawdata[slice_data] = bytearray(arr) def new(self):"""構(gòu)造新的數(shù)據(jù)對(duì)象 """self.rawdata = bytearray(1432) self.rawdata[slice_start_magic] = DATA_START_MAGIC self.rawdata[slice_partial_cnt] = 0x02self.rawdata[slice_partial_idx] = 0x00self.rawdata[slice_sample_line] = 0x03self.rawdata[slice_rgb_extern] = 0x01self.setLineIdx(0x00) self.setValidDataLen(1280) self.setLineBytes(1432) self.randomData() self.rawdata[slice_end_magic] = DATA_END_MAGIC def hex(self):return self.rawdata.hex() def __repr__(self):return '<CameraData@{} hex len: {}>'.format(hex(id(self)), len(self.rawdata))
CameraData 中的 rawdata 是一個(gè) bytearray 對(duì)象,它將會(huì)被 UdpServer 通過(guò)網(wǎng)絡(luò)接口發(fā)送出去。設(shè)置 4 個(gè)字節(jié)大小的整數(shù)時(shí)(如寫(xiě) LineIdx 行號(hào)),不能直接將數(shù)值賦到 rawdata 中,要將其中的 4 個(gè)字節(jié)分別賦值到對(duì)應(yīng)的地址上才行。
CameraData 中的 randomData 方法是模擬隨機(jī)數(shù)據(jù),更好的做法不是完全隨機(jī)給每個(gè)像素點(diǎn)賦值,而是有規(guī)律的變化,這樣在接收數(shù)據(jù)出現(xiàn)問(wèn)題、分析問(wèn)題的時(shí)候可以直觀地看到哪里有問(wèn)題。
然后我們需要定義一個(gè) UdpServer,用它來(lái)將數(shù)據(jù)對(duì)象中包含的信息發(fā)送出去。
import socketclass UdpServer( object ):"""該類(lèi)功能是處理底層的 UDP 數(shù)據(jù)包發(fā)送和接收,利用隊(duì)列緩存所有數(shù)據(jù) """def __init__(self, *args, **kwargs):self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)self._sock.bind( ('', DATA_PORT+11 ) ) self._sock.settimeout( None ) # never timeout# self._sock.setblocking( 0 ) # none blockdef send_msg( self, msg ):"""發(fā)送消息, @param msg 字典對(duì)象,發(fā)送 msg 的 rawdata 字段 """self._sock.sendto( msg.rawdata, ('192.168.8.1', DATA_PORT))
這個(gè) UdpServer 非常簡(jiǎn)單,因?yàn)楹罄m(xù)會(huì)通過(guò)這個(gè) UdpServer 不停的發(fā)包,但是每次發(fā)包必須等待發(fā)送端成功將 UDP 包發(fā)送出去,這里不要將 socket 對(duì)象設(shè)置成非阻塞的,否則程序運(yùn)行時(shí)會(huì)出現(xiàn)錯(cuò)誤提示(盡管可以忽略掉這個(gè)錯(cuò)誤提示,但是沒(méi)必要設(shè)置成非阻塞的,阻塞模式完全足夠了)。
在 github 中可以找到完整的 Python 文件,里面定義了其他類(lèi),如 DataSender
、RGBSender
。DataSender
是在一個(gè)線程里面發(fā)送 RGB 三個(gè)通道的值,RGBSender
的一個(gè)對(duì)象只會(huì)發(fā)送 RGB 三個(gè)通道中的某一個(gè)的值。
在本地測(cè)試的時(shí)候,為了方便在任務(wù)管理器中看到網(wǎng)絡(luò)占用率,最初是在 VirtualBox 的 ubuntu 虛擬機(jī)上運(yùn)行這個(gè) Python 程序的,但是受到虛擬機(jī)的資源分配和電腦性能影響,調(diào)用 singleMain
函數(shù)時(shí)每秒鐘最多只能產(chǎn)生 50MB 的數(shù)據(jù)量。但是在本地非虛擬機(jī)環(huán)境運(yùn)行的時(shí)候最多可以達(dá)到 80MB 的數(shù)據(jù)量。所以盡可能地使用本地環(huán)境運(yùn)行該 Python 程序可以最大限度的生成數(shù)據(jù)包。
如果讓 RGB 三個(gè)通道分別在三個(gè)不同的進(jìn)程中執(zhí)行發(fā)送過(guò)程(注釋掉 singleMain
的調(diào)用,換用 multiSend
方法),那么每秒鐘的數(shù)據(jù)量可到 200MB,不過(guò) 80MB 的數(shù)據(jù)量已經(jīng)足夠多了(接近千兆網(wǎng)卡的上限了,網(wǎng)絡(luò)利用率過(guò)高的話通過(guò)網(wǎng)線傳輸時(shí)會(huì)出現(xiàn)嚴(yán)重丟包的情況),不需要使用 multiSend
方法加大數(shù)據(jù)量。
在 singleMain 方法中,不直接執(zhí)行 dataSender.serve()
,而是在新進(jìn)程中執(zhí)行,可以更好的利用多核優(yōu)勢(shì),發(fā)送數(shù)據(jù)更快:
# singleMain()dataSender = DataSender() # dataSender.serve()p = Process(target=dataSender.serve) p.start()
實(shí)際開(kāi)發(fā)過(guò)程并不是這么順利,因?yàn)橐婚_(kāi)始并不知道在大量數(shù)據(jù)發(fā)送的時(shí)候,發(fā)送端能否有效地將數(shù)據(jù)發(fā)送出去,實(shí)際上是邊編寫(xiě) Python 的模擬發(fā)送數(shù)據(jù)程序,邊編寫(xiě) Qt 獲取數(shù)據(jù)的程序,根據(jù)出現(xiàn)的問(wèn)題逐步解決發(fā)送端和接收端的問(wèn)題的。
Qt 這邊作為客戶端,只需要將接收到的數(shù)據(jù)包保存下來(lái),獲取其中的有效數(shù)據(jù),再將 RGB 數(shù)據(jù)賦到 QImage 對(duì)應(yīng)的像素上顯示出來(lái)即可。GUI 部分比較簡(jiǎn)單,使用 QWidget 中的 label 控件,將 QImage 轉(zhuǎn)換成 QPixmap,顯示到 label 上就好了。初始化后的窗口如圖:
比較麻煩的是接收數(shù)據(jù)和拼接。同樣地,為了方便表示和解析每個(gè) UDP 包,我們構(gòu)造一些類(lèi)來(lái)存儲(chǔ)這些信息(現(xiàn)在想想似乎直接用結(jié)構(gòu)體表示會(huì)更簡(jiǎn)單)。
我們?cè)?Qt 中定義 CameraData
類(lèi)來(lái)表示數(shù)據(jù)包實(shí)體:
/** * @brief The CameraData class * 對(duì)應(yīng)從下位機(jī)接收到的字節(jié)數(shù)組的類(lèi),原始數(shù)據(jù)包,需要經(jīng)過(guò)處理后變成一行數(shù)據(jù) */class CameraData : public DataObj{ Q_OBJECTpublic: enum RGBType { R = 1, G = 2, B = 3, UNKOWN = 0}; static const QByteArray DATA_START_MAGIC; static const QByteArray DATA_END_MAGIC; static const int PacketSize; explicit CameraData(QObject *parent = 0); ~CameraData(); bool isPackageValid(); // 獲取保留區(qū)域的數(shù)據(jù)QByteArray getReserved(); // 設(shè)置原始數(shù)據(jù)void setRawData(const QByteArray &value); void setRawData(const char *data); // 獲取數(shù)據(jù)區(qū)域內(nèi)的所有數(shù)據(jù),默認(rèn)獲取有效數(shù)據(jù)QByteArray getData(bool valid = true); int getPackageCntInLine(); int getPackageIdxInLine(); int getSampleDiffLine(); int getRGBExtern(); RGBType getRGBType(); int getLineIdx(); int getValidDataLen(); int getLineBytes(); int sliceToInt(int start, int len = 4); // DataObj interfacevoid reset();signals:public slots:private: inline QByteArray slice(int start, int len = -1); inline QByteArray getStartMagic(); inline QByteArray getEndMagic(); QByteArray data; int packageCntInLine = -1; int packegeIdxInLine = -1; int lineIdx = -1; int lineBytes = -1; int rgbType = -1;};
CameraData
類(lèi)繼承自 DataObj
類(lèi),而 DataObj
類(lèi)又繼承自 QObject
,這樣方便進(jìn)行內(nèi)存管理和對(duì)象上的操作。DataObj
是為了方便復(fù)用對(duì)象而定義的基類(lèi),詳細(xì)代碼可參考 github 上的完整代碼。
C++ 部分的 CameraData
類(lèi)與 Python 中定義的 CameraData
類(lèi)是對(duì)應(yīng)的,不過(guò) C++ 部分的 CameraData
類(lèi)只需要調(diào)用 CameraData::setRawData
傳入一個(gè) QByteArray 對(duì)象后就可以自動(dòng)將其中包含的數(shù)據(jù)解析出來(lái),并且它只提供獲取數(shù)據(jù)的接口而不提供修改數(shù)據(jù)的接口。
另外我們還需要定義一個(gè)類(lèi) PreProcessData,來(lái)表示一行數(shù)據(jù):
/** * @brief The PreProcessData class * 預(yù)處理數(shù)據(jù) */class PreProcessData: public DataObj{ Q_OBJECTpublic: static const int PacketSize; static const int PacketPerLine; explicit PreProcessData(QObject *parent = 0, int line = -1); void put(CameraData *cd); bool isReady(); void reset(); int line() const; void setLine(int line); const QByteArrayList &getDataList() const; QByteArray repr();private: /** * @brief cameraData * 每 2 個(gè) CameraData 構(gòu)成一行的單通道數(shù)據(jù),有序存放 RGB 通道數(shù)據(jù) * 0-1 存放 R,2-3 存放 G, 4-5 存放 B */QByteArrayList dataList; int m_line; int m_readyCount = 0; int m_duplicateCount = 0; bool *dataPlaced = 0;};
目前的協(xié)議中,每 2 個(gè)數(shù)據(jù)包(對(duì)應(yīng) 2 個(gè) CameraData
對(duì)象)構(gòu)成某一行的單通道數(shù)據(jù),所以 PreProcessData
中至少會(huì)包含 6 個(gè) CameraData
對(duì)象,處理完 CameraData
對(duì)象后,只需要存儲(chǔ) Data 部分即可,所以這里沒(méi)有用 QList 列表,而是直接使用 QByteArrayList
來(lái)存儲(chǔ)數(shù)據(jù)。當(dāng)三個(gè)通道的數(shù)據(jù)都準(zhǔn)備好后,PreProcessData::isReady
就會(huì)返回 true,表示該行數(shù)據(jù)已經(jīng)準(zhǔn)備好,可以顯示在窗口中。
我們定義一個(gè) Controller
類(lèi)用來(lái)操作數(shù)據(jù)接收對(duì)象和子線程。用 Qt 的事件槽機(jī)制和 QObject::moveToThread
實(shí)現(xiàn)多線程非常方便,不重寫(xiě) QThread 的 run 方法就可以讓對(duì)象的方法在子線程中執(zhí)行。
class Controller : public QObject{ Q_OBJECTpublic: explicit Controller(QObject *parent = 0); ~Controller(); static const int DataPort; static const int CONTROL_PORT; static const QStringList BOARD_IP; void start(); void stop(); DataProcessor *getDataProcessor() const;signals:public slots:private: CameraDataReceiver *cdr; QThread recvThread; QThread recvProcessThread; QByteArrayList rawdataList; DataProcessor *dp = 0; QTimer *statsTimer; int statsInterval;};
其中 CameraDataReceiver
對(duì)象會(huì)被實(shí)例化,在子線程中接收 UDP 數(shù)據(jù)包(因?yàn)榘l(fā)送和接收數(shù)據(jù)的端口是不同的,操作和數(shù)據(jù)是分離的)。這里將 DataProcessor 通過(guò) getDataProcessor
暴露給上層應(yīng)用,以便上層應(yīng)用連接信號(hào)槽接收?qǐng)D像。僅到接收數(shù)據(jù),就用到了三個(gè)線程:分別是 GUI 線程,用于接收 UDP 包的 recvThread 線程和處理 UDP 的 recvProcessThread。
為什么接收 UDP 包和處理 UDP 包不是放在一個(gè)線程中執(zhí)行呢?因?yàn)檫@里的數(shù)據(jù)量實(shí)在太多,最開(kāi)始實(shí)現(xiàn)的時(shí)候這兩個(gè)邏輯代碼確實(shí)是在同一個(gè)線程中執(zhí)行,然而由于處理數(shù)據(jù)的代碼執(zhí)行起來(lái)也要消耗時(shí)間,將會(huì)導(dǎo)致無(wú)法接收其他的 UDP 包,這樣的話就會(huì)導(dǎo)致比較嚴(yán)重的丟包。為了保證接收端不會(huì)丟包,只好將處理邏輯放在其他的線程中執(zhí)行。
將接收數(shù)據(jù)和處理數(shù)據(jù)放在不同的線程中執(zhí)行,確實(shí)可以解決丟包問(wèn)題了,但是會(huì)出現(xiàn)新的問(wèn)題:接收到的包如果不能夠及時(shí)處理完,并且釋放掉相應(yīng)的資源,那么可能會(huì)出現(xiàn)程序?qū)?shù)據(jù)緩存下來(lái)但無(wú)法處理,程序占用的內(nèi)存越來(lái)越大,導(dǎo)致程序運(yùn)行起來(lái)越來(lái)越慢。
在編寫(xiě)程序時(shí)誤以為是 Qt 的事件循環(huán)機(jī)制過(guò)慢導(dǎo)致程序處理不了那么多數(shù)據(jù)(實(shí)際上它的速度足夠處理這些數(shù)據(jù)),因此將程序中使用的 QUdpSocket 對(duì)象換成了 [Windows 平臺(tái)的 Socket 通信代碼][winsock demo],并將其改寫(xiě)成類(lèi)方便調(diào)用。實(shí)際上是在 QThread 子線程中無(wú)限循環(huán)地運(yùn)行 recvfrom(clientSocket, recvedData.data(), recvbuflen, 0, &fromaddr, &addrLen);
這樣的接收數(shù)據(jù)包函數(shù),跳過(guò)了 Qt 事件循環(huán)機(jī)制,然后當(dāng)接收到包之后再通過(guò)回調(diào)函數(shù)通知數(shù)據(jù)處理線程進(jìn)行處理。
但當(dāng)我寫(xiě)這篇博客,重新用正常的代碼進(jìn)行測(cè)試時(shí),發(fā)現(xiàn)即便使用 QUdpSocket::readyRead
信號(hào)來(lái)接收 UDP 數(shù)據(jù),只要數(shù)據(jù)處理進(jìn)程不堆積數(shù)據(jù),就不會(huì)出現(xiàn)占用內(nèi)存越來(lái)越多的情況。換句話說(shuō),不是 Qt 無(wú)法處理實(shí)時(shí)性的數(shù)據(jù),而是自己編寫(xiě)的代碼里面有問(wèn)題。
回想最開(kāi)始寫(xiě)的程序,在處理 QByteArray 表示的原始數(shù)據(jù)時(shí),會(huì)為每一個(gè)接收到的數(shù)據(jù)包分配地址,而且分配的地址位于堆中。而實(shí)際上在堆 heap 中分配回收內(nèi)存地址相較于在棧 stack 中是慢得多的。為每個(gè)到來(lái)的數(shù)據(jù)用 new 構(gòu)造一個(gè)新的 CameraData 對(duì)象,然后在處理完后將這個(gè) CameraData delete 掉其實(shí)是很慢的,如果你這樣做了,并且你在 CameraData 的析構(gòu)函數(shù)中加上 qDebug 語(yǔ)句打印 "CameraData is deleting...",你會(huì)發(fā)現(xiàn),當(dāng)發(fā)送方(我們的 Python 模擬發(fā)送程序)停止發(fā)送數(shù)據(jù)包后很長(zhǎng)一段時(shí)間內(nèi),Qt 程序在一直打印著 "CameraData is deleting"。
而我最開(kāi)始就是這么做的,所以發(fā)生了 Qt 程序隨著數(shù)據(jù)接收的變多,占用的內(nèi)存越來(lái)越大的情況。當(dāng)然,這不排除 qDebug 語(yǔ)句輸出到控制臺(tái)上也會(huì)占用很多時(shí)間。如果每秒鐘要調(diào)用上萬(wàn)次 qDebug() << "CameraData is deleting"
,那么建議你使用一個(gè)計(jì)數(shù)變量控制 qDebug 的調(diào)用次數(shù),因?yàn)檫@條語(yǔ)句的調(diào)用也會(huì)讓數(shù)據(jù)處理變得緩慢。
為了讓接收端不丟包,需要快速的處理接收到的 UDP 包,并且在處理的代碼中不要調(diào)用耗時(shí)的函數(shù)或者 new 操作。為了避免重復(fù)調(diào)用 new 和 delete 操作符,我們需要構(gòu)建一個(gè)對(duì)象池,以便復(fù)用池中的對(duì)象,減少 new 操作。池的定義比較簡(jiǎn)單,封裝一個(gè) QList
容器類(lèi)就好了,為了簡(jiǎn)化和復(fù)用池的代碼,我用到了 c++ 的 template 特性,但是這個(gè) DataObjPool
中的容器只能是 DataObj 的子類(lèi):
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);};
當(dāng)然你也可以直接編寫(xiě)兩個(gè)類(lèi) RawDataObjPool
和 LineDataPool
,把池的操作分別復(fù)制到兩個(gè)類(lèi)中,使用模板特化的好處是改動(dòng)的時(shí)候不需要改動(dòng)兩個(gè)類(lèi)了。前面說(shuō)過(guò),DataObj
類(lèi)繼承自 QObject
,就是為了簡(jiǎn)化在對(duì)象池中進(jìn)行的操作。DataObjPool
會(huì)在構(gòu)造時(shí)在內(nèi)存中預(yù)分配一定數(shù)量的對(duì)象,以 RawDataObjPool
為例,構(gòu)造時(shí)傳入 size 參數(shù),便會(huì)預(yù)先在內(nèi)存中創(chuàng)建 size 個(gè) CameraData,在程序運(yùn)行過(guò)程中,這些對(duì)象都會(huì)被我們這個(gè) Qt 程序循環(huán)利用,直到關(guān)閉程序才會(huì)釋放掉這些 CameraData(如果操作系統(tǒng)的內(nèi)存不足,過(guò)多的對(duì)象占用的內(nèi)存還是會(huì)被釋放)。
對(duì)象池的主要接口有兩個(gè):getAvailable
和 release
分別用于獲取可用的對(duì)象或釋放掉池中的對(duì)象,注意這里的釋放是讓對(duì)象池對(duì)該對(duì)象進(jìn)行標(biāo)記,以便重復(fù)使用,而不是釋放掉該對(duì)象占用的內(nèi)存空間或 delete 掉。當(dāng)對(duì)象池中無(wú)可用對(duì)象時(shí),可以根據(jù)需要釋放掉超時(shí)的對(duì)象或者釋放掉全部對(duì)象。
使用對(duì)象池減少 new 操作符的使用后,處理數(shù)據(jù)的子線程的速度明顯加快。正常情況下就可以看到如下的圖片:
這里數(shù)據(jù)顯示的部分還有待完善,因?yàn)榘l(fā)送端的發(fā)送數(shù)據(jù)大小不夠湊成一行,所以圖片的右側(cè)部分是空白的。
這里說(shuō)一下數(shù)據(jù)的復(fù)制,從 Socket 接口中傳上來(lái)的數(shù)據(jù),我們用 QByteArray
對(duì)象保存了底層的數(shù)據(jù),即便在 UDP 數(shù)據(jù)包中含有很多個(gè) \x00
這樣的數(shù)據(jù),QByteArray 也會(huì)正確識(shí)別出字符串的結(jié)束位置。
在設(shè)置 CameraData::setRawData(const QByteArray &value)
函數(shù)中,盡量避免手動(dòng)調(diào)用 memcpy(data.data(), value, value.size());
這個(gè)底層 API,因?yàn)槟悴恢浪鼤?huì)將 QByteArray 對(duì)象 CameraData.data
中的 char * data()
指針指向哪個(gè)位置。
我在 CameraData.cpp
文件中將它注釋掉了,因?yàn)樵诔绦蜻\(yùn)行和調(diào)試時(shí)它給我?guī)?lái)了巨大的困惑:經(jīng)常出現(xiàn) invalid address specified to rtlvalidateheap
這種類(lèi)型的錯(cuò)誤。經(jīng)過(guò)很長(zhǎng)時(shí)間的排查后發(fā)現(xiàn)注釋掉這行代碼,程序就能一直穩(wěn)定運(yùn)行。
在 c++ 程序中要使用大量可重用的對(duì)象時(shí),盡量避免頻繁地使用 new 操作符新建對(duì)象,使用對(duì)象池來(lái)獲取對(duì)象,這樣可以加快程序的運(yùn)行速度。
Qt 的事件循環(huán)機(jī)制實(shí)際上運(yùn)行地足夠快,是可以處理實(shí)時(shí)性的數(shù)據(jù)的,在程序出現(xiàn)問(wèn)題時(shí),還是應(yīng)該多找找自己編寫(xiě)的代碼中的問(wèn)題。
對(duì)于 memcpy 這類(lèi)的底層 API,不熟悉的話盡量少用,否則出現(xiàn)問(wèn)題很難 debug。
聯(lián)系客服