Qt Android 崩溃追踪:原生信号捕获与本地化日志方案
核心机制
Android 端的 Qt 应用常面临线上环境崩溃难以复现的困境。本文基于 Qt 5.15.2 for Android,利用 POSIX 信号量机制实现一套零依赖的崩溃信息采集方案。当进程触发段错误、非法指令等异常时,系统自动回调注册的处理函数,完成堆栈回溯与日志持久化。
关键信号说明:
SIGSEGV:内存段违规访问(典型如空指针解引用)SIGABRT:调用 abort() 主动终止SIGFPE:算术运算异常(除零、浮点溢出)SIGILL:处理器无法识别的指令
工程配置
修改 .pro 文件,确保包含 Android 扩展模块:
QT += core gui widgets androidextras
CONFIG += release c++11
DEFINES += QT_NO_DEBUG_OUTPUT
# 精简目标架构,缩短构建时间
ANDROID_ABIS = arm64-v8a armeabi-v7a
SOURCES += main.cpp \
mainwindow.cpp \
exceptionhandler.cpp
HEADERS += mainwindow.h \
exceptionhandler.h异常处理器实现
头文件 exceptionhandler.h
#ifndef EXCEPTIONHANDLER_H
#define EXCEPTIONHANDLER_H
#include <QObject>
#include <QString>
class ExceptionHandler : public QObject
{
Q_OBJECT
public:
explicit ExceptionHandler(QObject *parent = nullptr);
static void registerSignalHandlers();
static void appendRuntimeLog(const QString &message);
static QString fetchLogDirectory();
private:
static void signalCallback(int signum);
static QString captureBacktrace();
static void persistCriticalLog(const QString &summary);
};
#endif // EXCEPTIONHANDLER_H源文件 exceptionhandler.cpp
#include "exceptionhandler.h"
#include <QDateTime>
#include <QFile>
#include <QDir>
#include <QTextStream>
#include <QtAndroid>
#include <signal.h>
#include <execinfo.h>
#define LOG_FOLDER "AppTrace"
ExceptionHandler::ExceptionHandler(QObject *parent)
: QObject(parent)
{
}
void ExceptionHandler::registerSignalHandlers()
{
std::signal(SIGSEGV, ExceptionHandler::signalCallback);
std::signal(SIGABRT, ExceptionHandler::signalCallback);
std::signal(SIGFPE, ExceptionHandler::signalCallback);
std::signal(SIGILL, ExceptionHandler::signalCallback);
}
QString ExceptionHandler::fetchLogDirectory()
{
// 外部存储便于调试阶段直接导出
QString base = QtAndroid::androidExternalStoragePath();
QString full = base + "/" + LOG_FOLDER;
QDir dir(full);
if (!dir.exists()) {
dir.mkpath(".");
}
return full;
}
void ExceptionHandler::appendRuntimeLog(const QString &message)
{
QString filePath = fetchLogDirectory() + "/runtime.txt";
QFile out(filePath);
if (out.open(QIODevice::Append | QIODevice::Text)) {
QTextStream stream(&out);
QString stamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
stream << QString("[%1] %2\n").arg(stamp).arg(message);
out.close();
}
}
void ExceptionHandler::signalCallback(int signum)
{
QString description;
switch (signum) {
case SIGSEGV: description = "SIGSEGV: 非法内存访问"; break;
case SIGABRT: description = "SIGABRT: 异常终止"; break;
case SIGFPE: description = "SIGFPE: 浮点运算错误"; break;
case SIGILL: description = "SIGILL: 非法机器指令"; break;
default: description = QString("未定义信号: %1").arg(signum); break;
}
persistCriticalLog(description);
// 注意:此处不恢复默认处理,让进程自然终止以确保日志落盘
}
void ExceptionHandler::persistCriticalLog(const QString &summary)
{
QString filePath = fetchLogDirectory() + "/fatal.txt";
QFile out(filePath);
if (out.open(QIODevice::Append | QIODevice::Text)) {
QTextStream stream(&out);
QString stamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
stream << "========================================\n";
stream << "崩溃时间: " << stamp << "\n";
stream << "异常摘要: " << summary << "\n";
stream << "回溯信息:\n" << captureBacktrace() << "\n";
stream << "========================================\n\n";
out.close();
}
}
QString ExceptionHandler::captureBacktrace()
{
const int MAX_DEPTH = 32;
void *frames[MAX_DEPTH];
int depth = backtrace(frames, MAX_DEPTH);
char **symbols = backtrace_symbols(frames, depth);
QString result;
for (int i = 0; i < depth; ++i) {
result += QString(" #%1 %2\n").arg(i).arg(symbols[i]);
}
free(symbols);
return result;
}主程序入口集成
main.cpp 中必须在 QApplication 构造完成后立即注册信号处理器:
#include "mainwindow.h"
#include "exceptionhandler.h"
#include <QApplication>
#include <QtAndroid>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// 越早注册,覆盖范围越完整
ExceptionHandler::registerSignalHandlers();
ExceptionHandler::appendRuntimeLog("应用进程已启动");
// 申请必要权限
QStringList permissions;
permissions << "android.permission.READ_EXTERNAL_STORAGE"
<< "android.permission.WRITE_EXTERNAL_STORAGE";
QtAndroid::requestPermissionsSync(permissions);
MainWindow w;
w.show();
return app.exec();
}测试界面搭建
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class QPushButton;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void recordUserAction();
void triggerSegfault();
private:
QPushButton *btnLog;
QPushButton *btnCrash;
};
#endif // MAINWINDOW_Hmainwindow.cpp
#include "mainwindow.h"
#include "exceptionhandler.h"
#include <QPushButton>
#include <QVBoxLayout>
#include <QMessageBox>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
auto *central = new QWidget(this);
auto *layout = new QVBoxLayout(central);
layout->setContentsMargins(40, 40, 40, 40);
btnLog = new QPushButton("记录运行日志", this);
btnCrash = new QPushButton("触发空指针崩溃", this);
connect(btnLog, &QPushButton::clicked, this, &MainWindow::recordUserAction);
connect(btnCrash, &QPushButton::clicked, this, &MainWindow::triggerSegfault);
layout->addWidget(btnLog);
layout->addWidget(btnCrash);
setCentralWidget(central);
setWindowTitle("崩溃追踪测试");
resize(360, 240);
}
MainWindow::~MainWindow() = default;
void MainWindow::recordUserAction()
{
ExceptionHandler::appendRuntimeLog("用户点击日志按钮");
QMessageBox::information(this, "完成", "日志已写入外部存储");
}
void MainWindow::triggerSegfault()
{
volatile int *nullPtr = nullptr;
*nullPtr = 0xDEAD; // 强制触发 SIGSEGV
}Android 清单配置
AndroidManifest.xml 需声明存储权限并兼容 Android 10 分区存储过渡方案:
<manifest ...>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:requestLegacyExternalStorage="true"
... >
...
</application>
</manifest>验证与查看
以 Release 模式编译并部署到真机:
- 点击"记录运行日志" → 生成
runtime.txt - 点击"触发空指针崩溃" → APP 闪退,生成
fatal.txt
文件位置:手机存储根目录下的 AppTrace 文件夹。
崩溃日志样例
========================================
崩溃时间: 2026-06-12 15:30:22
异常摘要: SIGSEGV: 非法内存访问
回溯信息:
#0 0x0000007f8a1b2c4c
#1 0x0000007f8a3d01a8
#2 0x0000007f8a3d0250
...
========================================生产环境优化建议
采用应用私有目录
替换 fetchLogDirectory() 中的路径获取逻辑,避免权限依赖:
QString ExceptionHandler::fetchLogDirectory()
{
// QtAndroid::applicationDataPath() 指向 /data/data/<包名>/files
QString path = QtAndroid::applicationDataPath() + "/trace";
QDir dir(path);
if (!dir.exists()) dir.mkpath(".");
return path;
}日志轮转与容量控制
在 persistCriticalLog 中追加文件大小检测:当日志超过阈值时,归档并重命名旧文件,防止无限制增长。
按日期组织文件
动态构建文件名如 fatal_20260612.txt,便于后期按时间维度检索与清理。
典型问题排查
| 现象 | 排查方向 |
|---|---|
| 崩溃后无日志文件 | 确认动态权限已授予;检查目录创建是否成功;验证 init 调用时机 |
| 仅有信号描述,无堆栈 | 高版本 Android 对 execinfo 支持受限,需结合 adb logcat 交叉分析 |
| 正常退出出现崩溃日志 | 排查全局对象的析构顺序,避免在静态变量清理阶段触发异常 |
| 编译失败提示 backtrace 未定义 | 清理构建产物后重新执行 qmake;确认 NDK 版本与工具链匹配 |