Highlight
Xcode 15 的新版 Debug Console 把日志消息作为核心、元数据按需展示,配合 token 化过滤和 LLDB 的 DWIM Print,让开发者从海量日志中定位问题的效率大幅提升。
核心内容
调试时最怕什么?控制台被 print 输出淹没,想找一条关键日志得翻半天。更糟的是,生产环境出了问题,用户设备上的日志根本拿不到。
Xcode 15 从三个层面解决了这个问题:重新设计的 Debug Console、更聪明的 LLDB 表达式求值、以及 Unified Logging 的最佳实践。
新版 Debug Console
旧版控制台每条日志前面都带一大串元数据(时间戳、进程 ID、线程 ID 等),真正重要的消息内容反而被挤到后面。(01:02)
新版把元数据默认隐藏,只展示开发者写的消息本身。需要看元数据时,点左下角的选项按钮按需开启。也可以选中单条日志按空格 Quick Look,在弹窗里看完整信息,包括调用该日志的函数名和源码位置。
错误(error)和故障(fault)级别的日志会用黄色和红色背景高亮,一眼就能定位严重问题。
Token 化过滤
过滤是新 Debug Console 的核心能力。(02:16)
在过滤栏输入时,自动补全会提示可用的过滤维度:subsystem、category、type、library 等。选中后输入过滤值,多个条件可以组合使用。
除了手动输入,还有三种快捷方式:
- 过滤菜单:直接勾选要显示的日志类型(debug、info、error、fault 等)
- 右键菜单:对某条日志选择 “Hide Similar” 或 “Show Only Similar”,自动生成过滤条件
- Category 过滤:输入关键词后选择 category 维度,快速聚焦到特定模块
实战:用日志定位 Bug
Session 用一个真实案例演示了这套工作流:用户更新个人资料后,修改没有保存。(03:35)
步骤如下:
- 复现问题:修改 display name,退出页面再进入,发现修改丢失
- 在 Debug Console 中输入 “account”,选择 category 过滤维度,只保留账户相关日志
- 找到 “Requesting to change displayName” 这条日志,悬停后点击右下角的源码位置,直接跳转到发出日志的代码
- 进入
setDisplayName函数,发现代码只更新了数据库,没更新本地缓存 - 修复:在数据库更新后加上
account.displayName = newDisplayName
整个定位过程不到两分钟,关键是日志写在了正确的位置,让过滤能精准命中。
LLDB 的 DWIM Print
调试时 po 和 p 该用哪个?很多人凭感觉选,结果常常拿到一个内存地址,什么信息都没有。(06:35)
Xcode 15 引入了 Do What I Mean Print(DWIM)。p 命令现在会智能判断表达式类型,自动选择最快的求值方式:
- 基础类型(Int、String 等)直接打印值
- 结构体和类优先打印属性列表
- 实现了
CustomStringConvertible的对象打印自定义描述
po 则相当于 p 加上强制对象描述模式。大部分情况下,用 p 就够了。
过去需要记 p、po、v、vo、expression、frame variable 等多个命令的场景,现在简化成两个。
从 print 迁移到 OSLog
print 有几个硬伤:没有日志级别、没有结构化元数据、生产环境无法收集、性能开销不可控。(09:18)
OSLog 解决了所有这些问题。迁移过程很简单:
import OSLog- 创建 Logger:
let logger = Logger(subsystem: "BundleID", category: "ComponentName") - 用
logger.info()、logger.error()等替代print
subsystem 通常用 Bundle Identifier,category 用类名或模块名。这样过滤时可以直接按模块定位。
详细内容
日志级别选择
OSLog 提供五个级别,按严重程度递增:
debug:开发调试信息,默认不存储到磁盘info:一般性信息,如操作开始/完成notice:需要注意的事件,默认级别error:错误,需要调查fault:严重故障,系统级问题
import OSLog
let logger = Logger(subsystem: "com.example.MyApp", category: "Account")
func login(password: String) -> Error? {
var error: Error? = nil
logger.info("Logging in user '\(username)'...")
// ... 验证逻辑 ...
if let error {
logger.error("User '\(username)' failed to log in. Error: \(error)")
} else {
logger.notice("User '\(username)' logged in successfully.")
}
return error
}
关键点:
Logger是值类型,可以全局创建也可以按需创建- subsystem 和 category 是字符串,用于 Debug Console 的过滤
- 日志消息支持字符串插值,但复杂对象需要用
\(object, privacy: .public)控制隐私级别 - 默认情况下,动态字符串会被标记为私有,保护用户数据
Bug 修复前后的代码对比
修复前的 setDisplayName 只更新了数据库,没更新本地模型:
public func setDisplayName(_ newDisplayName: String) {
logger.info("Sending Request to update DisplayName")
Database.setValueForKey(Database.Key.displayName, value: newDisplayName, forAccount: account.id)
logger.info("Updated DisplayName to '\(newDisplayName)'")
}
修复后增加了本地缓存更新:
public func setDisplayName(_ newDisplayName: String) {
logger.info("Sending Request to update DisplayName")
Database.setValueForKey(Database.Key.displayName, value: newDisplayName, forAccount: account.id)
account.displayName = newDisplayName // 新增
logger.info("Updated DisplayName to '\(newDisplayName)'")
}
关键点:
- 日志写在操作前后,形成完整的调用链路
- 通过
logger.info记录 “Sending Request” 和 “Updated” 两个状态点 - 修复后用 LLDB 的
p account验证本地属性已更新
LLDB 调试验证
在修复后的代码上打断点,用 LLDB 验证:
(lldb) p account
(BackyardBirdsData.Account) =0x000060000223b2a0 {
id = 3A9FC684-8DFC-4D7D-B645-E393AEBA14EE
joinDate = 2023-06-05 16:41:00 UTC
displayName = "Sample Account" // 修复前:旧值
emailAddress = "[email protected]"
isPremiumMember = true
}
// 执行 account.displayName = newDisplayName 后
(lldb) p account
(BackyardBirdsData.Account) =0x000060000223b2a0 {
...
displayName = "Johnny Appleseed" // 修复后:新值
emailAddress = "[email protected]"
...
}
关键点:
p现在会自动展开结构体和类的属性- 如果没有自定义
CustomStringConvertible,po只会输出对象地址 - 用
p作为默认调试命令,po仅在需要自定义描述时使用
生产环境日志收集
OSLogStore API 可以在 App 运行时读取系统日志,用于诊断用户报告的问题:
import OSLog
do {
let store = try OSLogStore(scope: .currentProcessIdentifier)
let position = store.position(timeIntervalSinceLatestBoot: 0)
let entries = try store.getEntries(at: position)
for entry in entries {
if let logEntry = entry as? OSLogEntryLog {
print("[\(logEntry.subsystem)] \(logEntry.composedMessage)")
}
}
} catch {
logger.error("Failed to read log store: \(error)")
}
关键点:
OSLogStore需要 iOS 15+/macOS 12+.currentProcessIdentifier只读取当前进程的日志composedMessage是完整的日志消息,包含所有插值结果- 可以结合
predicate过滤特定 subsystem 或 category
核心启发
1. 可诊断性优先的日志规范 做什么:在团队里建立日志规范,要求每个模块用独立的 Logger category,关键操作前后必须打日志。 为什么值得做:好的日志规范能让线上问题的定位时间从小时级降到分钟级。 怎么开始:在 App 启动时创建一组全局 Logger,按模块(Network、Database、UI、Account)分类。用 SwiftLint 规则或 Code Review 检查清单确保规范执行。
2. 内置诊断报告功能
做什么:在 App 设置里加一个”导出诊断日志”按钮,用户遇到问题时可以一键收集最近的操作日志发给开发者。
为什么值得做:OSLogStore 让生产环境日志收集变得可行,不再需要依赖难以复现的口头描述。
怎么开始:用 OSLogStore 读取最近 10 分钟的日志,过滤出本 App 的 subsystem,打包成文本文件通过邮件或上传接口发送。
3. 自动化日志断言测试
做什么:在单元测试中验证关键路径是否产生了预期的日志。
为什么值得做:日志和代码一样会腐烂,测试可以确保日志不会随着重构而丢失。
怎么开始:用 OSLogStore 在测试执行前后捕获日志,断言特定消息是否存在。或者封装一个日志中间件,在测试模式下把日志写入内存数组供断言使用。
4. 性能敏感的 Signpost 追踪
做什么:用 os.signpost 标记关键操作的起点和终点,在 Instruments 中可视化分析性能瓶颈。
为什么值得做:OSLog 和 Signpost 共享同一套基础设施,日志和性能分析可以无缝衔接。
怎么开始:用 OSSignposter 替代手动计时,在网络请求、数据库查询、图片解码等操作前后插入 begin/end 标记。在 Instruments 中选择 Logging 模板查看时间线。
关联 Session
- Explore logging in Swift — Swift 日志系统的深入介绍,包括隐私控制和日志存储
- Measure performance using Logging — 使用 OSLog Signpost 进行性能测量和分析
- Debug with Instruments — Instruments 调试工具详解,和日志系统配合使用
评论
GitHub Issues · utterances