qt-wayland平台下复制粘贴原理

一、复制粘贴的简单原理

复制粘贴大概可以分为以下两种场景:

  1. 在同一个进程中复制并粘贴
  2. 在进程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类型数据的信息

以下是平台接口类

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交互的流程:
在这里插入图片描述

对于上图的交互流程需要特别注意两点:

  1. wayland平台给客户端同步剪切板信息,只会在客户端激活的情况才会通知,没有激活是不会通知的
  2. 以上只是激活目的窗口的流程,激活后就会收到窗管的信号,并收到了相关的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连接管道破裂。该问题暂时没找到好解决方案。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值