今年WWDC的一大焦点是性能——不仅是苹果如何努力提高其操作系统和框架的整体性能,也包括向美国第三方开发者传达的信息。
就像在会议期间的几个会议中提到的,在许多情况下,保持稳定的性能水平对于提供良好的用户体验是必不可少的。 随着我们渲染屏幕的刷新率增加(iPad Pro高达120赫兹),我们(和系统)用于渲染每帧的时间会减少,维护平滑滚动和其他每帧渲染任务变得越来越具有挑战性。
在性能方面,更有挑战性的是调试和识别瓶颈。很多时候,性能下降是很难重现的,而且确实需要良好的信息才能修复。
为了解决这一问题,苹果今年推出了一个名为os_signpost的新开发者工具。使用这个新工具,我们可以很容易地在代码中放置标记(路标),从而简化性能分析和调试问题。 在今天的WWDC更新中,让我们看看如何开始使用这个新工具!
Bottlenecks
当我们发现我们正在开发的应用程序存在性能问题时,我们首先要做的是确定导致问题的瓶颈可能在哪里。设我们的应用程序在某个时刻需要加载一组记录,通过在Instruments中的time Profiler花费一些时间,我们怀疑问题在于加载记录的函数,如下所示:
func loadRecords() throws -> [Record] {
let decoder = JSONDecoder()
return try files.map { file in
let data = try file.read()
let record = try decoder.decode(Record.self, from: data)
return record
}
}
现在的问题是,到底是什么导致了性能问题? 它是读取我们文件的数据,还是解码它们的内容? 这只会发生在特定的一组文件还是所有文件? os_signpost可以帮助我们以一种友好的方式回答这些问题。
Setting things up
指示符成对放置,使用带有begin和end类型的平衡调用os_signpost。 但是在我们开始放置路标之前,我们需要创建一个日志句柄和路标id。 让我们首先创建我们的日志句柄,这是Instruments根据它们的子系统(在我们的例子中是应用程序的bundle identifier)和类别(我们正在执行的任务类型,在这个例子中是RecordLoading)来分组路标:
import os.signpost
let log = OSLog(
subsystem: "com.johnsundell.app",
category: "RecordLoading"
)
接下来,我们将为每个文件创建一个路标ID, Instruments将使用它来匹配每个开始和结束路标对:
let signpostID = OSSignpostID(log: log)
现在,设置完成后,我们可以开始放置一些路标了!
Placing signposts
我们将在每个操作之前放置一个带有begin类型的路标,在操作完成后放置一个带有end类型的路标。对于每个路标,我们都将传递我们的log和signpostID。 我们还将添加一个包含每个文件路径的描述,这样我们就能够识别出任何有特别问题的文件。下面是读取文件时的样子:
os_signpost(
type: .begin,
log: log,
name: "Read File",
signpostID: signpostID,
"%{public}s",
file.path
)
let data = try file.read()
os_signpost(
type: .end,
log: log,
name: "Read File",
signpostID: signpostID,
"%{public}s",
file.path
)
正如你在上面看到的,我们不能使用Swift的普通字符串插值。这是因为我们处理的是StaticString类型的字符串,需要在编译时完全定义。相反,我们将使用特殊的%{public}s格式来传递文件的路径作为参数。
Wrapping things up
最后,让我们在调用的周围添加相同的路标来解码每个文件的数据,最终结果如下:
import os.signpost
func loadRecords() throws -> [Record] {
let decoder = JSONDecoder()
let log = OSLog(
subsystem: "com.johnsundell.app",
category: "RecordLoading"
)
return try files.map { file in
let signpostID = OSSignpostID(log: log)
os_signpost(
type: .begin,
log: log,
name: "Read File",
signpostID: signpostID,
"%{public}s",
file.path
)
let data = try file.read()
os_signpost(
type: .end,
log: log,
name: "Read File",
signpostID: signpostID,
"%{public}s",
file.path
)
os_signpost(
type: .begin,
log: log,
name: "Decode Record",
signpostID: signpostID,
"%{public}@",
file.path
)
let record = try decoder.decode(Record.self, from: data)
os_signpost(
type: .end,
log: log,
name: "Decode Record",
signpostID: signpostID,
"%{public}s",
file.path
)
return record
}
}
有了上面的内容,我们现在可以在Xcode中选择Product > Profile来启动仪器并开始分析。 选择空白模板,使用右上角的+按钮添加os_signpost工具,现在,我们可以看到为每个文件🎉显示的路标。
现在剩下的就是深入研究仪器显示给我们的数据,识别出有问题的文件类型,以及我们需要在代码中修改哪些内容来处理这些情况。有了这些新的、更细粒度的信息,这个过程现在应该比以前容易得多。
Conclusion
Signposts似乎是一个非常有前途的新工具,尤其是在调试复杂的性能问题和异步操作时,它可以快速获得更全面的信息。然而,在这篇文章中,我们只触及了路标所能做的表面,所以我相信,我们将在未来的每周文章中重新讨论这个话题。
一个问题是,即使在我们完成分析并解决了问题之后,我们是否应该在代码库中保留使用os_signpost的调用。一方面,让这些信息总是可以访问是很好的——但另一方面,所有这些额外的调用真的会使我们的代码混乱,使它更难阅读。对于某些用例,保留它们可能是值得的,而对于另一些用例,我们可能想把它们更像一个临时的调试工具。
有关路标及其更强大的用例的更多信息,请确保查看“使用日志记录度量性能”WWDC会话。