
概述
昨天凌晨苹果刚刚发布了 WWDC2024
一系列新视频,这标志着苹果开发的一只脚已迈入人工智能(Apple Intelligence)的崭新时代。即便如此,我相信不少秃头码农们还在使用一些"远古简陋"的调试方法来剖析 2142 年的代码。

不过别担心,这一切将在小伙伴们学完本系列博文后变得"沧海桑田、天翻地覆"。
在本篇博文中,您将学到如下内容:
- print,我受够了!
- 设置 Logger 对象
- 记录我们的第一条消息
现代化的代码急需现代化的调试日志方法前来拯救,那还等什么呢?
让我们马上开始日志(Logging)记录之旅吧!
Let's go!!!:)
1. print,我受够了!
头发茂盛的小码农们都知道,print 方法是撸码者工具箱中最普遍、最简单也最实用的调试工具。把它们与断点机制相结合,大概可以搞定代码中 80% 的调试问题。
swift
class Item: Transferable, Codable {
var name: String = ""
deinit {
print("\(Self.self):\(name) deiniting!!!")
}
init() {
print("\(Self.self):\(name) init!")
}
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .item)
}
}

在上面的代码中,我们习以为常的分别使用了 print 和断点在调试中输出和中断代码流的执行。
不过,在某些调试场景中我们能够明显感觉到 print 语句和断点的力不从心:
- 需要代码的执行时刻;
- 需要代码的详细执行信息;
- 需要在海量调试输出中筛选出所需的调试信息;
- 需要调试在 App 启动早期或后台执行的代码;
- 需要调试由 App 外部触发的代码(比如远程通知消息);
- 需要在 Xcode 外部监控 App 的运行情况;
- 需要让 App 自己给自己"悬丝诊脉";
基于上面这些应用场景,我们需要更现代化的调试机制来解囊相助。
小伙伴们见识过凌晨四点还枯坐在屏幕前面,在浩如烟海的 print 输出中"大海捞针"的画面吗?
如果我告诉你们其实调试也可以很简单,很有趣,只需几行代码我们就可以做得更好,大家会作何感想呢?
我们可以将 print 的输出发送到更多地方、给它们优先权、明确时间点并且注重内容输出的隐私性。当然,这时它不再称之为 print,我们称之为日志(logging)。

日志记录是为应用程序收集重要数据的关键方法。从简单的调试字符串到记录整个事件链,拥有良好的日志策略可以帮助我们在用 Xcode 编写代码时更好的调试问题,我们甚至可以在将应用程序发布到 AppStore 后再利用日志进行调试。
下面,我们就从如何在 App 中用 OSLog 框架设置 Logger 聊起,并逐步介绍如何使用它们来记录消息吧。
2. 设置 Logger 对象
从 iOS 15.0(macOS 10.15)开始,苹果推出了 OSLog 框架,它是一个读取记录历史数据的通用日志系统:

为了设置日志记录对象 Logger,我们需要首先导入 OSLog 框架并且创建 Logger 对象的实例:
swift
import OSLog
let logger = Logger()
struct MyApp: App {
// ...
}
在以上示例代码中,我们创建了一个可以在 App 任何地方使用的全局日志记录器对象。因为我们并没有传递任何定制的参数,所以我们的 Logger 将会使用默认配置记录消息。

尽管用默认配置看起来很简洁,但实际上我们还是应该为 Logger 提供两个方面的配置参数:
- subsystem
- category
通过提供这两个参数我们可以更容易的过滤日志记录,并且可以将多个 Logger 的输出内容合并成组。
比如,我们可以在创建 Logger 对象时将 subsystem 和 category 信息也一并考虑进去:
swift
let modelLogger = Logger.init(
subsystem: "com.hopy.myapp.UI",
category: "myapp.debugging"
)
上面代码有两点需要注意:
- 苹果建议我们使用反向 DNS 标记来命名子系统 subsystem。例如,com.hopy.myapp.UI 用于表示我们应用程序中的 UI 子系统。这样我们就可以轻松地确定是哪个子系统生成了哪些日志消息;
- 我们可以使用 category 将相关日志消息分组在一起,即使它们来自不同的子系统;
一个应用程序可以具有多个日志记录器。比如,我们可以为多个子系统创建多个记录器,以便提供不同的类别。
在应用程序中使用命名良好类别和子系统的记录器将极大地改善调试体验,我们将会在后面继续讨论这一话题。
一旦我们创建了日志记录器的实例,并找到了合适的地方来妥善保存它(比如将其作为全局常量,或者将其注入或包装到自己的类中),"日志的齿轮"就开始缓缓转动啦。
下面就让我们来看看如何发送第一条日志消息吧!
3. 记录我们的第一条消息
我们可以使用 Logger 实例的 log 方法来记录日志消息。
比如在下面代码中,当用户点击按钮时我们的日志记录器将会记录一条 "Hello world!" 消息,它会在 Xcode 的调试控制台中显现出来:
swift
Button("Logging Message") {
modelLogger.log("Hello, world!")
}
不过现在因为我们使用的是 log 而不是 print 方法,所以可以让 Xcode 显示更多调试信息:

除了上图日志显示的信息以外,我们还可以在 Xcode 中"激活"显示更多调试信息的元数据(Metadata):

不过就我个人而言,时间戳是其中最有趣的信息之一。因为通常 print 语句不会显示它们,并且很难区分间隔一秒或两秒发生和同时、快速连续发生的事件。

能够从正在记录的日志信息中获取到如此多的元数据是非常有价值的,它会使调试变得如此容易。尤其是对于日志类别和子系统这两项,我们不必在日志消息中添加前缀或表情符号,就可以更轻松地追溯日志消息的源头。
如果希望按子系统或类别来过滤所有日志消息,我们则可以使用 Xcode 调试控制台的搜索区域过滤所需的日志内容:

这些过滤操作避免了我们淹没在海量日志噪声的"千山万壑"中,并可以快速定位我们感兴趣的任何内容。
因为我们可以在 App 中拥有任意多的子系统、类别和记录器,所以如果可以的话,强烈建议大家创建用于特定目的和模块的独立日志记录器,它将使调试变得 Easy 得多。
总结
在本篇博文中,我们介绍了 Swift 中更加现代化的调试日志系统以及如何利用日志记录器 Logger 创建我们自己"心仪"的日志记录。我们还讨论了如何利用 Xcode 控制台中的调试元数据和过滤条件进一步让日志记录栩栩如生、手到擒来。
感谢观赏,我们下一篇博文再会!8-)