Qt 程序单例运行

前言

随着某手机评测软件(beng huai 3rd)版本更新,我这16年的手机已经是卡得怀疑人生了,这时当初安利我玩这游戏的大学死宅室友说官方出桌面版了,就赶紧下载试试。当安装完成后发现这程序居然是用Qt写的,这就触及到我的敏感部位了,赶紧用IDA打开学习一下。

打开程序往下面翻了一下,在里面发现了一个字段QtSingleApplication,从这个命名就可以看出是用来运行单例程序的,也就是防止程序多开。因为平常写Qt程序比较少,之前也只是看过用QSharedMemory方式来实现这个功能,这里正好学习一下。话不多说,我们先来看看这个功能怎么使用吧。

正文

程序下载

QtSingleApplication并没有包含在发布的Qt库中,可以通过git下载:

    git clone git://code.qt.io/qt-solutions/qt-solutions.git

官网文档链接为:https://doc.qt.io/archives/qtextended4.4/qtopiadesktop/qtsingleapplication.html

运用与解析

从仓库克隆到本地过后,直接进入qtsingleapplication/src目录下面,看了一眼,就直接打开qtsingleapplication.cpp文件,随即在源文件的注释中就有写到使用的方法:

// Original
int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    MyMainWidget mmw;
    mmw.show();
    return app.exec();
}

// Single instance
int main(int argc, char **argv)
{
    QtSingleApplication app(argc, argv);

    if (app.isRunning())
        return !app.sendMessage(someDataString);

    MyMainWidget mmw;
    app.setActivationWindow(&mmw);
    mmw.show();
    return app.exec();
}

简单的来说就是通过QtSingleApplication替换QApplication来执行我们的Qt 程序,并且通过isRunning()方法判断是否程序实例正在运行。如果当前程序已经有实例运行,在发送消息后退出,用起来很简单嘛。如果我们需要建立的是一个非gui程序,可以使用QtSingleCoreApplication类,或者传入gui使能参数到QtSingleApplication(Qt5以上),后面按QtSingleApplication来讲解一下。

在源文件中发现所有的构造函数就只实现了两个步骤:

1. 传递参数给基类的构造函数

2. 调用sysInit()方法

其中sysInit()方法中初始化了成员变量actWin和peer,并建立了peer信号和本身messageReceived()的信号槽。其中actWin是ActivationWindow的缩写,用户可以通过setActivationWindow()方法,设置一个Widget,当接收到peer信号时,就将该Widget显示出来:

/* 系统初始化函数 */
void QtSingleApplication::sysInit(const QString &appId)
{
    actWin = 0; // 初始化actWin对象指针为空

    /* 初始化peer,并将收到的messageReceived信号直接转发出去 */
    peer = new QtLocalPeer(this, appId); 
    connect(peer, SIGNAL(messageReceived(const QString&)), SIGNAL(messageReceived(const QString&)));
}

/* 设置激活窗口,通常设置为主窗口 */
void QtSingleApplication::setActivationWindow(QWidget* aw, bool activateOnMessage)
{
    actWin = aw;
    if (activateOnMessage)
        connect(peer, SIGNAL(messageReceived(const QString&)), this, SLOT(activateWindow()));
    else
        disconnect(peer, SIGNAL(messageReceived(const QString&)), this, SLOT(activateWindow()));
}

那么实现单例功能最主要就是peer这个变量了,所以我们接着打开同目录下的qtlocalpeer.cpp文件看看:

QtLocalPeer::QtLocalPeer(QObject* parent, const QString &appId)
    : QObject(parent), id(appId)
{
    /* 通过传入的appId来生成socketname。 
     * 如果传入的appId为空,则取程序程序名。
     */
    QString prefix = id;
    if (id.isEmpty()) {
        /* 取程序绝对路径 */
        id = QCoreApplication::applicationFilePath();
#if defined(Q_OS_WIN)
        id = id.toLower();
#endif
        /* 取程序名,这里的section(QLatin1Char('/'), -1)就是取
         * '/'分割的最后一个字符串,即程序名
         */
        prefix = id.section(QLatin1Char('/'), -1);
    }
    /* 只取6个字母字符 */
    prefix.remove(QRegExp("[^a-zA-Z]"));
    prefix.truncate(6);

    /* 计算校验和,并按qtsingleapp-appName-checkSum格式组成socketName */
    QByteArray idc = id.toUtf8();
    quint16 idNum = qChecksum(idc.constData(), idc.size());
    socketName = QLatin1String("qtsingleapp-") + prefix
                 + QLatin1Char('-') + QString::number(idNum, 16);

    /* 将进程ID添加到socketName后面 */
#if defined(Q_OS_WIN)
    if (!pProcessIdToSessionId) {
        QLibrary lib("kernel32");
        pProcessIdToSessionId = (PProcessIdToSessionId)lib.resolve("ProcessIdToSessionId");
    }
    if (pProcessIdToSessionId) {
        DWORD sessionId = 0;
        pProcessIdToSessionId(GetCurrentProcessId(), &sessionId);
        socketName += QLatin1Char('-') + QString::number(sessionId, 16);
    }
#else
    socketName += QLatin1Char('-') + QString::number(::getuid(), 16);
#endif

    /* 实例化QLocalServer类,并根据socketName创建lock文件 */
    server = new QLocalServer(this);
    QString lockName = QDir(QDir::tempPath()).absolutePath()
                       + QLatin1Char('/') + socketName
                       + QLatin1String("-lockfile");
    lockFile.setFileName(lockName);
    lockFile.open(QIODevice::ReadWrite);
}

看到这里差不多就明白了QtSingleApplication的实现原理:

1. 单例程序是通过文件加锁的方式实现的;

2. 程序之间的通信,即sendMessage()方法是通过QLocalSocket实现的。

通过源文件中isClient()方法肯定了这一点:

/* 判断当前进程是否是QLocakSocket客户端 */
bool QtLocalPeer::isClient()
{
    /* 如果文件被当前进程上了锁,返回false,说明给文件加锁的进程为服务器进程 */
    if (lockFile.isLocked())
        return false;

    /* 如果文件未加锁,则进行加锁操作,如果失败,说明文件被其他进程加锁, 
     * 当前进程为重复打开的程序实例。
     */
    if (!lockFile.lock(QtLP_Private::QtLockedFile::WriteLock, false))
        return true;

    /* 如果加锁成功,则创建QLocalServer,用于通信 */
    bool res = server->listen(socketName);
#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(4,5,0))
    // ### Workaround
    if (!res && server->serverError() == QAbstractSocket::AddressInUseError) {
        QFile::remove(QDir::cleanPath(QDir::tempPath())+QLatin1Char('/')+socketName);
        res = server->listen(socketName);
    }
#endif
    if (!res)
        qWarning("QtSingleCoreApplication: listen on local socket failed, %s", qPrintable(server->errorString()));
    QObject::connect(server, SIGNAL(newConnection()), SLOT(receiveConnection()));
    return false;
}

但是在一开始我们贴上来的Qt实例中,并没有调用到isClient()方法,而是isRunning()方法。同时在qtsingleapplication.h头文件中实现的initialize()方法又调用了isRunning()方法,所以在使用QtSingleApplication时,我们必须在代码中使用initialize()或者isRunning()。

总结

遇到感兴趣的东西,打开“参考借鉴”一下,可以学习到自己平常很少接触到的知识。后续如果看到平常没有接触到的东西,也会跟大家分享出来。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值