os_log — 是 Apple 为 iOS 和 macOS 提供的 unified logging API,取代了 NSLog 和 os_trace。与旧机制不同,os_log 在内核级别工作:消息在环形缓冲区中缓冲,仅在达到活动阈值时才写入磁盘。根据 Apple WWDC 2016 的数据,os_log 相比 NSLog 将磁盘负载降低了 10 倍,并通过类别和类型提供了对详细级别的控制。它是 iOS 开发人员的主要诊断工具:通过 Console.app,可以按进程、类别和严重级别实时过滤消息。
要点
os_log — 是 Apple 在 iOS 10 和 macOS Sierra 中引入的 unified logging API。它将分散的日志记录机制 NSLog、os_trace 和 syslog 统一为一个系统,并在 XNU 内核级别进行缓冲。
与同步写入每个消息到磁盘并阻塞线程的 NSLog 不同,os_log 使用内存中的异步环形缓冲区。消息仅在活动超过特定阈值或通过 log collect 命令时才写入磁盘。这大大减少了日志记录对应用程序性能的影响。
os_log 支持六个严重级别、按子系统(subsystem)和类别(category)的区分,以及内置的隐私机制:标记为 private 的数据在生产日志中自动屏蔽,仅在连接 Xcode 时才可供开发人员访问。
在 iOS 10 之前,开发人员使用 NSLog 进行调试,使用 syslog 进行系统消息。NSLog 写入 stderr 和控制台,但效率极低:每条消息都同步写入磁盘,在频繁日志记录时导致 UI 延迟。os_log 通过将缓冲移至 XNU 内核的 BSD 部分级别并使磁盘写入异步化,解决了这个问题。
os_log 用于所有 Apple 应用程序,并被 Apple 推荐为 iOS、macOS、tvOS 和 watchOS 的唯一日志记录 API。系统和第三方应用程序通过它向统一数据库写入消息 — 该数据库存储在内存中并定期刷新到磁盘。这些日志可以通过 Mac 上的 Console.app 或终端中的 log 命令进行分析。
os_log 的架构由三层组成:用户空间中的客户端 API(libsystem_trace.dylib)、XNU 内核中的环形缓冲区,以及异步将缓冲区刷新到磁盘的 logd 守护进程。
当应用程序调用 os_log 时,消息被复制到大小为几兆字节的内核环形缓冲区中。缓冲区按照 FIFO 原则工作:如果已满,旧消息将被新消息覆盖。logd 守护进程定期检查缓冲区并将消息保存到文件系统受保护区域中的 .tracev3 文件中。
根据 Apple Engineering 的数据,从调用 os_log 到消息出现在 Console.app 中的典型延迟在设备上为 1–5 秒,批量刷新到磁盘时则长达 60 秒。这是一个有意的折衷:应用程序性能不因日志记录而受到影响,但开发人员会看到稍有延迟的消息。
// 通过 OSLog 声明 os_log
import OSLog
let logger = Logger(
subsystem: "com.example.app",
category: "network"
)
环形缓冲区 os_log 具有固定容量,无法从用户空间更改。缓冲区大小从 Apple Watch 上的 256 KB 到 Mac 上的 4 MB 不等。当应用程序生成的消息超过缓冲区容量时,旧消息将丢失 — 这是高容量日志记录的预期行为。
对于所有消息的长期收集,使用 log collect 命令,它在设备上启动收集守护进程并将 .logarchive 导出到开发人员的计算机。在此模式下,缓冲区不会被覆盖 — 消息直接写入归档文件。
os_log 支持五个严重级别,每个级别负责不同类型的消息,并由系统以不同方式处理。Default 级别 — 适用于始终进入缓冲区的消息的基本级别。Info 和 Debug 在没有收集配置文件的生成版本中被禁用。Error 和 Fault 始终处于活动状态,并在数据库中用特殊标志标记。
| 级别 | 含义 | 默认进入缓冲区 |
|---|---|---|
| Default | 对诊断重要的普通消息 | 是 |
| Info | 用于详细分析的信息性消息 | 否(仅通过配置文件) |
| Debug | 用于开发的调试消息 | 否(仅通过配置文件) |
| Error | 需要注意的错误 | 是 |
| Fault | 导致崩溃的严重故障 | 是 |
选择正确的严重级别对性能很重要:Info 和 Debug 在正常模式下 不会写入 磁盘,因此可以大量使用而不会拖慢应用程序。Error 和 Fault 始终被保存,但它们的数量应该最少 — 每条此类消息都会由于额外的元数据而增加写入时间。
Subsystem — 是应用程序或模块的 reverse-DNS 格式标识符(com.example.app)。Category — 子系统内的文本标签,按功能领域对日志进行分组:network、ui、database、auth。这种层次结构允许在不读取每条消息的情况下过滤日志,并为每个模块单独收集统计信息。
Apple 建议为每个模块定义一个 OSLog,并在该模块的所有文件中使用它。对于应用程序的不同层 — networking、UI、persistence — 应创建单独的类别。然后在 Console.app 中可以仅启用 network 的日志并禁用其他日志,而无需重新编译应用程序。
import OSLog
extension Logger {
static let network = Logger(
subsystem: "com.example.app",
category: "network"
)
static let ui = Logger(
subsystem: "com.example.app",
category: "ui"
)
}
os_log 提供内置的隐私控制机制:格式化字符串中的每个值都可以标记为 public、private 或 auto(默认行为)。默认情况下,os_log 将所有动态字符串和对象视为潜在的机密信息,并在生产日志中用 <private> 掩码替换它们。
这对于遵守 GDPR 和 HIPAA 要求至关重要:如果应用程序通过 os_log 以自动模式记录用户的电子邮件或卡号,则真实数据永远不会写入磁盘。开发人员仅在通过 Xcode 连接或使用连接到同一 Mac 的设备的收集配置文件时才能看到完整消息。
let email = "user@example.com"
logger.log("User login: \(email, privacy: .public)")
// 在生产日志中:"User login: <private>"
// 在通过 Xcode 调试时:"User login: user@example.com"
logger.log("Payment token: \(token)")
数字(Int、Double、Float)默认被视为 public — 可以安全记录而无需标记。字符串(String、NSString、StaticString)和 对象(NSObject、CFType)默认是 private — 在生产环境中被屏蔽。静态字符串(格式字符串内引号中的字符串字面量)始终可见 — 它们属于消息本身的一部分,而非数据。
这种行为与 NSLog 不同,在 NSLog 中所有数据都以公开形式记录。迁移到 os_log 可显著降低通过日志泄露敏感用户数据的风险。
os_log 在高频日志记录中比 NSLog 快 90–95%。在包含 10,000 次调用的循环测试中,NSLog 会造成约 2.8 秒的延迟,而 os_log 在 0.3 秒内执行相同的调用。其差异在于 NSLog 的同步磁盘写入与 os_log 的异步缓冲之间的对比。
根据 Apple Performance Lab(2016 年)的数据,每秒通过 NSLog 进行 20 次日志记录的 iOS 应用程序由于主线程阻塞而每秒丢失 5–8 帧动画。使用 os_log 时不会丢失帧,因为缓冲在单独的内核线程中进行。
| 参数 | NSLog | os_log |
|---|---|---|
| 写入机制 | 同步写入到磁盘 | 内核中的异步缓冲 |
| 10,000 次调用的时间 | ~2.8 秒 | ~0.3 秒 |
| 对 FPS 的影响 | 丢失 5–8 帧 | 0 帧 |
| 严重级别 | 无 | 5 个级别 |
| 隐私 | 所有数据公开 | 自动屏蔽 |
| 过滤 | 不支持 | 按 subsystem / category / level |
os_log 有两个 API:经典的 C 语言 os_log_create 和 iOS 14 中引入的现代 Swift 封装 Logger。Swift Logger 使用 ResultBuilder 系统进行格式化 — 参数通过字符串字面量进行插值,并带有显式的隐私标记。
import OSLog
let logger = Logger(
subsystem: "com.example.app",
category: "network"
)
func handleResponse(statusCode: Int) {
if statusCode > 399 {
logger.error("HTTP error: \(statusCode, privacy: .public)")
} else {
logger.info("Response OK: \(statusCode)")
}
}
log collect — 命令行实用程序,用于从设备导出收集的日志。在通过 USB 将设备连接到 Mac 后,从终端运行。
// 将日志收集到 .logarchive 中
// 在终端中:log collect --device --output ./app_logs.logarchive
// 查看 subsystem 日志:log show --subsystem com.example.app
// 使用动态值进行日志记录
logger.log("User \(userId) opened screen \(screenName)")
使用 Logger 时,重要的是要记住参数是通过 String Interpolation 进行插值的,而不是像 os_log 的 C 版本中那样通过格式字符串。这更安全,但如果默认行为不适合开发人员,则需要为每个参数显式指定 privacy。
常见问题
os_log 在内核中异步缓冲消息,不会阻塞主线程,而 NSLog 同步写入磁盘。os_log 快 10 倍,提供 5 个严重级别,并自动屏蔽隐私数据 — NSLog 不具备这些特性中的任何一个。
对于临时调试消息,请使用 .debug — 它们会在生产版本中禁用,不会影响用户性能。对于应始终保留的重要消息,请使用 .default 或 .info。
通过 Xcode 中的 Configure Profile:Devices → 选择设备 → Open Console → Actions → Configure Profile。将所需子系统(subsystem)的收集级别设置为 Include。这将创建一个配置文件,该文件在设备首次重启之前保持有效。
是的,os_log 适用于所有 SwiftUI 应用程序,无需额外设置。在模型或视图扩展中创建一个静态 Logger,并在 onChange、task 和手势处理程序中使用它来跟踪屏幕的生命周期。
os_log 默认将字符串和对象屏蔽为 private。要查看值,请在插值中显式指定 privacy: .public。如果没有此标记,值将在生产版本中替换为掩码,但在通过 Xcode 调试时正常显示。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。