一、复制粘贴的简单原理
复制粘贴大概可以分为以下两种场景:
- 在同一个进程中复制并粘贴
- 在进程A(源窗口)中复制,在进程B(目的窗口)中粘贴
场景1好理解,数据都在一个进程中,直接在内存中设置和读取就可以了,不涉及跨进程之间的问题
场景2数据会跨进程传递,那就应该用了某一种进程间通信技术,而在wayland平台使用的就是匿名管道(pipe)
但是,他们是直接进程A和进程B之间进行通信吗?还是怎样?具体下面会给出分析
二、几个关于复制粘贴相关的类
QClipboard: 提供了对窗口系统剪贴板的访问
-
该类提供了一些方便的接口来访问普通数据类型,更灵活的数据可以通过mimeData()接口获取
-
在应用程序中只有一个全局的QClipboard,你可以使用QApplication::clipboard()来获取
-
你可以通过调用clear()清空剪贴板
-
QClipboard和QDragObject支持一样的数据类型,并且使用类似的机制
拓展:不同平台下的剪切板实现不同
X窗口系统有一个选择的概念—当文本被选择,它立即被复制到全局鼠标选择系统中,此时可用鼠标中键来粘贴全局鼠标选择系统中的内容;X窗口系统还有一个所有权的概念,如果你在一个窗口中改变了选择,X11仅仅通知变化的拥有者和前任拥有者
Windows仅仅在文本被显示地复制或者剪切的时候才被复制到剪贴板,是一个完完全全的全局资源,所以所有应用程序都会被通知变化
macOS支持一个单独的查找缓冲区,该缓冲区在“查找”操作中保存当前的搜索字符串。可以通过指定FindBuffer模式来访问此查找剪贴板。
通过以上拓展可以了解,QClipboard几个Mode {Clipboard, Selection, FindBuffer}的用处:
- Clipboard 全局剪切板系统,所有平台都支持,包括wayland平台
- Selection 全局鼠标选择系统,X11平台支持
- FindBuffer 单独的查找缓冲区,只有macOS平台支持
QMimedata: 为数据提供一个容器,用来记录关于MIME类型数据的信息
-
常用来描述保存在剪切板里信息,或者拖拽信息
-
QMimeData对象把它所保存的信息和正确的MIME类型连接起来,来保证信息可以被安全的在应用程序之间转移,或者在同一个应用程序之间拷贝
-
对于最常见的MIME类型,QMimeData提供了方便的功能来访问数据:
Tester Getter Setter MIME Types hasText() text() setText() text/plain
hasHtml() html() setHtml() text/html
hasUrls() urls() setUrls() text/uri-list
hasImage() imageData() setImageData() image/
*hasColor() colorData() setColorData() application/x-color
以下是平台接口类
QInternalMimeData: 该类是一个接口类,由不同平台获取数据的类继承使用,其继承自QMimeData
QPlatformClipboard: 系统剪切板平台接口类,QClipboard数据设置和获取最终都是调用这个类
以下是wayland平台下和剪切板相关类:
QWaylandClipboard: wayland平台剪切板类,继承自QPlatformClipboard
QWaylandDataDevice: 该类是和窗口管理器交互的类,主要是通知和接受是否有剪切信息
QWaylandDataDeviceManager: 该类是管理QWaylandDataDevice的类,通过此类获取device对象
QWaylandDataSource: 该类是复制或剪切的源窗口需要构造的数据类
QWaylandDataOffer: 该类是粘贴的目的窗口需要构造的数据类
QWaylandMimeData: wayland平台真正接受剪切数据的类,继承自QInternalMimeData
总结了以上相关类的uml类图如下,可以更便于了解各个类之间的关系
三、wayland平台复制粘贴的内部实现
上面我们已经提前知道了,在wayland平台复制粘贴跨进程数据传递用的是匿名管道pipe,但这种通信是怎么进行的呢?带着上面的问题,再结合一下源代码详细梳理一下内部原理
首先我们通过调试代码知道,当我们在进程A中进行复制或剪切,会调用到代码:
void QClipboard::setMimeData(QMimeData* src, Mode mode)
{
QPlatformClipboard *clipboard = QGuiApplicationPrivate::platformIntegration()->clipboard();
if (!clipboard->supportsMode(mode)) {
if (src != nullptr) {
qDebug("Data set on unsupported clipboard mode. QMimeData object will be deleted.");
src->deleteLater();
}
} else {
clipboard->setMimeData(src,mode);
}
}
此时会调用的wayland平台下的类中,即QWaylandClipboard类:
void QWaylandClipboard::setMimeData(QMimeData *data, QClipboard::Mode mode)
{
auto *seat = mDisplay->currentInputDevice();
if (!seat) {
qCWarning(lcQpaWayland) << "Can't set clipboard contents with no wl_seats available";
return;
}
static const QString plain = QStringLiteral("text/plain");
static const QString utf8 = QStringLiteral("text/plain;charset=utf-8");
if (data && data->hasFormat(plain) && !data->hasFormat(utf8))
data->setData(utf8, data->data(plain));
switch (mode) {
case QClipboard::Clipboard:
if (auto *dataDevice = seat->dataDevice()) {
//构造QWaylandDataSource类,并设置给dataDevice
dataDevice->setSelectionSource(data ? new QWaylandDataSource(mDisplay->dndSelectionHandler(), data) : nullptr);
emitChanged(mode);
}
break;
.....
}
}
上面的关键函数为setSelectionSource,我们看下setSelectionSource的实现:
void QWaylandDataDevice::setSelectionSource(QWaylandDataSource *source)
{
if (source)
connect(source, &QWaylandDataSource::cancelled, this, &QWaylandDataDevice::selectionSourceCancelled);
// wayland协议接口,通知窗口管理器此时有复制的数据
set_selection(source ? source->object() : nullptr, m_inputDevice->serial());
m_selectionSource.reset(source);
}
此时我们可以看出,进程A最终只是跟窗口管理器进行了通信,并不是直接跟进程B进行交互,这里也是回答了一开始提出的那个问题。
那窗口管理器又是如果通知到进程B的呢?当然我们猜测也是通过wayland协议接口,具体是哪几个接口呢?
这里先简单介绍一下wayland相关的内容,我们应该知道在wayland平台中所有窗口都是由窗口管理器进行统一管理的,而窗口和窗口管理器之间的交互是基于wayland协议。前面我们介绍了三个和窗口管理器通信的类,而这三个类就是继承了wayland的通信协议并实现了协议中的接口:
QWaylandDataDevice 继承自 wl_data_device
QWaylandDataSource 继承自 wl_data_source
QWaylandDataOffer 继承自 wl_data_offer
从第二章节我们了解到,QWaylandDataSource和QWaylandDataOffer是真正保存数据的类,并且前者是源窗口保存数据的类,后者是目的窗口保存数据的类,而QWaylandDataDevice就是管理和构造以上两个类,并且主要负责和窗口管理器进行同步的类。
从上面的复制过程的代码中,我们看到了QWaylandDataSource构造的过程,那目的窗口中的QWaylandDataOffer是如何构造的还不清楚,通过查看wayland_debug的日志可以看到,当进程B窗口刚激活时会有如下信号:
结合以上信号继续调试代码可以发现,会调到qtwayland的如下接口:
void QWaylandDataDevice::data_device_data_offer(struct ::wl_data_offer *id)
{
// 这里创建QWaylandDataOffer对象
new QWaylandDataOffer(m_display, id);
}
void QWaylandDataOffer::data_offer_offer(const QString &mime_type)
{
// 同步format信息
m_mimeData->appendFormat(mime_type);
}
最后调用selection接口,在这里发送了一个信号给到上层客户端,客户端可以通过绑定dataChanged()信号来开始获取剪切板的数据,但真正的数据还没获取到
void QWaylandDataDevice::data_device_selection(wl_data_offer *id)
{
if (id)
// 这里将创建的QWaylandDataOffer对象给到dataDevice保管
m_selectionOffer.reset(static_cast<QWaylandDataOffer *>(wl_data_offer_get_user_data(id)));
else
m_selectionOffer.reset();
#if QT_CONFIG(clipboard)
QGuiApplicationPrivate::platformIntegration()->clipboard()->emitChanged(QClipboard::Clipboard);
#endif
}
以下画了个交互图,方面理解wayland交互的流程:
对于上图的交互流程需要特别注意两点:
- wayland平台给客户端同步剪切板信息,只会在客户端激活的情况才会通知,没有激活是不会通知的
- 以上只是激活目的窗口的流程,激活后就会收到窗管的信号,并收到了相关的format信息,但此时真正的复制或剪切的数据还没有发送到目的窗口中。
此时在进程B中做粘贴的操作,继续调试代码,发现调用mimeData()接口取数据:
const QMimeData* QClipboard::mimeData(Mode mode) const
{
QPlatformClipboard *clipboard = QGuiApplicationPrivate::platformIntegration()->clipboard();
if (!clipboard->supportsMode(mode)) return nullptr;
return clipboard->mimeData(mode);
}
查看对应wayland平台的类:
QMimeData *QWaylandClipboard::mimeData(QClipboard::Mode mode)
{
auto *seat = mDisplay->currentInputDevice();
if (!seat)
return &m_emptyData;
switch (mode) {
case QClipboard::Clipboard:
if (auto *dataDevice = seat->dataDevice()) {
// 如果在同一进程复制粘贴,那source不为空,直接取source里面的mimedata
if (auto *source = dataDevice->selectionSource()) {
return source->mimeData();
}
// 如果是跨进程粘贴,那source为空,offer不为空,就取offer里面的mimedata
if (auto *offer = dataDevice->selectionOffer()) {
return offer->mimeData();
}
}
return &m_emptyData;
...
}
}
从上面的代码中就可以看出,两种不同场景数据获取的来源。
获取了mimedata之后,需要取出里面的数据,例如text(),那看看怎么取text数据
QString QMimeData::text() const
{
Q_D(const QMimeData);
QVariant utf8Text = d->retrieveTypedData(textPlainUtf8Literal(), QMetaType::QString);
if (!utf8Text.isNull())
return utf8Text.toString();
QVariant data = d->retrieveTypedData(textPlainLiteral(), QMetaType::QString);
return data.toString();
}
QVariant QMimeDataPrivate::retrieveTypedData(const QString &format, QMetaType::Type type) const
{
Q_Q(const QMimeData);
QVariant data = q->retrieveData(format, QVariant::Type(type));
...
}
这里看出,retrieveData接口是虚函数,就要看看子类的实现:
QVariant QInternalMimeData::retrieveData(const QString &mimeType, QVariant::Type type) const
{
QVariant data = retrieveData_sys(mimeType, type);
// 以下是对不同数据类型进行解析
...
return data;
}
接着看retrieveData_sys的实现:
QVariant QWaylandMimeData::retrieveData_sys(const QString &mimeType, QVariant::Type type) const
{
Q_UNUSED(type);
if (m_data.contains(mimeType))
return m_data.value(mimeType);
QString mime = mimeType;
if (!m_types.contains(mimeType)) {
if (mimeType == QStringLiteral("text/plain") && m_types.contains(utf8Text()))
mime = utf8Text();
else
return QVariant();
}
// 创建管道,pipefd[0]表示r端口,pipefd[1]表示w端口
int pipefd[2];
if (qt_safe_pipe(pipefd) == -1) {
qWarning("QWaylandMimeData: pipe2() failed");
return QVariant();
}
// 这里是发消息给窗管,通知窗管进程B准备接受数据,窗管会通知进程A写数据
m_dataOffer->startReceiving(mime, pipefd[1]);
close(pipefd[1]);
QByteArray content;
// 从管道中读数据
if (readData(pipefd[0], content) != 0) {
qWarning("QWaylandDataOffer: error reading data for mimeType %s", qPrintable(mimeType));
content = QByteArray();
}
close(pipefd[0]);
m_data.insert(mimeType, content);
return content;
}
看到这里已经可以看出真相了,这里是开了一个管道,通过startReceiving接口通知窗管,再由窗管通知进程A写数据,然后在进程B中通过readData函数读取。
而进程A中写管道的代码如下:
void QWaylandDataSource::data_source_send(const QString &mime_type, int32_t fd)
{
QByteArray content = QWaylandMimeHelper::getByteArray(m_mime_data, mime_type);
if (!content.isEmpty()) {
// Create a sigpipe handler that does nothing, or clients may be forced to terminate
// if the pipe is closed in the other end.
struct sigaction action, oldAction;
action.sa_handler = SIG_IGN;
sigemptyset (&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGPIPE, &action, &oldAction);
write(fd, content.constData(), content.size());
sigaction(SIGPIPE, &oldAction, nullptr);
}
close(fd);
}
到此,关于wayland平台中复制粘贴的过程基本都和清楚了。以上的几个问题,也都得到了解答
总结一下:
-
同一个进程的复制粘贴数据就保存在自己的内存中,直接设置和获取;
-
不同进程之间复制粘贴的交互是通过窗口管理器进行间接交互的,数据是通过管道传输的。
四、目前wayland平台复制粘贴存在的问题
问题一:当在进程A中复制后,如果关闭了进程A,再在进程B中粘贴,发现没有数据?
原因:是因为wayland平台的数据传输是通过管道传输的,如果管道的一端关闭了,数据就传输不成功,如果要解决该问题,可能要修改wayland平台下的数据传输的方式,例如可以改成消息队列
问题二:大数据量(>70M)的复制和拷贝文本,在此过程中如果不停做点击鼠标和移动鼠标等动作,最后会导致进程崩溃?
原因:该问题的是flushrequest的问题,最终会打印:“The wayland connectting broken, …”,这个在qt官网有类似问题,具体是因为拷贝复制过程中数据量过大,导致主线程卡死,此时不停点击或移动鼠标会使socket数据不停积累最后溢出,导致wayland连接管道破裂。该问题暂时没找到好解决方案。