Swift实现:崩溃日志俘获

2024-11-27  本文已影响0人  大成小栈

CrashLogger 是一个崩溃日志俘获工具,支持异常和信号的转发,而且在应用中集成多个崩溃监控 SDK 时,为了避免彼此冲突,可以将已经捕获的异常和信号转发给其他 SDK。代码如下:

import Foundation

class CrashLogger {
    static let shared = CrashLogger()

    private var previousExceptionHandler: NSUncaughtExceptionHandler?
    private var previousSignalHandlers: [Int32: sig_t] = [:]

    private init() {}

    /// 初始化崩溃日志监控
    func startMonitoring() {
        setUncaughtExceptionHandler()
        setSignalHandler()
    }

    /// 捕获未捕获的异常,并转发给其他 SDK
    private func setUncaughtExceptionHandler() {
        // 保存当前的异常处理器
        previousExceptionHandler = NSGetUncaughtExceptionHandler()
        
        NSSetUncaughtExceptionHandler { exception in
            // 记录崩溃日志
            let crashInfo = """
            *** Uncaught Exception ***
            Name: \(exception.name)
            Reason: \(exception.reason ?? "Unknown")
            UserInfo: \(exception.userInfo ?? [:])
            CallStack:\n\(exception.callStackSymbols.joined(separator: "\n"))
            """
            CrashLogger.shared.saveCrashLog(crashInfo)
            
            // 调用之前的异常处理器(如果存在)
            self.previousExceptionHandler?(exception)
        }
    }

    /// 捕获 Unix 信号,并转发给其他 SDK
    private func setSignalHandler() {
        let signalTypes: [Int32] = [
            SIGABRT, // Abort signal
            SIGILL,  // Illegal instruction
            SIGSEGV, // Segmentation violation
            SIGFPE,  // Floating point exception
            SIGBUS,  // Bus error
            SIGPIPE  // Broken pipe
        ]
        
        for signalType in signalTypes {
            // 保存当前的信号处理器
            let previousHandler = signal(signalType) { signal in
                let crashInfo = """
                *** Signal Crash ***
                Signal: \(signal)
                Description: \(CrashLogger.signalDescription(for: signal))
                """
                CrashLogger.shared.saveCrashLog(crashInfo)
                
                // 调用之前的信号处理器(如果存在)
                if let previousHandler = CrashLogger.shared.previousSignalHandlers[signal] {
                    previousHandler(signal)
                } else {
                    // 恢复默认信号处理器并重新触发信号
                    signal(signal, SIG_DFL)
                    raise(signal)
                }
            }
            previousSignalHandlers[signalType] = previousHandler
        }
    }

    /// 获取信号描述
    private static func signalDescription(for signal: Int32) -> String {
        switch signal {
        case SIGABRT: return "SIGABRT: Abort signal"
        case SIGILL: return "SIGILL: Illegal instruction"
        case SIGSEGV: return "SIGSEGV: Segmentation violation"
        case SIGFPE: return "SIGFPE: Floating point exception"
        case SIGBUS: return "SIGBUS: Bus error"
        case SIGPIPE: return "SIGPIPE: Broken pipe"
        default: return "Unknown signal"
        }
    }

    /// 保存崩溃日志
    private func saveCrashLog(_ log: String) {
        let fileName = "CrashLog_\(Date().timeIntervalSince1970).log"
        let logDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let logFileURL = logDirectory.appendingPathComponent(fileName)
        
        do {
            try log.write(to: logFileURL, atomically: true, encoding: .utf8)
            print("Crash log saved to: \(logFileURL)")
        } catch {
            print("Failed to save crash log: \(error.localizedDescription)")
        }
    }
}
  1. 保存现有处理器

    • 调用 NSGetUncaughtExceptionHandler() 保存现有的异常处理器。
    • 调用 signal() 时,将返回的信号处理器保存在 previousSignalHandlers 中。
  2. 转发异常

    • 在自定义的异常处理器中调用之前的异常处理器 previousExceptionHandler
  3. 转发信号

    • 在自定义的信号处理器中调用之前的信号处理器 previousSignalHandlers[signal]
    • 如果没有之前的信号处理器,恢复默认信号处理器并重新触发信号。
  4. 信号恢复

    • 如果当前的信号处理器无法完全处理信号,则通过 raise(signal) 重新触发信号。

使用示例

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        CrashLogger.shared.startMonitoring()
        return true
    }
}

注意事项

通过这种方式,多个崩溃监控 SDK 可以和谐共存,互不干扰,确保每个 SDK 都能正常收到崩溃信息。




捕获异常和信号,涉及以下核心原理:

1. NSUncaughtExceptionHandler

NSSetUncaughtExceptionHandler 是一个全局函数,用于设置未捕获的异常处理器。

工作机制:

原理:

限制:


2. Unix 信号处理器 (signal)

Unix 信号是操作系统用于向应用程序发送事件通知的机制,常用于处理严重错误(如非法内存访问、除零等)。

工作机制:

原理:

常见信号:

信号代码 描述 默认行为
SIGABRT 调用 abort() 触发 终止程序
SIGILL 非法指令 终止程序
SIGSEGV 非法内存访问 终止程序
SIGFPE 算术错误(如除零) 终止程序
SIGBUS 总线错误 终止程序
SIGPIPE 向无读者的管道写入 终止程序

限制:


3. 转发异常和信号

当多个崩溃监控工具(如 Firebase Crashlytics、Bugly 等)注册处理器时,需要将崩溃事件转发给之前注册的处理器,以避免覆盖。

原理:


4. 保存崩溃日志

崩溃信息(包括异常或信号)会被捕获后保存到文件,或发送到远程服务器。

需要保存的信息:

原理:


5. 与多个崩溃监控 SDK 共存


典型流程总结

  1. 应用启动时,CrashLogger 注册异常处理器和信号处理器。
  2. 当崩溃发生时:
    • 如果是 NSException,触发异常处理器。
    • 如果是信号(如 SIGSEGV),触发信号处理器。
  3. 崩溃信息被记录并保存。
  4. 如果有其他 SDK 注册的处理器,崩溃事件会被转发给这些处理器,确保所有 SDK 能正常接收到崩溃事件。
  5. 崩溃日志上传到服务器或本地保存,供后续分析。

通过上述机制,CrashLogger 不仅可以捕获崩溃,还能与其他 SDK 协同工作,最大化信息收集的完整性。




Unix 信号处理器在 App 中被接收的原理

Unix 信号是一种操作系统内核与进程之间的通信机制,用于通知进程发生了某种事件,例如访问非法内存、算术错误或手动触发的中断。以下是 Unix 信号在 iOS 应用中被接收的详细原理:


1. 信号的触发与传递

信号由操作系统内核发送,可以通过以下几种方式触发:

当信号触发时,内核会查找目标进程的信号处理机制。


2. 信号的处理机制

每个进程有一张信号处理表(signal table),其中列出了进程对各种信号的处理方式。处理方式包括:

  1. 默认处理:系统定义的默认行为(如终止程序)。
  2. 忽略信号:通过设置 SIG_IGN 忽略信号。
  3. 自定义处理器:通过 signal()sigaction() 为信号注册处理函数。
  4. 挂起进程:某些信号会暂停进程运行,等待进一步的操作。

当信号触发时,内核会根据信号处理表决定调用哪个处理器。如果是自定义处理器,内核会将控制权转交给注册的处理函数。


3. 在 App 中接收信号的过程

iOS 应用运行时可以通过 signal() 或更高级的 sigaction() 函数注册自定义信号处理器:

注册处理器

signal(SIGSEGV, signalHandler)

触发信号

信号的触发方式如前所述。对于 iOS 应用,最常见的是由于非法操作(如数组越界访问)触发 SIGSEGV 或调用 abort() 触发 SIGABRT

信号的分发

调用处理器

注册的信号处理函数会被调用,开发者可以在其中执行日志记录或其他操作。但由于信号处理器运行在系统上下文中,有以下限制:

  1. 异步安全:只能调用异步信号安全的函数(如 write()),否则可能导致死锁或未定义行为。
  2. 简单操作:尽量只记录关键信息,不做复杂操作,因为应用可能已处于不可恢复状态。

4. 信号处理的生命周期

信号处理器的注册通常在应用启动时完成,代码如下:

func setupSignalHandlers() {
    signal(SIGSEGV, signalHandler)
    signal(SIGABRT, signalHandler)
    signal(SIGFPE, signalHandler)
}

当信号触发时,系统自动调用 signalHandler 函数。示例:

void signalHandler(int signal) {
    // 捕获信号后的处理逻辑
    writeToLog("Received signal: \(signal)")
    // 可选:将信号转发给之前的处理器
    if (previousHandler) {
        previousHandler(signal)
    }
}

5. iOS 的特殊限制


6. 信号的转发

在多个崩溃监控工具中,每个工具可能注册自己的信号处理器。为了避免冲突,需要将信号转发给之前的处理器:

void signalHandler(int signal) {
    // 记录信号
    writeToLog("Signal received: \(signal)")
    // 调用之前的处理器
    if (previousHandler) {
        previousHandler(signal)
    }
}

通过这种方式,可以实现信号处理器的链式调用,使多个工具共存。


总结

Unix 信号处理机制通过内核触发信号、分发给应用进程的信号处理器实现。当信号到达时,iOS 应用通过注册的处理函数捕获信号并记录相关信息。信号处理器运行在系统上下文中,限制较多,需要特别注意安全性和转发机制,以确保多工具协作。

上一篇 下一篇

猜你喜欢

热点阅读