保持稳定的性能,是我们提供良好的用户体验的关键之一。
但是很多时候,性能下降很难重现,也无法获得良好的数据支撑。
为了解决这个问题,苹果在iOS12上推出了一个用于辅助开发调试的工具:Signposts(路标)。
Signposts(路标) 是 OSLog 家族成员之一,我们可以用它来测量和收集性能数据,以便结合 Instruments 来做可视化分析。其对应的API为 os_signpost,它主要有两大功能:标记时间段 ( intervals) 和单个时间点 ( events)。
下面,我们来看看如何使用 Signposts。
Signposts 通过用 os_signpost(.begin, ...) 和 os_signpost(.end, ...) 来标记一个路标间隔的开始和结束。

代码如下:
import os.signpost
// 1.
let refreshLog = OSLog(subsystem: "com.example.your-app", category: "RefreshOperations")
// 2.
os_signpost(.begin, log: refreshLog, name: "Refresh Panel")
for element in panel.elements {
// 3.
os_signpost(.begin, log: refreshLog, name: "Fetch Asset")
// 4.
fetchAsset(for: element)
// 5.
os_signpost(.end, log: refreshLog, name: "Fetch Asset")
}
// 6.
os_signpost(.end, log: refreshLog, name: "Refresh Panel")
下面详细解释一下上面的代码:
subsystem 表示应用子系统,可以是应用的包名或者框架的名称,采用反向DNS 表示法,如 com.your_company.your_subsystem_name; category 表示子系统中的一个类别,用于对日志消息进行分类和过滤。在每次开始、结束获取资源时,我们都添加了一个 路标。因为在开始和结束的路标的标识一样,所以我们可以将两者匹配在一起。
for element in panel.elements {
os_signpost(.begin, log: refreshLog, name: "Fetch Asset")
fetchAsset(for: element)
os_signpost(.end, log: refreshLog, name: "Fetch Asset")
}
像上面路标名称为 Fetch Asset 会运行多次,我们无法区分是获取哪个资源的时间间隔。为了让系统能够区分它们并知道哪个开始匹配哪个结束,我们可以添加一个路标ID。
for element in panel.elements {
// 通过日志句柄来生成一个路标ID。
let spid = OSSignpostID(log: refreshLog)
os_signpost(.begin, log: refreshLog, name: "Fetch Asset", signpostID: spid)
fetchAsset(for: element)
os_signpost(.end, log: refreshLog, name: "Fetch Asset", signpostID: spid)
}
如果调用的对象本身具有唯一性,还可以用对象作为路标ID。
let spid = OSSignpostID(log: log, object: element)

路标还支持测量异步任务的时间间隔。由于是异步的,这些时间间隔可能会相互重叠,所以我们需要在创建路标时加上路标ID来区分。
let refreshLog = OSLog(subsystem: "com.example.your-app", category: "RefreshOperations")
let spidForRefresh = OSSignpostID(log: refreshLog)
os_signpost(.begin, log: refreshLog, name: "Refresh Panel", signpostID: spidForRefresh)
for element in panel.elements {
// 通过对象去创建路标ID
let spid = OSSignpostID(log: refreshLog, object: element)
// 通过ID创建一个路标
os_signpost(.begin, log: refreshLog, name: "Fetch Asset", signpostID: spid)
fetchAssetAsync(for: element) {
// 每一个完成之后的回调
os_signpost(.end, log: refreshLog, name: "Fetch Asset", signpostID: spid)
}
}
// 全部完成完成的回调
notifyWhenDone {
os_signpost(.end, log: refreshLog, name: "Refresh Panel", signpostID: spidForRefresh)
}
你可能还给路标的开始/结束提供一些上下文信息,如成功/失败信息,有利于我们后续去分析和追踪特定场景下的问题。
os_signpost 函数支持可变参数,允许我们在使用的时候通过格式化字符串的方式增加元数据。
// 添加上下文信息
os_signpost(.begin, log: log, name: "Compute Physics", "for particle")
// 格式化字符串
os_signpost(.begin, log: log, name: "Compute Physics", "%d %d %d %d",
x1, y1, x2, y2)
需要注意的是,os_signpost这个API的 name 和 format 参数都是 StaticString 类型(format是可选参数)。StaticString 与 String 的区别是前者的值是由编译时确认的,其初始化之后无法修改,即使是使用 var 创建。系统的日志库 OSLog 也是选择 StaticString 作为参数类型,这么做的目的一部分在于编译器可采取一定的优化,另一部分则是出于对隐私的考量。
如果需要动态拼接字符串,需要使用这种格式化语法:%{public}s。
// 拼接动态字符串
os_signpost(.begin, log: log, name: "Compute Physics",
"Calculating %{public}s: %d %d %d %d", description, x1, y2, x2, y2)
除了标记开始和结束之外,您还可以使用 .event 路标类型标记过程中的特定时间点。

示例代码:
os_signpost(.event, log: log, name: "Fetch Asset", "Connected to service")
os_signpost(.event, log: log, name: "Fetch Asset",
"Fetched first chunk, size %u", size)
Signposts(路标)是非常轻量级的,通过编译器做了很多优化,这些优化主要是在编译时完成的而不是运行时,而且很多工作都交给了 Instruments后端来完成,这使得Signposts在发送的时候只会占用非常少的系统资源。
路标默认是开启的,如果我们希望禁用路标,可以将其日志句柄设置为 OSLog.disabled
let refreshLog: OSLog
// 可在 Xcode -> Edit Scheme -> Arguments 下面添加环境变量
if ProcessInfo.processInfo.environment.keys.contains("SIGNPOSTS_FOR_REFRESH") {
refreshLog = OSLog(subsystem: "com.example.your-app", category: "RefreshOperations")
} else {
refreshLog = .disabled //禁用路标
}
os_signpost(.begin, log: refreshLog, name: "Refresh Panel")
for element in panel.elements {
os_signpost(.begin, log: refreshLog, name: "Fetch Asset")
fetchAsset(for: element)
os_signpost(.end, log: refreshLog, name: "Fetch Asset")
}
os_signpost(.end, log: refreshLog, name: "Refresh Panel")
如果你有一些基于Instruments的特定的功能,你可以检查特定的日志句柄,查看其 signpostsEnabled 属性。然后用该属性来控制添加该附加操作。
if refreshLog.signpostsEnabled {
let information = copyDescription()
os_signpost(..., information)
}
Signposts不仅支持Swift,同时也支持C、Objective-c,对应的API如下图所示。

在Xcode中,长按运行按钮,选择Profile,也可以直接用快捷健 CMD + I 来打开Instruments,选择空白(Blank)模块,在右上角添加 os_signpost。

然后,点击左上角的Record按钮,开始启动我们的App,然后在 Instruments 可以陆续看到我们添加的路标。
