大多数浏览器和
Developer App 均支持流媒体播放。
-
探索 Swift 中的日志记录
了解最新的 Swift 统一日志记录 API。了解如何在保留隐私的同时在 app 中记录事件和错误。 利用功能强大但易读的数据格式化选项——这些都不会影响性能。我们将向你展示如何收集和处理日志消息,从而帮助你了解和更正 app 中的异常行为。
资源
相关视频
WWDC23
WWDC20
-
下载
(你好 WWDC 2020)
你好 欢迎来到 WWDC
欢迎观看《探索 Swift 中的日志记录》 我叫 Ravi Kandhadai Madhavan 是 Apple 的一位工程师 在这期讲座中 我会为大家展示如何使用 Apple 的统一日志记录 API 也就是 os_log 来更轻松地调试你的 app 我会讲解 如何从一台你可以访问的设备上采集日志 我将操作演示 可用于分析日志的工具 来了解 并修复你的 app 遇到的问题
我会介绍如何控制日志调用的性能 以及使用格式化选项 来改善日志消息的可读性
修复程序错误非常重要 用户期望高质量的 app 是可靠的 并且很少出现程序错误 即使小程序错误也会导致不良用户体验 不过 有些程序错误会相对更难修正 通常 最让人无从下手的 都是那些在开发过程中难以重现的错误
日志是查找和修复 难以重现的程序错误的一款优秀工具 它们会提供一系列线索 让你甚至无需通过重现的方法 就能够追查并了解一个程序错误 现在我给大家展示 我正在开发的 app 中 一个难以重现的程序错误 稍后 我会演示如何通过添加日志记录 来有助于了解并修复这个程序错误
我的 app 叫 Fruta 用户可以在上面购买奶昔 我可以浏览这些奶昔 轻点一款奶昔并购买
我最近在 app 中 添加了一项“礼品卡”功能 我创建了一个新标签用来浏览礼品卡 我可以轻点一张卡片 并使用 Apple Pay 购买 当我滚动到这些卡片的底部后 我的 app 就会与一个服务器通信 并开始加载更多卡片 当我通过轻点选中了一张卡片后 我的 app 就会停止加载卡片 并终止任何正在进行的通信 但是我也可以返回 并继续查看更多卡片 大多数时候都可以正常运行 不幸的是 有时我会看到一个程序错误 如果当 app 正在加载更多卡片时 我轻点一张卡片 加载有时候会失败并出现一个错误 (错误:加载失败) 这很令人沮丧 因为发生时我并不在开发机器附近 这种情况非常偶然 所以我无法在调试器中重现 在你的 app 中添加日志记录 可以帮助你了解此类错误 且无需在桌面上重现它们 在 Xcode 12 中 我们为 统一日志记录引入了新的 Swift API 你可以使用这些 API 来记录你的 app 运行中发生的重要事件 这些日志由操作系统进行归档 所以之后你可以从设备上取回它们 因为这些新的 API 十分高效 所以它们可以被广泛使用 且不会降低你的 app 运行速度
将日志记录 添加到你的 app 只需简单的三步 首先 导入定义新日志记录 API 的“os”模块 其次 创建一个类型为“Logger”的实例 为它传递一个子系统和分类 这些会附加在 Logger 记录的每一条消息上
子系统通常是一个捆绑标识符 有助于识别出来自你的 app 的消息 你可以使用分类来进一步区分 来源于程序不同部分的消息
这里 我为 Logger 使用了“礼品卡”分类
第三 通过在 Logger 实例上调用一个方法 将日志记录添加到代码中有趣的位置 这里 每当我的 app 从服务器下载数据时 我都添加一条日志
通过 Logger 你可以使用字符串插值 将运行数据 添加到日志消息中 比如 这里我为日志消息 添加了一个任务标识符 这就类似于调用打印功能
不过 日志消息有一个关键的不同点 和打印不一样 日志消息 不会被全部转换为字符串 那样会非常慢 相反 编译器 和日志记录资料库会协同工作 来生成日志消息高度优化后的表示 以充分利用所记录的数据类型 使用优化后的表示 你只需在实际显示日志消息时 花费转换为字符串的成本 日志消息可以包含的数据类型种类广泛 你可以记录数值类型 比如 Int 和 Double、Objective-C 对象 以及任何其它类型 只要它们符合 Swift 的 CustomStringConvertible 协议 也就是说 要想在日志消息中添加你自己的类型 只需使其符合 CustomStringConvertible 在日志消息中添加运行数据时 你要注意 像是字符串 或对象这样的非数值类型 会在日志中被默认删改 这么做是为了确保你的 app 在发布后 并且在用户的设备上运行时 日志不会显示任何个人信息
比如 这里我在记录一条日志消息 里面包含一位用户的银行账号 是由字符串表示的 在输出的日志中 账号会被删改为“私密”
不过 不处理敏感信息的数据 可以在日志中被设置为可见 当你记录运行数据时 如图所示 传递一个 .public 值给可选的隐私参数
现在日志就会显示数据内容了 这里显示了奶昔的名称 稍后我会介绍有关隐私的更多内容
当你的 app 记录一条消息时 操作系统会以压缩形式 将它储存在设备中 你可以在你的 Mac 上 使用“日志采集”命令来取回这些日志 首先 将你的设备连接到 Mac 接着 使用设备选项 从终端运行“日志采集”命令
根据你需要日志的时间点 来提供一个开始时间 通常是你看见第一个程序错误之前几分钟 还要为储存日志存档提供一个文件名 你可以在控制台 app 中 通过双击日志存档来打开它们 这个 app 让浏览和过滤日志都变得很轻松 我们来看看如何使用日志记录来了解 之前我展示的 Fruta app 中 难以重现的程序错误 我已经将日志记录 添加进礼品卡视图的源代码了
我导入了 os 模块来访问日志记录 API 并创建了一个带有捆绑标识符 和“礼品卡”分类的 logger 我添加了日志记录 来记录这个视图下不寻常的事件 比如 当 app 开始了一项任务 来与服务器通信时 它现在会记录一个 标识出任务的独特 UUID 由于标识符不包含敏感信息 我将它设置为公开 这样就在日志中可见了 使用日志采集 我已经将 Fruta app 中的日志提取到日志存档中了 现在我会在控制台 app 中打开它
这里有很多日志条目 这是因为日志存档包含了系统中 所有进程记录的消息 我可以使用控制台的“搜索”和“过滤”功能 来缩小至我感兴趣的日志范围 首先 我会按子系统来过滤 这个例子中就是我的 app 的捆绑标识符 把显示范围限制为 只有我的 Fruta app 内的消息
我会点击右上角的搜索框 输入子系统
并在下拉列表中选中“子系统”
我可以只滚动浏览 app 的日志 然后找到与失败相对应的消息
但由于我的 app 日志记录太多 仍然有过多的条目 让我无法了解其它地方出了什么问题 我真正需要的是能缩小到问题范围的方法 我的已记录任务标识符提供了解决方法 (不在运行 无法停止!) 我可以用失败任务的任务标识符过滤 来只看与失败相关的日志 为此 我现在要 把任务标识符 作为另一个关键词添加到搜索框内
现在剩下的日志比较少了 我可以逐条查看并了解错误
第一个条目显示 app 正开始一项任务来获取更多礼品卡 接着我看到 由于一个网络错误 任务已完成 并且在超时后等待重试
下一个条目显示与此同时 用户已经选中了一个礼品卡 由此会试图将任务停止 但由于此时没有正在进行的任务 因此 app 发现 自己处于不一致状态而导致失败
这就足以让我重新构建实际发生的情况了 通过选中一张卡片 我试图停止加载更多礼品卡的任务 但恰恰在此之前 它由于一个网络错误已经停止了 这就解释了这个程序错误为何这么难重现 因为它取决于 事件和网络错误发生的先后时机 多亏了日志 我现在才能了解这个程序错误并修复它
我们看到 通过使用“日志采集”命令 你可以在 app 运行结束后采集日志 你还可以在 app 运行中 将日志进行流式传输 如果你的设备连接了你的 Mac 你就可以在控制台 app 中 实时进行日志消息的流式传输 如果你的 app 是从 Xcode 启动的 你也会在 Xcode 的控制台看到它们 这是“printf”调试的有效替代方法 它具有易于过滤的更加结构化的输出 (日志级别)
你也许已经注意到 当我在控制台 app 中浏览日志时 “失败”消息被特别标出了 这是因为 我用了“故障”日志级别来指示这条记录 日志记录 API 提供了五个日志级别 来指示消息的重要性
按照它们的重要性进行升序排列 依次为调试 信息、通知 这也是默认级别 错误和故障
“调试”级别只用在调试期间有用的消息上
“信息”级别用于对故障排除有用 但并非关键的消息 “通知”指示的是 对故障排除绝对关键的消息 你可以使用“错误”级别 来记录执行期间发生的错误 “故障”级别是最严重的 你应该用它来记录程序中 由于潜在程序错误而引发的情况 比如 记录在运行过程中违反了一个 程序预期会保留的假设 “错误”和“故障”级别在控制台 app 中 分别由黄色和红色气泡突出显示
logger 类型 针对每个日志级别有不同方法 比如 要想记录一条调试消息 要在 logger 上调用调试函数 当选择日志级别时 有一件很重要的事 你必须考虑到 那就是永久性 意思是 一条日志消息是否已归档 及是否能在 app 执行结束后被取回 不被保留的日志 只能在 app 运行期间进行流式传输 一条消息是否被保留由日志级别决定 永久性与方法的重要性成正比
“调试”级别的消息不会被保留 也就是说 它们无法在 app 执行完成后被取回 “信息”错误的消息大部分不会被保留 除非它们是在 一条日志采集命令之前被生成 记录为其它级别的消息都会被保留 而且你之后可以取回它们 不过 归档消息的数量 是有存储容量上限的 一旦超过了上限 旧消息就会被清除 变为不可用 “错误”和“故障”级别的消息 甚至会比“通知”级别的消息被保存得更久 通常 这些消息都会被保留数日 不过这取决于你设备的存储容量
日志级别同样也影响到性能 尽管日志记录一般开销很低 但日志级别 相对于彼此之间具备不同的性能 重要性较低的级别速度更快 “故障”级别是最慢的 而“调试”级别是性能最高的
记录“调试”级别的日志非常快 因为调试消息都不会被保留 它们在日志不进行流式传输时就会被丢弃 此外 Swift 编译器使用复杂的优化 来确保丢弃调试消息时 甚至都不会执行 创建消息的代码 也就是说 你可以在“调试”级别记录详尽的消息 并且调用昂贵的函数来建立消息 你的用户不用为它们买单 (日志消息格式化)
正如我之前在 Fruta app 中展示的 在一条日志消息中包含运行数据 比如任务标识符 会使它更有助于调试 但是 例如数字和字符串之类的原始数据 就比较难以被了解和解读 日志记录 API 为提高可读性 提供了多种数据格式化方式 而且无需消耗运行时间 让我们再回到 Fruta app 来看看日志消息格式化是如何帮助调试的 在礼品卡视图中 我看到一个性能问题 有时候卡片加载时间过长 这个 app 使用了多个服务器来加载礼品卡 我怀疑性能问题与选择的服务器有关 为了对此进行调查 我添加了一些日志记录 来收集与服务器之间的通信统计信息
对于每一项任务 我记录下任务标识符 所获取礼品卡的标识符 服务这项请求的服务器 以及完成任务的总时长
现在我把我的 iPhone 连上我的 Mac 然后从 Xcode 运行 app 并在 Xcode 的控制台中查看日志
不幸的是 由于日志排列不整齐 所以很难理解它们 我现在要使用格式化选项 来让它们看起来更整齐
首先 通过显示 一个卡片标识符可以包含的最大字符数 我固定了礼品卡标识符的宽度
我还把持续时间四舍五入到小数点后两位 因为我并不需要太高的精确度
我现在重启 app 并再次查看日志
现在你能看到日志变得易读多了 实际上 由于它们非常整齐 我甚至可以按住 option 键 然后通过“选取列”来复制这些区域
我会将它们 粘贴进 Numbers 表格 并使数据可视化
通过这张图 我能立即注意到 速度慢的任务都经由服务器三 所以我就要对这个服务器进行脱机维护
回顾一下 你可以 有选择地使用“格式化”和“对齐”参数 来格式化数据 由于格式化数据使用的日志记录 API 不会增加日志调用成本 你可以尽情使用格式化功能 来让你的数据看起来更美观易懂 日志记录 API 提供了许多格式化选项 我只为你展示了很少的几种
你可以用 Xcode 的代码补全来查看所有选项 其中包括将数字格式化为十六进制 八进制、指数以及更多选择
正如我们之前看到的 你可以使用隐私选项 来控制日志中的数据是否可见 认真对待已记录数据的隐私非常重要 因为日志记录随时都在发生 甚至是在你的 app 发布 并到达用户手中之后 任何可以物理访问该设备且掌握密码的人 都可以采集日志 因此 不在日志消息中 将任何个人信息标记为公开十分重要 否则就会在日志中被暴露
通过使用一个保留相等散列 你可以获得许多与使用“公开”同样的好处 但实际上却不用真的那么做 这不会显示数据内容 但仍然可以让你知道 记录的数据什么时候是相同的 从而有助于过滤日志
比如这里 我要将一个 mask 参数 传递给 .private 隐私选项 以此来使用散列记录一位客户的银行账号 这样在不显示银行账号的情况下 就可以让我知道什么时候 两条日志消息引用的是同一个账户
我所描述的 Logger API 可以在 iOS 14 上使用 如果你的 app 针对先前的发行版本 那么你可以使用 os_log 函数 它接受 printf 样式的格式化字符串 从这个发行版本开始 字符串插值也可以被传递给 os_log 函数 也就和使用 Logger 一样
总之 你可以充分利用新的日志记录 API 以调试那些原本几乎 不可能理解和修复的问题 这个得以实现是因为 你可以从你的开发设备上取回日志 并对它们进行深入分析 而无需重现一个程序错误 日志记录 API 为你提供高性能的同时 也提供了丰富的格式 因此 你可以记录包含有用信息的消息 同时可以确信 对于最终用户来说 你的 app 不会变慢 感谢观看
-
-
2:44 - Example illustrating how to add logging to your app in three simple steps
// Add logging to your app in three simple steps import os let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards") func beginTask(url: URL, handler: (Data) -> Void) { launchTask(with: url) { handler($0) } logger.log("Started a task") }
-
3:32 - An example code that logs a message with run-time data
// Add runtime data to the log messsage using string interpolation import os let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards") func beginTask(url: URL, handler: (Data) -> Void) { launchTask(with: url) { handler($0) } logger.log("Started a task \(taskId)") }
-
4:28 - Example illustrating why nonnumeric types are redacted in the logs by default
logger.log("Paid with bank account \(accountNumber)")
-
5:01 - Code that shows how to mark public data so that it is displayed in the logs
logger.log("Ordered smoothie \(smoothieName, privacy: .public)")
-
6:03 - Code shown during first demo
import SwiftUI import os let logger = Logger(subsystem: "com.example.Fruta", category: "giftcards") struct GiftCardView: View { // Denotes whether there is an active task for loading gift cards. @State private var taskRunning: Bool = false // A UUID that uniquely identifies a task. @State private var currentTaskID: UUID = UUID() // An unrecoverable error seen during execution. @State private var error: Error? = nil // A model that stores information about gift cards. @ObservedObject var model: GiftCardModel var body: some View { // Display a list of gifts which can be tapped on and scrolled through. GiftCardList(model: model, taskRunning: $taskRunning, currentTaskID: $currentTaskID, error: $error, downloadAction: beginTask, stopAction: endTask) .navigationTitle("Gift Cards") } // Start a task to download gift cards from a server. func beginTask(serverURL: URL, cardDownloadHandler: @escaping (Data) -> Void) { logger.log("Starting a new task for loading cards \(currentTaskID, privacy: .public)") launchTask(with: serverURL) { cardDownloadHandler($0) } } // Stop the currently running task for downloading cards from a server. func endTask() { guard taskRunning else { logger.fault("Task \(currentTaskID, privacy: .public) is not runinng, cannot be stopped!") error = TaskError.noActiveTask return } taskRunning = false logger.log("Task \(currentTaskID, privacy: .public) interrupted") } // Start a URLSession dataTask with the given URL. func launchTask(with url: URL, handler: @escaping (Data) -> Void) { guard error == nil else { return } taskRunning = true let task = URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { self.error = ConnectionError.other(error) } if let data = data { handler(data) } } task.resume() } }
-
11:51 - Illustration of how debug-level logging will not evaluate the code that constructs log message
logger.debug("\(slowFunction(data))")
-
import SwiftUI import os let statisticsLogger = Logger(subsystem: "com.example.Fruta", category: "statistics") // Log statistics about communication with a server. func logStatistics(taskID: UUID, giftCardID: String, serverID: Int, seconds: Double) { statisticsLogger.log("\(taskID) \(giftCardID, align: .left(columns: GiftCard.maxIDLength)) \(serverID) \(seconds, format: .fixed(precision: 2))") }
-
15:00 - Example of formatting log messages
logger.log("\(data, format: .hex, align: .right(columns: width))")
-
logger.log("Paid with bank account: \(accountNumber, privacy: .private(mask: .hash))")
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。