当前位置:首页 > 技术 > 正文内容

Qt Android 崩溃追踪:原生信号捕获与本地化日志方案

访客 技术 2026年9月21日 12

核心机制

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_H

mainwindow.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 模式编译并部署到真机:

  1. 点击"记录运行日志" → 生成 runtime.txt
  2. 点击"触发空指针崩溃" → 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 版本与工具链匹配

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。