QIODevice继承注意事项

103 阅读5分钟

从使用者的角度来看,QIODevice及其子类可以说是非常好用的。当我们想拥有自己的设备类时,我们可以参照QIODevice来设计自己的设备类,也可以直接去继承QIODevice。当我们选择继承QIODevice时,需要考虑的就很多了。这里咱们讨论继承QIODevice时需要注意哪些事项。

  1. 随机设备or顺序设备

我们先来看继承QIODevice的类都有哪些。

捕获.PNG

当然这张图少了一些类,但这不是重点,重点是我们观察到,QFile这种随机读写的设备与QTcpSocket这种顺序读写的设备共同继承于QIODevice,那么QIODevice理所当然就要区分这两种设备。

如上一章内容所讲,我们继承QIODevice时需要重写isSequential()函数,这个函数会返回一个bool值,如果返回true,那么这就是一个顺序设备;返回false代表这是一个随机设备。

我们从下面QIODevice的源代码中也可以看到,QIODevice的一些函数也会先判断设备类型,再决定执行某些操作。

1.PNG

  1. 顺序设备不可用函数

我们在使用QFile类去操作文件时,时常需要去调用QIODevice::size()函数去获取文件的大小,这没什么问题,但如果在顺序设备中调用QIODevice::size()函数就不太合适了,例如我们在使用QTcpSocket时,我们并不需要socket的大小,我们只需要调用QIODevice::bytesAvailable()来获取在当前缓冲区存储的信息的大小。

还有一些其他的函数需要我们根据设备类型去区分,例如QIODevice::seek()、QIODevice::bytesToWrite()、QIODevice::canReadLine()、QIODevice::atEnd()等,有些是顺序设备专用,而有些是随机设备专用。

  1. QIODevice内部缓冲区

我们知道如果继承QIODevice只需要重写两个函数,分别是QIODevice::readData()、QIODevice::writeData()。这分别代表了对设备的读和写,但细心的人可能会发现,如果只是单纯的继承的了这两个函数,那调用QIODevice::bytesAvailable()函数时,QIODevice是怎么知道缓冲区的大小的。这也勾起了我的好奇心,于是我查看了源代码。

2.PNG

我们从源码上能看出,当我们是顺序设备时,会得到一个buffer的长度减去transactionPos,transactionPos我们先暂且不管,这个变量和QIODevice的事务有关,我们先暂且认为transactionPos为0,熟悉qt的同学可能知道q指针和d指针的概念,不熟悉的同学可以认为d里面的变量都属于QIODevice的私有变量,那buffer是什么?我们接着看源码。

3.PNG

buffer的类型为QRingBufferRef,而QRingBufferRefQRingBuffer的封装类,QRingBuffer其实就是一个环形字节数组。也就是说QIODevice有自己的缓冲区,当我们获取缓冲区的大小时,QIODevice直接读取整个buffer的大小。

那缓冲区的数据从哪来,答案肯定通过我们重写的QIODevice::readData()函数。

  1. 顺序设备必须重写函数

当我们实现一个顺序设备时,我们除了要重写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实现的那么复杂。