QIODevice 为支持读取和写入数据块的设备(如QFile、QBuffer、和QTcpSocket)提供了通用实现和抽象接口。QIODevice 是抽象的,无法实例化,但通常使用它定义的接口来提供与设备无关的 I/O 功能。例如,Qt的XML类在QIODevice指针上运行,允许它们与各种设备(如文件和缓冲区)一起使用。
在访问设备之前,必须调用open()
来设置正确的 OpenMode(例如 ReadOnly 或 ReadWrite)。然后,可以使用write()
或putChar()
写入设备,并通过调用read()
、readLine()
或readAll()
进行读取。使用完设备后调用close()
。
public:
virtual bool open(OpenMode mode);
virtual void close();
QIODevice::open()对QIODevicePrivate类d指针的属性数据进行设置。
bool QIODevice::open(OpenMode mode)
{
Q_D(QIODevice);
d->openMode = mode;
d->pos = (mode & Append) ? size() : qint64(0);
d->accessMode = QIODevicePrivate::Unset;
d->readBuffers.clear();
d->writeBuffers.clear();
//.............................
return true;
}
public:
qint64 read(char *data, qint64 maxlen);
QByteArray read(qint64 maxlen);
QByteArray readAll();
qint64 readLine(char *data, qint64 maxlen);
QByteArray readLine(qint64 maxlen = 0);
virtual bool canReadLine() const;
QIODevice类中提供了读写等操作的接口,QIODevicePrivate中保存实际的数据和相关属性、相关操作。几个读取函数最终都调用了qint64 QIODevice::read(char *data, qint64 maxSize)
。在内部调用d指针的buffer成员的getchar()
或者QIODevicePrivate::read()
。
qint64 QIODevice::read(char *data, qint64 maxSize)
{
Q_D(QIODevice);
const bool sequential = d->isSequential();
// getChar()的快捷方式,除非我们需要将数据保存在缓冲区中。
if (maxSize == 1 && !(sequential && d->transactionStarted)) {
while ((chint = d->buffer.getChar()) != -1) {
//................................
return (qint64(1));
}
}
//....................................
const qint64 readBytes = d->read(data, maxSize);
return readBytes;
}
public:
qint64 peek(char *data, qint64 maxlen);
QByteArray peek(qint64 maxlen);
qint64 skip(qint64 maxSize);
peek:从设备读取maxlen的字节,而不会产生不好的结果。
skip:从设备跳到maxSize字节。返回实际跳过的字节数。
public:
qint64 write(const char *data, qint64 len);
qint64 write(const char *data);
inline qint64 write(const QByteArray &data)
{ return write(data.constData(), data.size()); }
写操作调用了writeData纯虚函数,同时对d指针的属性成员做相应更改,在windows系统和非windows系统中的处理不同。
qint64 QIODevice::write(const char *data, qint64 maxSize)
{
Q_D(QIODevice);
//........................................
#ifdef Q_OS_WIN
//.................writeData
#endif
qint64 written = writeData(data, maxSize);
if (!sequential && written > 0) {
d->pos += written;
d->devicePos += written;
d->buffer.skip(written);
}
return written;
}
public:
void ungetChar(char c);
bool putChar(char c);
bool getChar(char *c);
putChar:将字符c写入设备。调用了QIODevicePrivate::putCharHelper(char)
,该函数中又会调用QIODevice::write
。
bool QIODevice::putChar(char c)
{
return d_func()->putCharHelper(c);
}
bool QIODevicePrivate::putCharHelper(char c)
{
return q_func()->write(&c, 1) == 1;
}
getChar:从设备中读取一个字符并将其存储在c中。调用了QIODevice::read(char *, int)
bool QIODevice::getChar(char *c)
{
// readability checked in read()
char ch;
return (1 == read(c ? c : &ch, 1));
}
ungetChar:将字符c放回设备中,并递减当前位置,除非位置为0。调用此函数通常用于“撤消”getChar()操作。
QIODevice分为两种类型的设备:随机存取设备和顺序设备,可以使用isSequential()
来确定设备的类型。
seek()
寻找任意位置。通过调用pos()
可以获取文件中的当前位置。QFile和QBuffer都是随机存取设备。
pos()
和size()
不适用于顺序设备。QTcpSocket和QProcess都是顺序设备。
public:
virtual qint64 pos() const;
virtual qint64 size() const;
virtual bool seek(qint64 pos);
virtual bool atEnd() const;
virtual bool reset();
seek:对于随机存取设备,此函数将当前位置设置为 pos。
当有新数据可供读取时,QIODevice 发出readyRead()
;例如,如果新数据已到达网络,或者如果将其他数据追加到要读取的文件。您可以调用bytesAvailable()
来确定当前可用于读取的字节数。在使用异步设备(例如QTcpSocket)进行编程时,通常将bytesAvailable()
与readyRead()
信号一起使用,其中数据片段可以在任意时间点到达。QIODevice 每次将数据有效载荷写入设备时都会发出bytesWritten()
信号。使用bytesToWrite()
确定当前等待写入的数据量。
Q_SIGNALS:
void readyRead();
void channelReadyRead(int channel);
void bytesWritten(qint64 bytes);
void channelBytesWritten(int channel, qint64 bytes);
void aboutToClose();
void readChannelFinished();
Q_SIGNALS和signals宏的内容一样,都是替换为public
# define Q_SIGNALS public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal)
# define QT_ANNOTATE_ACCESS_SPECIFIER(x)
public:
virtual qint64 bytesAvailable() const;
virtual qint64 bytesToWrite() const;
QIODevice 的某些子类(QTcpSocket和QProcess)是异步的。这意味着write()
或read()
等I/O 功能总是立即返回,而当控制返回到事件循环时,可能会与设备本身进行通信。QIODevice提供了一些函数,允许强制立即执行这些操作,同时阻塞调用线程,而无需进入事件循环。这允许在没有事件循环的情况下使用QIODevice子类,或者在单独的线程中使用:
waitForReadyRead()
,此函数暂停调用线程中的操作,直到有新数据可供读取。waitForBytesWritten()
,此函数暂停调用线程中的操作,直到将一个数据有效负载写入设备。waitFor....()
,QIODevice的子类为特定于设备的操作实现阻塞功能。例如,QProcess有一个名为waitForStarted()
的函数,该函数暂停调用线程中的操作,直到进程启动。public:
virtual bool waitForReadyRead(int msecs);
virtual bool waitForBytesWritten(int msecs);
从主 GUI 线程调用这些函数可能会导致用户界面冻结。
通过对 QIODevice 进行子类化,可以为自己的 I/O 设备提供相同的接口。QIODevice 的子类只需要实现受保护的readData()
和writeData()
函数。QIODevice 使用这些函数来实现其所有便利功能,例如getChar()
、readLine()
和write()
。QIODevice还处理访问控制,因此在调用writeData()
可以放心地假设设备以写入模式打开。
protected:
virtual qint64 readData(char *data, qint64 maxlen) = 0;
virtual qint64 readLineData(char *data, qint64 maxlen);
virtual qint64 writeData(const char *data, qint64 len) = 0;
某些子类(QFile和QTcpSocket)是使用内存缓冲区实现的,用于中间存储数据。这减少了设备访问调用的数量,这些调用通常非常慢。缓冲使getChar()
和putChar()
等函数变得快速,因为它们可以在内存缓冲区上运行,而不是直接在设备本身上运行。但是,某些 I/O 操作不能很好地与缓冲区配合使用。例如,如果多个用户打开同一设备并逐个字符读取它,则当他们打算读取每个单独的块时,他们最终可能会读取相同的数据。出于这个原因,QIODevice 允许通过将 Unbuffered 标志传递给open()
来绕过任何缓冲。在对 QIODevice 进行子类化时,请记住在无缓冲模式下打开设备时绕过可能使用的任何缓冲区。
通常,来自异步设备的传入数据流是碎片化的,数据块可以在任意时间点到达。要处理数据结构的不完整读取,请使用 QIODevice 实现的事务机制。
void startTransaction();
void commitTransaction();
void rollbackTransaction();
bool isTransactionStarted() const;
startTransaction:在设备上启动新的读取事务,定义读取操作序列中的可还原点。
commitTransaction:完成读取事务。对于顺序设备,事务期间记录在内部缓冲区中的所有数据都将被丢弃。
rollbackTransaction:回滚读取事务。
class Q_CORE_EXPORT QIODevicePrivate : public QObjectPrivate
{
Q_DECLARE_PUBLIC(QIODevice)
public:
QIODevicePrivate();
virtual ~QIODevicePrivate();
QIODevice::OpenMode openMode;
QString errorString;
//...........................................
class QRingBufferRef {
QRingBuffer *m_buf;
//getChar.putChar.size.clear.skip.append.peek...............
};
QRingBufferRef buffer;
QRingBufferRef writeBuffer;
qint64 pos;
qint64 devicePos;
enum AccessMode {//............};
mutable AccessMode accessMode;
//........函数.......
};
如果允许缓冲且有数据在缓冲区中,直接buffer.read()
读取缓冲区数据。如果读取数据小于目标数据,1.readData()
直接从设备上读取。2.readData()
从设备读取到缓存区,进入下一次循环,继续buffer.read()
从缓冲区读取。
qint64 QIODevicePrivate::read(char *data, qint64 maxSize, bool peeking)
{
Q_Q(QIODevice);
//.............
forever {
qint64 bufferReadChunkSize = keepDataInBuffer
? buffer.peek(data, maxSize, bufferPos)
: buffer.read(data, maxSize);
maxSize -= bufferReadChunkSize;
if (maxSize > 0 && !deviceAtEof) {
//.............
if ((!buffered || maxSize >= readBufferChunkSize) && !keepDataInBuffer) {
readFromDevice = q->readData(data, maxSize);
//.............
} else {
readFromDevice = q->readData(buffer.reserve(bytesToBuffer), bytesToBuffer);
//.............
}
//.............
}
}
}
QRingBuffer是QIODevice用来存储缓冲数据的类,在该类中定义了一个动态数组,在头部读取,尾部追加。
bufferSize为缓冲区存储数据大小,basicBufferSize为可存储数据大小,#define QRINGBUFFER_CHUNKSIZE 4096
,在QRingBuffer初始化时会将这个宏赋值给basicBufferSize。
class QRingBuffer
{
public:
inline int getChar() {//........}
inline void putChar(char c) {
char *ptr = reserve(1);//返回地址
*ptr = c;
}
Q_CORE_EXPORT qint64 read(char *data, qint64 maxLength);
//..........................
private:
QVector<QRingChunk> buffers;
qint64 bufferSize;
int basicBlockSize;
};
缓冲区读取最终调用函数,先取最大长度和缓冲区数据大小的较小值作为总读取数据长度。每一次循环中取剩余长度和下一块待读取数据长度的较小值作为单次读取数据的长度。memcpy拷贝缓冲数据,拷贝完释放数据,从动态数组buffers中移除。
qint64 QRingBuffer::read(char *data, qint64 maxLength)
{
const qint64 bytesToRead = qMin(size(), maxLength);
qint64 readSoFar = 0;
while (readSoFar < bytesToRead) {
const qint64 bytesToReadFromThisBlock = qMin(bytesToRead - readSoFar,
nextDataBlockSize());
if (data)
memcpy(data + readSoFar, readPointer(), bytesToReadFromThisBlock);
readSoFar += bytesToReadFromThisBlock;
free(bytesToReadFromThisBlock);
}
return readSoFar;
}
QRingBuffer用来存储数据的QVector<QRingChunk> buffers
存储了若干个QRingChunk对象,该类对象就是对QByteArray做简单封装,增加头尾索引值。
class QRingChunk
{
public:
void allocate(int alloc);
inline const char *data() const
{
return chunk.constData() + headOffset;
}
//...............................
private:
QByteArray chunk;
int headOffset, tailOffset;
};