大多数浏览器和
Developer App 均支持流媒体播放。
-
使用结构化日志进行调试
探索了解 Xcode 15 中的调试控制台,并学习如何通过日志来改善你的诊断体验。探索如何使用高级过滤器和改进的可视化功能轻松高效地浏览日志。我们还将向你展示如何在调试时使用 dwim-print 命令来评估代码中的表达式。
章节
- 0:44 - Tour of the Debug Console
- 3:28 - Live debugging
- 7:25 - LLDB improvements
- 9:04 - Tips for logging
- 12:04 - Get the most out of your logging
资源
相关视频
WWDC23
WWDC20
WWDC18
-
下载
♪ ♪
Nathan:大家好 我是 Nathan 我是 Xcode Debugger UI 团队的一名工程师 今天 我很高兴地向大家介绍 即将推出的 Xcode 15 的 全新调试控制台
在本次讲座中 我将快速 向你展示调试控制台的功能 接着 我将通过诊断我 自己的 App 中的一个真实错误 来展示调试控制台有多大作用 然后 我将向你介绍 即将推出的 LLDB 改进功能 最后 我将分享一些 小技巧 教你如何利用 Apple 的统一日志 API 来改善你的诊断体验
让我们深入了解 并浏览调试控制台的新功能 在我的设备上 我启动了 Backyard Birds App 该 App 允许用户 管理后院并照顾虚拟鸟类
启动 App 后 调试控制台中已经产生了许多日志 随即 我就注意到 控制台不再在每条日志前 加上我所习惯的元数据 相反 其重点 落在开发者想让我查看的 底层信息上 当然 我仍可能想查看 与这些日志相关的其他信息 因此我们也提供了此功能 你可在调试控制台 左下角选择元数据选项按钮 并选择最适合 当前需求的类型来查看这些信息 在本例中 我将选择 Type、 Library、Subsystem 和 Category
当启用了该选项 元数据被放在 控制台中每条日志下面 它现在更小、更不易察觉 这样不会影响预期的输出 我还可能遇到 黄色或红色背景的日志 这表明这些日志的重要性等级更高 黄色和红色分别代表错误和故障 如果我不想同时 查看所有日志的元数据 控制台允许我选择有问题的日志 并按空格键进行快速查看 以检查单个日志的元数据 这样 会出现一个弹窗 为我提供所有可用的元数据 这些元数据甚至包括调用站点 其中显示了最初生成日志的函数名 查看附加元数据这项功能很出色 但新的调试控制台 真正耀眼的地方在于其过滤功能 控制台很容易产生 一些我并不关心的无用日志 但在 Xcode 15 中 过滤日志比以往任何时候都更容易 现在控制台可执行复杂的标记过滤 可轻松定位与 我的需求最相关的日志 控制台还提供了许多 不同的方法来创建这些过滤器
当然 你也可直接在 过滤器中输入过滤条件 就像这样
当你这样做时 将出现一个自动补全弹窗 在我创建我想 输入的过滤条件时为我提供帮助
此外 过滤器菜单让你可快速访问 特定类型日志的过滤器 允许我选择我想要查看的类型
最后 当我再次点击 我更感兴趣或不太感兴趣的日志时 控制台提供了 隐藏和显示类似日志的选项 以便从我的视图中 快速聚焦或排除特定的日志 就像这样
所有这些过滤方法 都允许我快速高效地 删减我所有的日志输出 以便定位与我当前 调试需求最相关的日志 现在 让我们使用新的调试控制台 查找并修复我的 App 中的一个真实问题 我收到报告称 一些用户在更新其个人资料后 发现其内容似乎并没有被保存 让我们来探索如何 利用良好的日志实践 和新调试控制台的帮助 来快速轻松地确定该错误原因
首先 我将尝试 选择标签栏中的 Account 来复现该问题 我现在 选择铅笔图标来编辑我的帐户
最后 我将尝试更改我的显示名称
改完后 这看来已经 更改成功了 但如果我退出页面 并查看我的帐户 则更改似乎都已丢失 现在 我想到 有几件事可能出了问题 但让我们探索一下新的 调试控制台如何能帮我缩小范围 找到该问题的根源 在执行这些步骤时 调试控制台中 生成了大量输出 幸好 新控制台 总是可以发现很多日志 而我可以设置过滤器 在其中寻找我最感兴趣的内容 在本例中 我有几个 仅用于帐户管理的类别 为了专注于这些类别 我将在筛选字段中输入“account” 筛选出名字 包含“account”的所有类别 并在弹出的 类别过滤器中选择某个类别
现在留给我的是我的 代码中与账户相关部分的所有日志 设置了该过滤器后 输出变得更易管理 其中几个日志似乎表明我已经请求 设置“displayName”属性 让我们深入排查并找出为何我的 App 没能像我预期那样工作的原因 现在 我记不清 这段代码的确切位置了 所以我将鼠标 悬停在我更感兴趣的日志上 并在右下角选择源代码位置……
Xcode 将跳转到 日志的源代码位置 在本例中 我请求设置显示名称 在查看源代码后 我似乎对当前账户调用了 setDisplayName 函数 来实际执行此操作 让我们跳转到 负责更新帐户信息的 函数处以深入调查该问题 在进一步审查这段代码后 我发现 虽然我一直在将这些更改 发送到我的中央帐户数据库 但我似乎忘记更新本地帐户缓存 在更新数据库后 我应该将本地显示名称 设置为新名称 就像这样
同时 我注意到 电子邮件地址也有同样的错误 幸好 这也可用同样的方法来解决
现在让我们在这一行上 设置一个断点来验证我的怀疑 并检查这是否已解决我的问题
现在我将重新编译 我的 App 并复现之前的步骤 以暂停在该断点处 当编译暂停在断点处后 我想验证我的怀疑是否正确 为此 我将“po”当前的账户状态 并检查是否得到了 我所期望的陈旧数据
哦 不 看来我只得到了该对象地址 好吧 为什么呢? 事实证明 “po”虽然很常用 但不是我想运行的表达式类型 因为我没有为该类 声明我自己的自定义调试描述 实际上 在此情况下 我只想运行“p” 所以现在让我这样做
现在 这就是 我想要的结果 这证实了我的怀疑 即仅更新数据库无法设置显示名称 现在让我们来到我添加的行 并确认显示名称已经更新
完美 看来我的更新解决了该问题 现在我可以回去喂我的鸟了 现在让我们进入 Xcode 15 中的 LLDB 在此我们让最简单的 LLDB 表达式更加完善 回想一下刚才我解决那个错误时 我发现自己在一个 不正确的地方使用了“po” 最好的情况是 这可能 会使表达式执行时间更长 但最坏的情况是 当我未执行 CustomStringConvertible 时 可能就像我的例子一样 只会返回该属性的地址 这让我感到沮丧 我希望能有一个更好的选择 然后我对属性 运行“p”来进行追踪 这样我就得到了正确的结果 但还有许多其他命令 如“expression”、 “v”、“vo”、“frame variable”等等 可能都需要我记住 这很难记住 因此为了帮助开发者 我们引入了 Do What I Mean Print Do What I Mean Print 允许你 使用单个命令评估你 代码中的许多不同的表达式 同时以最快的方式 返回结果 从而节省时间
当然 你不希望每次检查变量时 都输入这个长命令 因此 我们替换了以前的“p”别名 现在用以执行 Do What I Mean Print 这将允许你 在大多数用例中仅运行“p” 此外 当你实际上想要打印 某个变量的自定义对象描述时 你可使用可选的对象描述标志 运行 Do What I Mean Print 命令 我们也替换了之前的“po” 现在用于执行自定义对象描述的 Do What I Mean Print 通过使用新的 Do What I Mean Print 函数 你现可为许多不同的表达式 运行这两个命令中的一个 这在过去 需要多个不同的命令 才能以最快的方式获得预期的输出 最后 让我们来看一下如何确保 每个人都能充分利用其日志 从而改善调试体验 并使你能更有效地定位和解决 可能难以重现或依赖 所接收的用户报告的问题 首先 我要提醒大家 标准 I/O 用于命令行 UI 而 OSLog 用于调试 因此 在程序执行中 记录事件时 应少使用“print” 最好使用 OSLog 来获取终端用户的结构化日志 并在“调试控制台”中保留结构 现在让我们 花点时间来研究几个例子 看看从标准 I/O 转换到 OSLog 是多么容易 这是一个简单的函数 我想对其添加一些额外的日志 一种好的做法是 记录任何正在进行的任务 以及执行这些任务的结果 让我花点时间 以我所知道的最好方式来添加 太好了 我现在 添加了一些简单的“print”语句 以帮助我追踪这段代码 我现在正在打印 我在这个函数中正在执行的操作 并打印任务完成时间及结果 不过 在我的项目中 多次执行此操作后 已经很难找到所有这些输出的来源 这导致我开始在打印时添加标记 就像许多人需要做的那样 不过 这感觉就像失控了一样 在添加了所有这些额外的输出之后 我的控制台更加凌乱了 我只希望有更好的方法 让我获取所有这些元数据 而不需要所有的额外工作 好吧 事实证明 OSLog 正是我需要的 现在 我想更新 此函数以利用统一日志 首先 我需要将 OSLog 导入到我的项目中 然后我就可创建一个日志处理器 我需要在此指定我的日志所要显示的 子系统和类别 它们可以是使我的调试 筛选更容易的任意字符串 但其通常是用 Bundle 标识符来表示子系统 用类或组件名称来表示类别的 一旦我创建了我的日志记录器 我只需调用记录器对象上提供的函数 来指定日志的级别 并提供我想要显示的消息 这样好读多了 且最终代码量显著减少 现在让我看看当我运行这代码时 其在控制台中会是什么样子
为此 让我们分离出两个 这样的日志 以便我们可以检查 所有这些元数据的来源 在日志输出中 我发现我打算输出的消息 在其正下方有我为日志指定的 额外元数据 (如果启用的话) 其中一些元数据是从 我写初始日志的地方收集的 比如消息和级别 其他元数据是从 我创建日志处理器时收集的 以节省重复工作 如子系统和类别 还有一些是在后台处理的 这些包括时间戳、 库名称、进程 ID、线程 ID 和源位置 仅举几个例子说明 所有这些信息在 需要时可提供很大的帮助 但在打印输出所有日志时会 占用大量空间 无论你的需求为何 幸好 新的“调试控制台” 允许你自定义其视图 以提供你想要的内容 最后 请在构建 App 时考虑这点 以充分利用你的日志 首先 你应为你的 App 的不同组件 创建单独的日志处理器 从而允许你在底层元数据上 设置有意义的搜索条件 以更快地定位与你的 App 某部分最相关的日志 此外 利用 OSLogStore 在你的 App 出现问题时 收集有价值的诊断信息 最后 请记住 OSLog 是一个追踪设施 这意味着其能够使用 像 Instruments 这样的工具 为你的 App 提供复杂的性能分析 在本例中 我使用日志分析模板 以 OSLogs 和指示标记 来分析我的 App 的性能 现在 让我们回顾一下我们 刚刚介绍的内容以及你可以如何 改善你自己的编程体验 首先 你应探索了解 Xcode 15 中的新调试控制台 我们对其进行了大量的改进 以满足你的所有日志需求 接着 你应努力将代码 从标准 I/O 迁移到 OSLog 以此使用新调试控制台 提供的所有新功能 然后 尝试使用 LLDB 的新 Do What I Mean Print 或“p”命令 确保首先使用此命令 进行变量检查 最后 如需了解更多 Apple 统一日志 API 的相关信息 请观看我们之前的 讲座“使用日志来衡量性能” 以及 “探索 Swift 中的日志功能” 愿你愉快使用日志 感谢你的观看
-
-
5:17 - Calling setDisplayName from Edit Account page
.onSubmit { logger.info("Requesting to change displayName to \(displayName)") accountViewModel.setDisplayName(displayName) }
-
5:34 - Account Data Setters (Before Fix)
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 setEmailAddressName(_ newEmailAddress: String) { logger.info("Sending Request to update EmailAddress") Database.setValueForKey(Database.Key.emailAddress, value: newEmailAddress, forAccount: account.id) logger.info("Updated EmailAddress to '\(newEmailAddress)'") }
-
6:04 - Account Data Setters (After Fix)
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)'") } public func setEmailAddressName(_ newEmailAddress: String) { logger.info("Sending Request to update EmailAddress") Database.setValueForKey(Database.Key.emailAddress, value: newEmailAddress, forAccount: account.id) account.emailAddress = newEmailAddress logger.info("Updated EmailAddress to '\(newEmailAddress)'") }
-
6:35 - po account
(lldb) po account
-
6:39 - po account (with result)
(lldb) po account <Account: 0x60000223b2a0>
-
7:00 - p account
(lldb) p account
-
7:04 - po account (with result)
(lldb) p account (BackyardBirdsData.Account) =0x000060000223b2a0 { id = 3A9FC684-8DFC-4D7D-B645-E393AEBA14EE joinDate = 2023-06-05 16:41:00 UTC displayName = "Sample Account" emailAddress = "sample_account@icloud.com" isPremiumMember = true }
-
7:18 - p account (after fix)
(lldb) p account (BackyardBirdsData.Account) =0x000060000223b2a0 { id = 3A9FC684-8DFC-4D7D-B645-E393AEBA14EE joinDate = 2023-06-05 16:41:00 UTC displayName = "Johnny Appleseed" emailAddress = "johnny_appleseed@icloud.com" isPremiumMember = true }
-
9:43 - Login Method Skeleton
func login(password: String) -> Error? { var error: Error? = nil //... loggedIn = true return error }
-
9:56 - Login Method with Print Statements
func login(password: String) -> Error? { var error: Error? = nil print("Logging in user '\(username)'...") … if let error { print("User '\(username)' failed to log in. Error: \(error)") } else { loggedIn = true print("User '\(username)' logged in successfully.") } return error }
-
10:18 - Login Method with Extended Print Statements
func login(password: String) -> Error? { var error: Error? = nil print("🤖 Logging in user '\(username)'... (\(#file):\(#line))") //... if let error { print("🤖 User '\(username)' failed to log in. Error: \(error) (\(#file):\(#line))") } else { loggedIn = true print("🤖 User '\(username)' logged in successfully. (\(#file):\(#line))") } return error }
-
10:40 - Login Method with Partial OSLog Transition
import OSLog let logger = Logger(subsystem: "BackyardBirdsData", category: "Account") func login(password: String) -> Error? { var error: Error? = nil print("🤖 Logging in user '\(username)'... (\(#file):\(#line))") //... if let error { print("🤖 User '\(username)' failed to log in. Error: \(error) (\(#file):\(#line))") } else { loggedIn = true print("🤖 User '\(username)' logged in successfully. (\(#file):\(#line))") } return error }
-
11:00 - Login Method with OSLog Statements
import OSLog let logger = Logger(subsystem: "BackyardBirdsData", 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 { loggedIn = true logger.notice("User '\(username)' logged in successfully.") } return error }
-
11:16 - Example Logging Statements
let logger = Logger(subsystem: "BackyardBirdsData", category: "Account") logger.error("User '\(username)' failed to log in. Error: \(error)") logger.notice("User '\(username)' logged in successfully.")
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。