从使用者的角度来看,QIODevice及其子类可以说是非常好用的。当我们想拥有自己的设备类时,我们可以参照QIODevice来设计自己的设备类,也可以直接去继承QIODevice。当我们选择继承QIODevice时,需要考虑的就很多了。这里咱们讨论继承QIODevice时需要注意哪些事项。
- 随机设备or顺序设备
我们先来看继承QIODevice的类都有哪些。
当然这张图少了一些类,但这不是重点,重点是我们观察到,QFile这种随机读写的设备与QTcpSocket这种顺序读写的设备共同继承于QIODevice,那么QIODevice理所当然就要区分这两种设备。
如上一章内容所讲,我们继承QIODevice时需要重写isSequential()函数,这个函数会返回一个bool值,如果返回true,那么这就是一个顺序设备;返回false代表这是一个随机设备。
我们从下面QIODevice的源代码中也可以看到,QIODevice的一些函数也会先判断设备类型,再决定执行某些操作。
- 顺序设备不可用函数
我们在使用QFile类去操作文件时,时常需要去调用QIODevice::size()函数去获取文件的大小,这没什么问题,但如果在顺序设备中调用QIODevice::size()函数就不太合适了,例如我们在使用QTcpSocket时,我们并不需要socket的大小,我们只需要调用QIODevice::bytesAvailable()来获取在当前缓冲区存储的信息的大小。
还有一些其他的函数需要我们根据设备类型去区分,例如QIODevice::seek()、QIODevice::bytesToWrite()、QIODevice::canReadLine()、QIODevice::atEnd()等,有些是顺序设备专用,而有些是随机设备专用。
- QIODevice内部缓冲区
我们知道如果继承QIODevice只需要重写两个函数,分别是QIODevice::readData()、QIODevice::writeData()。这分别代表了对设备的读和写,但细心的人可能会发现,如果只是单纯的继承的了这两个函数,那调用QIODevice::bytesAvailable()函数时,QIODevice是怎么知道缓冲区的大小的。这也勾起了我的好奇心,于是我查看了源代码。
我们从源码上能看出,当我们是顺序设备时,会得到一个buffer的长度减去transactionPos,transactionPos我们先暂且不管,这个变量和QIODevice的事务有关,我们先暂且认为transactionPos为0,熟悉qt的同学可能知道q指针和d指针的概念,不熟悉的同学可以认为d里面的变量都属于QIODevice的私有变量,那buffer是什么?我们接着看源码。
buffer的类型为QRingBufferRef,而QRingBufferRef是QRingBuffer的封装类,QRingBuffer其实就是一个环形字节数组。也就是说QIODevice有自己的缓冲区,当我们获取缓冲区的大小时,QIODevice直接读取整个buffer的大小。
那缓冲区的数据从哪来,答案肯定通过我们重写的QIODevice::readData()函数。
- 顺序设备必须重写函数
当我们实现一个顺序设备时,我们除了要重写QIODevice::readData()、QIODevice::writeData()函数时,还需要重写其他函数,例如QIODevice::bytesAvailable()、QIODevice::bytesToWrite()、QIODevice::canReadLine()等函数,这是由于QIODevice的内部缓存的问题。
当我们调用QIODevice::bytesAvailable()时,我们会获取QIODevice内部buffer的长度,但这是我们设备自己内部还有一些信息并没有同步到内部buffer时,便会导致我们获取的长度与实际的长度不符。
class MyStreamDevice : public QIODevice
{
Q_OBJECT
public:
explicit MyStreamDevice(QObject *parent = nullptr);
virtual bool isSequential() const override{
return true;
}
protected:
qint64 readData(char *data, qint64 maxSize) override{
if (m_buffer.isEmpty())
return 0;
qint64 bytesToRead = qMin(maxSize, static_cast<qint64>(m_buffer.size()));
memcpy(data, m_buffer.constData(), bytesToRead);
m_buffer.remove(0, bytesToRead);
return bytesToRead;
}
qint64 writeData(const char *data, qint64 maxSize) override {
m_buffer.append(data, maxSize);
return maxSize;
}
private:
QByteArray m_buffer;
};
这里我简单的实现了一个流式设备,信息存储到了m_buffer里。我们写下代码测试一下。
MyStreamDevice device;
device.open(QIODevice::ReadWrite);
device.write("1234567\r\n");
qDebug()<<"device.bytesAvailable():"<<device.bytesAvailable();
输出的结果如下
device.bytesAvailable(): 0
明明我们向device写入了数据,但获取长度时显示为0,这显然是错误的。那我们就需要自己去实现QIODevice::bytesAvailable()了,如何实现?这里qt文档给了我们指引。
qint64 CustomDevice::bytesAvailable() const
{
return buffer.size() + QIODevice::bytesAvailable();
}
当要获取顺序设备可读数据的大小时,我们需要将自己buffer的大小和QIODevice内部buffer的大小相加,才能获取真实的大小。
同理,其他需要重写的函数也是如此,这里我直接贴下代码。
//MyStreamDevice.h
class MyStreamDevice : public QIODevice
{
Q_OBJECT
public:
explicit MyStreamDevice(QObject *parent = nullptr);
virtual bool isSequential() const override;
virtual qint64 bytesAvailable() const override;
virtual qint64 bytesToWrite() const override;
virtual bool canReadLine() const override;
protected:
qint64 readData(char *data, qint64 maxSize) override;
qint64 writeData(const char *data, qint64 maxSize) override;
private:
QByteArray m_buffer;
};
//MyStreamDevice.cpp
#include "MyStreamDevice.h"
MyStreamDevice::MyStreamDevice(QObject *parent)
: QIODevice{parent}
{}
qint64 MyStreamDevice::readData(char *data, qint64 maxSize){
if (m_buffer.isEmpty())
return 0;
qint64 bytesToRead = qMin(maxSize, static_cast<qint64>(m_buffer.size()));
memcpy(data, m_buffer.constData(), bytesToRead);
m_buffer.remove(0, bytesToRead);
return bytesToRead;
}
qint64 MyStreamDevice::writeData(const char *data, qint64 maxSize){
m_buffer.append(data, maxSize);
return maxSize;
}
bool MyStreamDevice::isSequential() const {
return true;
}
qint64 MyStreamDevice::bytesAvailable() const
{
return QIODevice::bytesAvailable() + m_buffer.size();
}
qint64 MyStreamDevice::bytesToWrite() const
{
return 0;
}
bool MyStreamDevice::canReadLine() const
{
return QIODevice::canReadLine() || m_buffer.indexOf('\n',0)>=0;
}
QIODevice注意事项就这些,如果日后我再遇到坑我再补充,其实我个人觉得顺序设备和随机设备并不应该在一个类里实现,但没办法。同时我还推荐看一看QLocalSocket的实现,这个类告诉了我们一个标准的顺序设备怎么实现,同时也没有QTcpSocket实现的那么复杂。