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)")
}
}
}
-
保存现有处理器:
- 调用
NSGetUncaughtExceptionHandler()保存现有的异常处理器。 - 调用
signal()时,将返回的信号处理器保存在previousSignalHandlers中。
- 调用
-
转发异常:
- 在自定义的异常处理器中调用之前的异常处理器
previousExceptionHandler。
- 在自定义的异常处理器中调用之前的异常处理器
-
转发信号:
- 在自定义的信号处理器中调用之前的信号处理器
previousSignalHandlers[signal]。 - 如果没有之前的信号处理器,恢复默认信号处理器并重新触发信号。
- 在自定义的信号处理器中调用之前的信号处理器
-
信号恢复:
- 如果当前的信号处理器无法完全处理信号,则通过
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 注册了异常或信号处理器,转发的顺序是按照注册的顺序调用的。
-
信号处理器限制:某些信号(例如
SIGKILL和SIGSTOP)无法被捕获或转发,因为它们是由系统直接处理的。 - 线程安全:如果崩溃监控需要跨线程调用(例如,异常处理器可能涉及多线程日志处理),需确保线程安全。
通过这种方式,多个崩溃监控 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 |
向无读者的管道写入 | 终止程序 |
限制:
-
信号处理器运行在系统上下文中:不能调用非异步安全的函数(如
printf、malloc等)。 -
部分信号不可捕获:如
SIGKILL和SIGSTOP是系统保留信号,无法通过signal()捕获。 - 应用可能进入不可恢复的状态:例如访问非法内存后,继续运行可能引发更多崩溃。
3. 转发异常和信号
当多个崩溃监控工具(如 Firebase Crashlytics、Bugly 等)注册处理器时,需要将崩溃事件转发给之前注册的处理器,以避免覆盖。
原理:
- 对于
NSUncaughtExceptionHandler,系统只允许一个全局的异常处理器。因此在设置新的处理器之前,需要保存旧的处理器,等处理完成后调用旧处理器。 - 对于
signal,每个信号只允许一个处理函数。通过signal()获取当前的处理器地址并保存,然后在自定义处理器中手动调用旧处理器。
4. 保存崩溃日志
崩溃信息(包括异常或信号)会被捕获后保存到文件,或发送到远程服务器。
需要保存的信息:
- 崩溃类型:
Exception或Signal。 - 崩溃原因:
NSException.reason或信号描述。 - 调用堆栈:通过
exception.callStackSymbols获取调用栈。 - 设备信息:如系统版本、设备型号等(可通过
UIDevice或ProcessInfo获取)。
原理:
- 使用
FileManager将日志写入应用的沙盒目录,通常选择Documents目录。 - 如果需要实时上传,可在后台线程完成日志上传,但要注意在崩溃信号处理器中调用异步安全的 API。
5. 与多个崩溃监控 SDK 共存
- 全局竞争:多个 SDK 会试图设置自己的异常和信号处理器。为了避免冲突,需保存之前的处理器并在完成自己的逻辑后调用之前的处理器。
- 顺序问题:异常和信号处理是链式调用的,顺序由各 SDK 的加载顺序决定。如果某个 SDK 没有转发行为,后续的处理器将无法生效。
典型流程总结
- 应用启动时,
CrashLogger注册异常处理器和信号处理器。 - 当崩溃发生时:
- 如果是
NSException,触发异常处理器。 - 如果是信号(如
SIGSEGV),触发信号处理器。
- 如果是
- 崩溃信息被记录并保存。
- 如果有其他 SDK 注册的处理器,崩溃事件会被转发给这些处理器,确保所有 SDK 能正常接收到崩溃事件。
- 崩溃日志上传到服务器或本地保存,供后续分析。
通过上述机制,CrashLogger 不仅可以捕获崩溃,还能与其他 SDK 协同工作,最大化信息收集的完整性。
Unix 信号处理器在 App 中被接收的原理
Unix 信号是一种操作系统内核与进程之间的通信机制,用于通知进程发生了某种事件,例如访问非法内存、算术错误或手动触发的中断。以下是 Unix 信号在 iOS 应用中被接收的详细原理:
1. 信号的触发与传递
信号由操作系统内核发送,可以通过以下几种方式触发:
-
程序异常:程序运行时发生错误,如访问非法内存(触发
SIGSEGV)或除以零(触发SIGFPE)。 -
手动发送:开发者或其他进程通过
kill系统调用向进程发送信号。 -
硬件异常:如总线错误(
SIGBUS)。 -
系统行为:操作系统发送信号,如管道破裂(
SIGPIPE)或调用abort()触发SIGABRT。
当信号触发时,内核会查找目标进程的信号处理机制。
2. 信号的处理机制
每个进程有一张信号处理表(signal table),其中列出了进程对各种信号的处理方式。处理方式包括:
- 默认处理:系统定义的默认行为(如终止程序)。
-
忽略信号:通过设置
SIG_IGN忽略信号。 -
自定义处理器:通过
signal()或sigaction()为信号注册处理函数。 - 挂起进程:某些信号会暂停进程运行,等待进一步的操作。
当信号触发时,内核会根据信号处理表决定调用哪个处理器。如果是自定义处理器,内核会将控制权转交给注册的处理函数。
3. 在 App 中接收信号的过程
iOS 应用运行时可以通过 signal() 或更高级的 sigaction() 函数注册自定义信号处理器:
注册处理器
signal(SIGSEGV, signalHandler)
- 注册后,当
SIGSEGV信号触发时,系统会调用signalHandler函数。
触发信号
信号的触发方式如前所述。对于 iOS 应用,最常见的是由于非法操作(如数组越界访问)触发 SIGSEGV 或调用 abort() 触发 SIGABRT。
信号的分发
- 当信号触发后,内核会暂停当前线程的执行。
- 内核检查该信号的处理方式:
- 如果有自定义处理器,内核调用该处理器函数。
- 如果没有处理器或信号不可捕获(如
SIGKILL),执行默认行为。
调用处理器
注册的信号处理函数会被调用,开发者可以在其中执行日志记录或其他操作。但由于信号处理器运行在系统上下文中,有以下限制:
-
异步安全:只能调用异步信号安全的函数(如
write()),否则可能导致死锁或未定义行为。 - 简单操作:尽量只记录关键信息,不做复杂操作,因为应用可能已处于不可恢复状态。
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 禁止捕获某些信号,如
SIGKILL和SIGSTOP,因为它们由系统保留。
6. 信号的转发
在多个崩溃监控工具中,每个工具可能注册自己的信号处理器。为了避免冲突,需要将信号转发给之前的处理器:
void signalHandler(int signal) {
// 记录信号
writeToLog("Signal received: \(signal)")
// 调用之前的处理器
if (previousHandler) {
previousHandler(signal)
}
}
通过这种方式,可以实现信号处理器的链式调用,使多个工具共存。
总结
Unix 信号处理机制通过内核触发信号、分发给应用进程的信号处理器实现。当信号到达时,iOS 应用通过注册的处理函数捕获信号并记录相关信息。信号处理器运行在系统上下文中,限制较多,需要特别注意安全性和转发机制,以确保多工具协作。












网友评论