1.主界面卡死的几种情况及处理办法
1.1 页面未响应的本质
qtui框架是基于事件循环的,一般,不建议在ui线程中处理比较复杂的耗时操作,局部长时间循环操作,批量下载操作,如果页面中这些耗时操作的逻辑处理不好时,会导致ui短暂性卡死,对于由于上述操作引起的卡其,可以处理的办法有以下几种
2.1逻辑一:在ui所在线程内,进行大批量的循环操作
例如,如下代码
auto process = [] (){
for(int i = 0; i < 100000000;i++){
//do somethings you want to do
}
}
假设将上述逻辑与一个按钮事件绑定,当点击按钮时,之后的短时间内会造成页面的卡死,ui无法响应各种事件,只能等到for循环执行完成之后才可以响应页面事件。
2.2对于上述代码,做如下修改,可以避免页面假死
connect(ui->btn,&QPushButton::clicked,this,[=](){
for(int i = 0; i < 10000000000;i++)
{
//do somethings you want to do
QCoreApplication::processEvents();
}
});
加入QCoreApplication::processEvents();根据官方文档解释:
调用此函数只处理调用线程的事件,并在所有可用事件处理完毕后返回。可用事件是在函数调用之前排队的事件。这意味着在函数运行时发布的事件将被放入队列,直到下一轮的事件处理,该函数可以以手动的方式处理事件队列中的事件
2.3逻辑二:弱网环境下,需要下载一张很大的图片
假设耗时操作不是一个循环内,而是存在这样一种情况,在一个弱网环境下,需要下载一张很大的图片的时候,假设该下载动作是在ui线程中执行的时候,应该怎么办?使用QtConcurrent::run()
使用QtConCurrent,这个类是将一个函数放到新的线程里来执行再加上QFuture<T> 这个类,可以控制这个新的线程的函数开始,控制,结束等操作,具体方法如下:
static bool moreTimeFunction(){
bool t = downLoadBigPicture(); //假设该函数很耗时,图片很大,网速很差
}
auto okayOperation= [](){
QFuture<bool> future = QtConcurrent::run(downLoadBigPicture);
while(!future.isFinished()){
QApplication::processEvents(QEventLoop::AllEvents.100)
}
}
connection(ui->btn,QPushButton::clicked,this,&ClassName::okayOperation);
QFutuwre+QtConCurrent框架非常强大,可以将线程同步,异步的状态抽象出来,QtConCurrent是Qt封装的一个线程池,使用时需要在pro文件中配置,再加该命名空间便可以使用。
2.4使用QThread另起线程
通常处理ui卡死最常用的办法,就是将耗时操作放进线程中执行,等待线程执行完毕后发信号出来通知,当前耗时操作已经被执行完,允许进入下一环节。
Qt线程创建的方式有两种,一种是继承至QThread,之后重写run()方法来实现线程的创建,另一种是继承至QObject,通过moveToThread的方式创建,官方推荐使用后者,两者的区别是:
moveToThread 方法,是把我们需要的工作全部封装在一个类中,将每个任务定义为一个槽函数,再建立触发这些槽函数的信号,然后连接信号和槽,最后调用 moveToThread 方法将这个类交给一个 QThread 对象,再调用 QThread 的 start() 函数使其全权处理事件循环。于是,任何时候我们需要让子线程执行某个任务,只需要发出对应的信号就可以。
其优点是我们可以在一个worker类中定义很多个需要做的工作,然后触发信号,子线程就可以执行。相比于继承 QThread 方法,只能执行 run() 函数中的任务,moveToThread 的方法中一个线程可以做很多不同的工作,只要实现对应的槽函数,触发对应的信号即可,针对第二种方法,有如下代码
#ifndef DEL_WORKER_H
#define DEL_WORKER_H
#include <QObject>
#include <QDebug>
#include <QThread>
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr){
}
public slots:
void doWork(int parameter); // doWork 定义了线程要执行的操作
{
qDebug() << "receive the execute signal" ;
qDebug() << "\tCurrent thread ID: " << QThread::currentThreadId();
// 循环一百万次
for (int i = 0; i != 1000000; ++i)
{
++parameter ;
}
// 发送结束信号
qDebug() << "\tFinish the work and sent the result Ready signal\n" ;
emit resultReady(parameter);
}
signals:
void resultReady(const int result); // 线程完成工作时发送的信号
};
#endif //DEL_WORKER_H
2.5.使用moveToThread开启线程
//
// Controller.h
//
#ifndef DEL_CONTROLLER_H
#define DEL_CONTROLLER_H
#include <QObject>
#include <QThread>
#include <QDebug>
#include "Worker.h"
// controller 用于 启动子线程 和 处理子线程执行的结果
class Controller : public QObject
{
Q_OBJECT
QThread workerThread ;
public:
explicit Controller(QObject *parent = nullptr){
auto *worker = new Worker ;
// 调用 moveToThread 将该任务交给 workThread
worker->moveToThread(&workerThread);
// operate 信号发射后启动线程工作
connect(this, SIGNAL(operate(const int)), worker, SLOT(doWork(int)));
// 该线程结束时销毁
connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
// 线程结束后发送信号,对结果进行处理
connect(worker, SIGNAL(resultReady(int)), this, SLOT(handleResults(int)));
// 启动线程
workerThread.start();
// 发射信号,开始执行
qDebug() << "emit the signal to execute!" ;
qDebug() << "\tCurrent thread ID:" << QThread::currentThreadId() << '\n' ;
emit operate(0);
}
~Controller(){
workerThread.quit();
workerThread.wait();
};
public slots:
static void handleResults(int result); // 处理子线程执行的结果
{
qDebug() << "receive the resultReady signal" ;
qDebug() << "\tCurrent thread ID: " << QThread::currentThreadId() << '\n' ;
qDebug() << "\tThe last result is: " << result ;
}
signals:
void operate(const int); // 发送信号,触发线程
};
#endif //DEL_CONTROLLER_H
3.小知识
1.在主线程退出之前,析构函数中总是需要先停掉子线程,这是比较合理的操作,否则系统报错误:主线程先于子线程退出
2.QThread中的静态方法currentThreadId()可以返回当前线程句柄
3.QtConCurrent的线程是线程安全型的