Xcode14 新增加了一个 Thread Performance Checker tool 的功能,这个工具就是帮助你尽可能早的发现性能相关的问题并解决。我是在使用 DispatchGroup 相关的 API 的时候意识到这个工具的,因为它给我报警告了。
我之前使用 DispatchGroup 的方式大概是下面这样的,
func viewDidLoad() {
let group = DispatchGroup.init()
group.enter()
DispatchQueue.global().async {
//do sth async
group.leave()
}
group.wait()
DispatchQueue.main.async {
//do things like ui update
}
}
上面这段代码其实有值得改进的地方,我们先忽略,后面再说。这里 Thread Performance Checker 直接给我报警告:
Thread running at QOS_CLASS_USER_INTERACTIVE waiting on a lower QoS thread running at QOS_CLASS_DEFAULT. Investigate ways to avoid priority inversions
系统是根据队列的 Quality of Service(QoS) 来决定队列中任务执行的优先级的。所以这里的意思是优先级更高的任务在等待低优先级的任务执行,这样造成优先级翻转,低优先级的任务迟迟等不到执行,这里主要是影响了主线程中高优先级的执行,可能造成界面卡顿。
官方给了一篇文章来解释 Diagnosing performance issues early(https://developer.apple.com/documentation/xcode/diagnosing-performance-issues-early (opens new window)),一种解决方式是通过显式指定的 QoS 的方式保证等待的任务优先级要低于执行任务的优先级,还有一种方式是把整体的这些执行都放到一个后台的队列中执行,这样也不用担心优先级翻转。
let queue = DispatchQueue.global(qos: .background)
queue.async {
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
//do sth async
group.leave()
}
group.wait()
DispatchQueue.main.async { [weak self] in
guard let wself = self else { return }
//update ui and so on
}
}
之前忽视的一个点是,初始化 DispatchQueue.global() 的时候如果不提供 QoS 参数的话,默认是 default,但是这个 default 并不是一个最终的值,比如我们打印刚初始化好的 Global Queue 的 QoS 属性的话得到的值是 unspecified,表示缺失了 QoS 信息,这时候系统会自动根据当前环境推断 QoS,它最终应该是 User-initiated 或者是 Utility 这俩优先级。这部分内容可以参考苹果官方的这篇文章 Prioritize Work with Quality of Service Classes(https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html (opens new window))
let queue = DispatchQueue.global()
print(queue.qos)
//DispatchQoS(qosClass: Dispatch.DispatchQoS.QoSClass.unspecified, relativePriority: 0)
重新回到上面代码,几个改进的点是
主线程里不要使用 group.wait() 这种 API,倒不是说一定会出问题,主要是这个习惯很不好。说不好那次改动影响了 group.leave() 没有执行的话,界面就完全卡死了。
很多时候其实没有必要用 group.wait() 用 group.notify() 能更好的解决问题。这俩的区别在于 group.wait() 会阻塞当前线程,group.notify() 不会,group.notify() 只是保证 group 管理的顺序流是按顺序执行。举个例子
- group.wait() 执行顺序
let group = DispatchGroup.init() group.enter() print("1") DispatchQueue.global().async { print("2") group.leave() } group.wait() print("3") //执行顺序是 1,2,3
b. group.notify() 执行顺序
let group = DispatchGroup.init() group.enter() print("1") DispatchQueue.global().async { print("2") group.leave() } group.notify(queue: .main) { print("3") } print("4") //执行顺序是 1,4,2,3
优化下最开始的代码,主要是把 group.wait() 干掉,这个 API 有点敏感。
func viewDidLoad() {
super.viewDidLoad()
let group = DispatchGroup.init()
group.enter()
DispatchQueue.global(qos: .background).async {
//do sth async get result
group.leave()
}
group.notify(queue: .main) {
//do things like use async result to update ui
}
//do other things
}