log4qt,是大名鼎鼎的阿帕奇的java日志库log4j的qt移植版。本是挺常用的开源库,然而在使用过程中发现了内存泄露的坑。为了验证下,这里单独写了个测试demo,并使用qtcreator集成的hoeb内存泄露检测工具分析下。
测试用例很简单,就是一个MainWindow界面上放置两个按钮。点下按钮分别启动一个线程,间隔10ms不断的向日志文件里写日志。
测试用例
测试用例如下:
void MainWindow::on_pushButton_clicked() { ui->pushButton->setEnabled(false); QFuture<void> future = QtConcurrent::run([&]() { while(1) { QMutex mutex; QMutexLocker locker(&mutex); logger->info("&&&&&on_pushButton_clicked&&&&&&&",__FILE__,__FUNCTION__,QString::number(__LINE__)); QThread::msleep(10); } }); } void MainWindow::on_pushButton_2_clicked() { ui->pushButton_2->setEnabled(false); QFuture<void> future = QtConcurrent::run([&]() { while(1) { QMutex mutex; QMutexLocker locker(&mutex); logger->info("&&&&&on_pushButton_2_clicked&&&&&&&",__FILE__,__FUNCTION__,QString::number(__LINE__)); QMetaObject::invokeMethod(qApp, [this]{ ui->tb->setText("count:"); }); QThread::msleep(10); } }); }
测试结果
测试运行了一个小时,内存竟占用到惊人的四百多兆,结果如下:
为此,我还在github上提交了一个isuse,期待作者和其他开源爱好者们的回复。
测试源码目录结构
我的测试代码目录结构如下,把log4qt的源码整个复制过来,单独的log4qt文件夹内。
先说下测试环境,使用qt5.10.0的32位msvc工具链 和qt5.12.11的64位msvc工具链测试,结果一样,同样存在泄露。
使用的log4qt的版本是1.5.0
log4qt的github地址:GitHub - MEONMedical/Log4Qt: Log4Qt - Logging for the Qt cross-platform application framework
以下是我的log4qt_test.pro文件内容:
其中的build.pri和g++.pri文件,在log4qt的master分支里有。
#------------------------------------------------- # # Project created by QtCreator 2022-07-26T16:57:19 # #------------------------------------------------- QT += core gui concurrent greaterThan(QT_MAJOR_VERSION, 4): QT += widgets CONFIG += c++11 DESTDIR = $$PWD/./bin TARGET = log4qt_test #TEMPLATE = app # The following define makes your compiler emit warnings if you use # any feature of Qt which has been marked as deprecated (the exact warnings # depend on your compiler). Please consult the documentation of the # deprecated API in order to know how to port your code away from it. DEFINES += QT_DEPRECATED_WARNINGS # You can also make your code fail to compile if you use deprecated APIs. # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 DEFINES +=LOG4QT_STATIC LOG4QTSRCPATH = $$PWD/log4qt INCLUDEPATH += -L $$LOG4QTSRCPATH \ $$LOG4QTSRCPATH/helpers \ $$LOG4QTSRCPATH/spi \ $$LOG4QTSRCPATH/varia DEPENDPATH += $$LOG4QTSRCPATH \ $$LOG4QTSRCPATH/helpers \ $$LOG4QTSRCPATH/spi \ $$LOG4QTSRCPATH/varia include($$PWD/log4qt/log4qt.pri) include($$PWD/log4qt/build.pri) include($$PWD/log4qt/g++.pri) include(logger/logger.pri) SOURCES += \ main.cpp \ mainwindow.cpp HEADERS += \ mainwindow.h FORMS += \ mainwindow.ui # Default rules for deployment. qnx: target.path = /tmp/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target LIBS += -L$$PWD/./bin
其中的logger.pri,仅是对log4qt的一个简单封装:
HEADERS += $$PWD/include/mylogger.h \ $$PWD/include/logqt.h \ $$PWD/include/operation_log.h SOURCES += \ $$PWD/src/logqt.cpp \ $$PWD/src/mylogger.cpp INCLUDEPATH += $$PWD
#include "../include/mylogger.h" #include "../include/logqt.h" #include <QJsonObject> #include <QJsonValue> #include <QJsonParseError> #include <QJsonDocument> #include <QDir> #include <QFile> MyLogger* MyLogger::logger_ = new MyLogger; MyLogger *MyLogger::getInstance() { return logger_; } bool MyLogger::setConfPath(const QString &path) { return log4qt_->setConfPath(path); } MyLogger::MyLogger(QObject *parent):QObject(parent),log4qt_(new LogQt),machine_id("Undefined") { QString conf_path = QDir::currentPath()+"/../config/machine_info.json"; QFile loadFile(conf_path); if(!loadFile.open(QIODevice::ReadOnly)) { // log4qt_->error("load id config file failed"); return; } else { // log4qt_->info("load id config file success"); } QByteArray allData = loadFile.readAll();//将文件内容放入allData对象 loadFile.close();//使用(读取)完成,结束占用 QJsonParseError json_error;//解析期间报告错误 QJsonDocument jsonDoc(QJsonDocument::fromJson(allData, &json_error));//新建一个json文档对象 if(json_error.error != QJsonParseError::NoError)//如果解析成功,则该分支不会进入 { // log4qt_->error("json format error: "+json_error.errorString()); return; } QJsonObject obj = jsonDoc.object(); if(obj.contains("machine_id")&&obj["machine_id"].isString()) machine_id=obj["machine_id"].toString(); } MyLogger::~MyLogger() { if (log4qt_) { log4qt_->deleteLater(); log4qt_ = NULL; } } void MyLogger::info(const QString& data,const QString& file,const QString& func,const QString& line) { log4qt_->info(QString("%1 file: %2 func: %3 line: %4").arg(data).arg(file).arg(func).arg(line)); } void MyLogger::debug(const QString& data,const QString& file,const QString& func,const QString& line) { log4qt_->debug(QString("%1 file: %2 func: %3 line: %4").arg(data).arg(file).arg(func).arg(line)); } void MyLogger::warn(const QString& data,const QString& file,const QString& func,const QString& line) { log4qt_->warn(QString("%1 file: %2 func: %3 line: %4").arg(data).arg(file).arg(func).arg(line)); } void MyLogger::error(const QString& data,const QString& file,const QString& func,const QString& line) { log4qt_->error(QString("%1 file: %2 func: %3 line: %4").arg(data).arg(file).arg(func).arg(line)); } QString get_data_time(); void MyLogger::operation_log(QString &business, QString &data, QString &user, business_state state, QString &level) { QJsonObject data_obj; data_obj.insert("type","operation"); data_obj.insert("time",get_data_time()); data_obj.insert("level",level); data_obj.insert("user",user); data_obj.insert("business",business); QString str_business_state; if(state==business_state::start) str_business_state="start"; else if(state==business_state::finsish) str_business_state="finish"; else str_business_state="running"; data_obj.insert("state",str_business_state); data_obj.insert("data",data); QJsonObject root_obj; root_obj.insert("type","operationlog"); root_obj.insert("data",QJsonValue(data_obj)); root_obj.insert("id",machine_id); log4qt_->info("*#*#"+QString(QJsonDocument(root_obj).toJson(QJsonDocument::Compact))+"#*#*"); } void MyLogger::operation_log(QJsonObject &operation_obj) { operation_obj.insert("type","operation"); operation_obj.insert("time",get_data_time()); QJsonObject root_obj; root_obj.insert("type","operationlog"); root_obj.insert("data",QJsonValue(operation_obj)); root_obj.insert("id",machine_id); log4qt_->info("*#*#"+QString(QJsonDocument(root_obj).toJson(QJsonDocument::Compact))+"#*#*"); } void MyLogger::operation_log(QString &operation_str) { } void MyLogger::data_change_log(QString &key, QString &value) { QJsonObject data_obj; data_obj.insert("type","data"); data_obj.insert("time",get_data_time()); data_obj.insert("key",key); data_obj.insert("value",value); QJsonObject root_obj; root_obj.insert("type","operationlog"); root_obj.insert("data",QJsonValue(data_obj)); root_obj.insert("id",machine_id); log4qt_->info("*#*#"+QString(QJsonDocument(root_obj).toJson(QJsonDocument::Compact))+"#*#*"); } void MyLogger::exception_log(QString &code, QString &desc) { QJsonObject data_obj; data_obj.insert("type","exception"); data_obj.insert("code",code); data_obj.insert("resion",desc); QJsonObject root_obj; root_obj.insert("type","operationlog"); root_obj.insert("data",QJsonValue(data_obj)); root_obj.insert("id",machine_id); log4qt_->info("*#*#"+QString(QJsonDocument(root_obj).toJson(QJsonDocument::Compact))+"#*#*"); } void MyLogger::shutdown() { log4qt_->shutdown(); } #include <QDateTime> QString get_data_time() { return QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss:zzz"); }
#ifndef LOGGER_H #define LOGGER_H #include <QObject> #include "logger_global.h" enum business_state { start, running, finsish }; class QJsonObject; class LogQt; /** * @class Logger logger.h * @brief 日志客户端/服务端 * @note 根据日志配置文件中配置启动为客户端或服务端 */ class LOGGERSHARED_EXPORT MyLogger:public QObject { Q_OBJECT private: MyLogger(QObject *parent = NULL); ~MyLogger(); public: Q_DISABLE_COPY(MyLogger) static MyLogger* getInstance(); /** * @brief 设置配置文件路径 * @param[in] path 配置文件路径 * @return * -true 成功 * -false 失败 * @retval bool */ bool setConfPath(const QString& path); public: /** * @brief * @param[in] data 日志 * @param[in] table 表名称 * @param[in] id * @note * -systemlog 系统运行表 * -devlog 用户操作表 */ Q_INVOKABLE void info(const QString& data,const QString& file = QString(),const QString& func = QString(),const QString& line = QString()); /** * @brief * @param[in] data 日志 * @param[in] table 表名称 * @param[in] id * @note * -systemlog 系统运行表 * -devlog 用户操作表 */ Q_INVOKABLE void debug(const QString& data,const QString& file = QString(),const QString& func = QString(),const QString& line = QString()); /** * @brief * @param[in] data 日志 * @param[in] table 表名称 * @param[in] id * @note * -systemlog 系统运行表 * -devlog 用户操作表 */ Q_INVOKABLE void warn(const QString& data,const QString& file = QString(),const QString& func = QString(),const QString& line = QString()); /** * @brief * @param[in] data 日志 * @param[in] table 表名称 * @param[in] id * @note * -systemlog 系统运行表 * -devlog 用户操作表 */ Q_INVOKABLE void error(const QString& data,const QString& file = QString(),const QString& func = QString(),const QString& line = QString()); /** * @brief 记录操作日志的接口 * @param business 业务名称 * @param data 最终显示的字符串 * @param user 当前机器的操作员 * @param level 日志的等级,默认是1 * @param state 只有存在多次交互的业务需要这个字段,不需要多次交互的可以不填 */ Q_INVOKABLE void operation_log(QString& business,QString& data,QString& user,business_state state=running,QString& level=QString("1")); /** * @brief 依旧是操作日志,供c++直接调用。 * @param operation_obj 日志的json对象 */ void operation_log(QJsonObject& operation_obj); /** * @brief 操作日志,供界面调用。传递json的字符串 * @param operation_str 日志的json对象的字符串 */ Q_INVOKABLE void operation_log(QString& operation_str); /** * @brief 数据变更操作的接口,这里的数据仅限于用户需要知道的参数,不是每一个key的变更都要追踪 * @param key 数据库中定义的有的变量key的名字使用与数据库一致的,数据库未定义的,写文档里面 * @param value 变更后的值 */ Q_INVOKABLE void data_change_log(QString& key,QString& value); /** * @brief 异常的日志,这个异常的日志时给用户看的,不能随便打 * @param code 异常代码 * @param desc 异常发生时的代码 */ Q_INVOKABLE void exception_log(QString& code,QString& desc); /** * @brief 关闭日志,刷新buffer * @note 调用此方法后日志关闭,必须重新初始化 */ Q_INVOKABLE void shutdown(); private: static MyLogger* logger_; LogQt* log4qt_; QString machine_id; }; #define logger_debug(msg) MyLogger::getInstance()->debug(msg,__FILE__,__FUNCTION__,QString::number(__LINE__)) #define logger_info(msg) MyLogger::getInstance()->info(msg,__FILE__,__FUNCTION__,QString::number(__LINE__)) #define logger_warn(msg) MyLogger::getInstance()->warn(msg,__FILE__,__FUNCTION__,QString::number(__LINE__)) #define logger_error(msg) MyLogger::getInstance()->error(msg,__FILE__,__FUNCTION__,QString::number(__LINE__)) #endif // LOGGER_H
#include "../include/Logqt.h" #include "log4qt/loggerrepository.h" #define LINE_MAX 1024 LogQt::LogQt(QObject *parent) : QObject(parent) { } bool LogQt::setConfPath(const QString &path) { return Log4Qt::PropertyConfigurator::configure(path); } LogQt::~LogQt() { } void LogQt::info(const QString& data) { if(data.size()<LINE_MAX) { logger()->info(data); return; } int size = data.size(); logger()->info(QString("**********line start***%1**********").arg(size)); int i=0; for(;i<size - LINE_MAX;i +=LINE_MAX) { logger()->info(data.mid(i,LINE_MAX).append("\\")); } logger()->info(data.mid(i,LINE_MAX)); logger()->info("**********line finish*************"); } void LogQt::debug(const QString& data) { if(data.size()<LINE_MAX) { logger()->debug(data); return; } int size = data.size(); logger()->debug(QString("**********line start***%1**********").arg(size)); int i=0; for(;i<size - LINE_MAX;i +=LINE_MAX) { logger()->debug(data.mid(i,LINE_MAX).append("\\")); } logger()->debug(data.mid(i,LINE_MAX)); logger()->debug("**********line finish*************"); } void LogQt::warn(const QString& data) { if(data.size()<LINE_MAX) { logger()->warn(data); return; } int size = data.size(); logger()->warn(QString("**********line start***%1**********").arg(size)); int i=0; for(;i<size - LINE_MAX;i +=LINE_MAX) { logger()->warn(data.mid(i,LINE_MAX).append("\\")); } logger()->warn(data.mid(i,LINE_MAX)); logger()->warn("**********line finish*************"); } void LogQt::error(const QString& data) { if(data.size()<LINE_MAX) { logger()->error(data); return; } int size = data.size(); logger()->error(QString("**********line start***%1**********").arg(size)); int i=0; for(;i < size - LINE_MAX;i +=LINE_MAX) { logger()->error(data.mid(i,LINE_MAX).append("\\")); } logger()->error(data.mid(i,LINE_MAX)); logger()->error("**********line finish*************"); } void LogQt::shutdown() { logger()->loggerRepository()->shutdown(); }
#ifndef LOGQT_H #define LOGQT_H #include <QObject> #include "log4qt/logger.h" #include "log4qt/propertyconfigurator.h" class LogQt : public QObject { Q_OBJECT LOG4QT_DECLARE_QCLASS_LOGGER public: explicit LogQt(QObject *parent = nullptr); ~LogQt(); /** * @brief 设置配置文件路径 * @param[in] path 配置文件路径 * @return * -true 成功 * -false 失败 * @retval bool */ bool setConfPath(const QString& path); /** * @brief * @param[in] data 日志 * @param[in] table 表名称 * @param[in] id * @note * -systemlog 系统运行表 * -devlog 用户操作表 */ void info(const QString& data); /** * @brief * @param[in] data 日志 * @param[in] table 表名称 * @param[in] id * @note * -systemlog 系统运行表 * -devlog 用户操作表 */ void debug(const QString& data); /** * @brief * @param[in] data 日志 * @param[in] table 表名称 * @param[in] id * @note * -systemlog 系统运行表 * -devlog 用户操作表 */ void warn(const QString& data); /** * @brief * @param[in] data 日志 * @param[in] table 表名称 * @param[in] id * @note * -systemlog 系统运行表 * -devlog 用户操作表 */ void error(const QString& data); void shutdown(); signals: public slots: }; #endif // LOG4QT_H
日志配置
log4j.rootLogger=DEBUG,daily,console log4j.appender.logfile=org.apache.log4j.FileAppender log4j.appender.logfile.layout=org.apache.log4j.PatternLayout log4j.appender.logfile.layout.ConversionPattern=%-d [%t] %-5p: %m%n log4j.appender.logfile.File=./log/uiTest.log log4j.appender.logfile.ImmediateFlush=FALSE log4j.appender.logfile.Threshold=DEBUG log4j.appender.logfile.AppendFile=TRUE log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=%-d [%t] %-5p: %m%n #设置一个每日储存一个log文件的记录器 log4j.appender.daily=org.apache.log4j.DailyFileAppender log4j.appender.daily.file=./log/uiTest.log log4j.appender.daily.appendFile=true log4j.appender.daily.datePattern=_yyyy_MM_dd #log4j.appender.daily.keepDays=90 log4j.appender.daily.layout=${log4j.appender.console.layout} #log4j.appender.daily.layout.dateFormat=${log4j.appender.console.layout.dateFormat} log4j.appender.daily.layout.ConversionPattern=%-d [%t] %-5p: %m%n #log4j.appender.daily.layout.contextPrinting=${log4j.appender.console.layout.contextPrinting}
heob内存泄露工具分析
https://doc.qt.io/qtcreator/creator-heob.html
heob-堆观察器,qtcreator的4.6以后的版本集成了它的插件。 heob覆盖被调用进程的堆函数,以检测缓冲区溢出和内存泄漏。 在缓冲区溢出时,将引发访问冲突,并提供有问题的指令和缓冲区分配的堆栈跟踪。但heob.exe还是需要单独下载的。可以从github上下载生成好的heob.exe工具。
github:GitHub - ssbssa/heob: Detects buffer overruns and memory leaks.
下载地址:heob
转换QT为VisualStudio工程
有时候使用visualStudio工程打开项目,调试更方便好用些。
可以通过一个插件一键转换qt的pro工程为vs的工程。使用qt-vsaddin-msvc2015-2.2.0.vsix插件。在vs中打开qt项目报错,可能是需要执行以下转换:qmake -tp vc
插件镜像下载地址:
http://mirrors.tuna.tsinghua.edu.cn/qt/archive/vsaddin/2.2.0/qt-vsaddin-msvc2015-2.2.0.vsix
使用vs启动程,点击工具栏中的:调试,选择:“显示诊断工具”,profiler,选择memory usage.
结论
log4qt名声是挺大,开源的是个好东西,但是不代表它就没问题。还是要多做测试,尤其是多做压力情况下的测试,否则可能根本看不出来有问题。QT是好用,但是它的半自动化的内存托管方式是把双刃剑,平常你的new都很小心的对内存操作,记得释放。但是用了qt且习惯了它,它容易让你养成坏习惯。