美文网首页
Swift实现:崩溃日志俘获

Swift实现:崩溃日志俘获

作者: 大成小栈 | 来源:发表于2024-11-27 21:42 被阅读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 注册了异常或信号处理器,转发的顺序是按照注册的顺序调用的。
  • 信号处理器限制:某些信号(例如 SIGKILLSIGSTOP)无法被捕获或转发,因为它们是由系统直接处理的。
  • 线程安全:如果崩溃监控需要跨线程调用(例如,异常处理器可能涉及多线程日志处理),需确保线程安全。

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




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

1. NSUncaughtExceptionHandler

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

工作机制:

  • 当应用中抛出 NSException,而且没有被 do-catch 或其他捕获机制处理时,系统会调用通过 NSSetUncaughtExceptionHandler 注册的处理器。
  • 你可以在这个处理器中记录异常信息,比如异常的名称、原因、调用堆栈等。

原理:

  • NSUncaughtExceptionHandler 是一个全局变量,指向开发者设置的处理函数。
  • 每次未捕获的异常抛出时,运行时会检查这个变量,并调用其中的处理器函数。

限制:

  • 只能捕获 NSException,无法捕获系统信号(如 SIGSEGV 等)或 Swift 的 fatalError
  • 这种机制是线程不安全的,如果多个线程同时抛出异常,可能导致行为不一致。

2. Unix 信号处理器 (signal)

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

工作机制:

  • 使用 signal() 函数,可以为某些信号(如 SIGABRT, SIGSEGV 等)注册自定义处理器。
  • 当应用程序触发信号时(例如访问非法内存地址),系统会调用注册的处理器函数。

原理:

  • 每种信号都有一个默认的行为(例如终止程序)。
  • signal() 函数允许开发者修改这种行为,将信号处理交给自定义函数。
  • 注册自定义处理器后,信号触发时系统会调用你的函数,开发者可以记录日志或转发信号。

常见信号:

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

限制:

  • 信号处理器运行在系统上下文中:不能调用非异步安全的函数(如 printfmalloc 等)。
  • 部分信号不可捕获:如 SIGKILLSIGSTOP 是系统保留信号,无法通过 signal() 捕获。
  • 应用可能进入不可恢复的状态:例如访问非法内存后,继续运行可能引发更多崩溃。

3. 转发异常和信号

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

原理:

  • 对于 NSUncaughtExceptionHandler,系统只允许一个全局的异常处理器。因此在设置新的处理器之前,需要保存旧的处理器,等处理完成后调用旧处理器。
  • 对于 signal,每个信号只允许一个处理函数。通过 signal() 获取当前的处理器地址并保存,然后在自定义处理器中手动调用旧处理器。

4. 保存崩溃日志

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

需要保存的信息:

  • 崩溃类型:ExceptionSignal
  • 崩溃原因:NSException.reason 或信号描述。
  • 调用堆栈:通过 exception.callStackSymbols 获取调用栈。
  • 设备信息:如系统版本、设备型号等(可通过 UIDeviceProcessInfo 获取)。

原理:

  • 使用 FileManager 将日志写入应用的沙盒目录,通常选择 Documents 目录。
  • 如果需要实时上传,可在后台线程完成日志上传,但要注意在崩溃信号处理器中调用异步安全的 API。

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

  • 全局竞争:多个 SDK 会试图设置自己的异常和信号处理器。为了避免冲突,需保存之前的处理器并在完成自己的逻辑后调用之前的处理器。
  • 顺序问题:异常和信号处理是链式调用的,顺序由各 SDK 的加载顺序决定。如果某个 SDK 没有转发行为,后续的处理器将无法生效。

典型流程总结

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

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




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

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


1. 信号的触发与传递

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

  • 程序异常:程序运行时发生错误,如访问非法内存(触发 SIGSEGV)或除以零(触发 SIGFPE)。
  • 手动发送:开发者或其他进程通过 kill 系统调用向进程发送信号。
  • 硬件异常:如总线错误(SIGBUS)。
  • 系统行为:操作系统发送信号,如管道破裂(SIGPIPE)或调用 abort() 触发 SIGABRT

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


2. 信号的处理机制

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

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

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


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

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

注册处理器

signal(SIGSEGV, signalHandler)
  • 注册后,当 SIGSEGV 信号触发时,系统会调用 signalHandler 函数。

触发信号

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

信号的分发

  • 当信号触发后,内核会暂停当前线程的执行。
  • 内核检查该信号的处理方式:
    • 如果有自定义处理器,内核调用该处理器函数。
    • 如果没有处理器或信号不可捕获(如 SIGKILL),执行默认行为。

调用处理器

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

  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 的特殊限制

  • 沙盒机制:iOS 沙盒限制了应用对系统资源的直接访问,但信号机制仍可正常工作,因为它是操作系统内核的一部分。
  • 可捕获信号有限:iOS 禁止捕获某些信号,如 SIGKILLSIGSTOP,因为它们由系统保留。

6. 信号的转发

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

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

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


总结

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

相关文章

网友评论

      本文标题:Swift实现:崩溃日志俘获

      本文链接:https://www.haomeiwen.com/subject/ojfhsjtx.html