导言
在发动优化时,咱们常常通过添加并发的方法来减轻主线程的耗时。而在 iOS 中,GCD 是并发编程最常用的框架。添加并发是否是发动优化的良策?开发者适合选用哪个优先级的 GCD 行列?本文将结合飞书发动优化,给出选取 GCD 行列的最佳实践,也供给针对低端机的发动优化思路。
运用此思路,咱们在未修改飞书事务逻辑的情况下,在飞书低端机上,取得了不错的用户体会收益:首屏展现时刻优化 100ms,音讯列表首刷时刻优化 1500ms。
低端机的特性
通过 Instruments 的 App Launch 功用,咱们能看到 App 发动时的线程状况、Time Profiler 等信息。其间,咱们发现不同设备在发动时的体现有很大差异。
以 iPhone 7p(低端)和 iPhone 12(高端)举例,它们的设备参数分别为:
设备 | CPU 参数 | 实际核数ProcessInfo.processInfo.activeProcessorCount | 跑满的 CPU 占比(Xcode 测验) |
---|---|---|---|
iPhone 7p | A10 芯片[1],2 高功能 + 2 低功耗,可是只要 2 核能同时作业 | 2 | 200% |
iPhone 12 | A14 芯片[2],2 高功能 + 4 低功耗 | 6 | 600% |
发动飞书时,咱们通过 Instruments 调查两个设备的线程状况,通过统计发现,iPhone 7p 上,主线程 Preempted 和 Runnable 状况的占比高达 21%。Instruments 的图中能看到主线程大片被抢占。

一个典型的部分,能看到主线程是 preempted 状况,CPU0 在履行其他进程,CPU1 在履行 GCD 线程。
而 iPhone 12,主线程 Preempted 和 Runnable 状况占比则只占 1%从这儿咱们能发现:对低端机来说,CPU 已经成为了发动的瓶颈,“增大并发”已不是一个全能的发动优化措施,而想办法减少其他线程对主线程的抢占,或许会是优化思路。
GCD queue 对主线程的抢占评测
为了评估“减少其他线程对主线程的抢占”是否是一个可行的优化思路,咱们首要需求弄了解,主线程被抢占的程度会有多大?
咱们能够运用 Demo 制造一些极端场景,了解极端场景下,主线程有多少比例会被其他线程抢占,因此有了如下 Demo 试验:
试验组1:
- 异步线程 QoS:DispatchQoS.userInteractive
- 代码:
for _ in 1...100 {
let queue = DispatchQueue.init(label: "serialQueue", qos: .userInteractive)
queue.async {
while true {
}
}
}
while true {
}
- qos_class_self 数值:33
- 主线程 Preempted + Runnable 占比:74%
试验组2:
- 异步线程 QoS:不指定 QoS 或 DispatchQoS.userInitiated
- 代码:
for _ in 1...100 {
let queue = DispatchQueue.init(label: "serialQueue")
queue.async {
while true {
}
}
}
while true {
}
- qos_class_self 数值:25
- 主线程 Preempted + Runnable 占比:73%
试验组3:
- 异步线程 QoS:DispatchQoS.utility
- 代码:
for _ in 1...100 {
let queue = DispatchQueue.init(label: "serialQueue", qos: .utility)
queue.async {
while true {
}
}
}
while true {
}
- qos_class_self 数值:17
- 主线程 Preempted + Runnable 占比:1.3%
试验组4:
- 异步线程 QoS:DispatchQoS.background
- 代码:
for _ in 1...100 {
let queue = DispatchQueue.init(label: "serialQueue", qos: .background)
queue.async {
while true {
}
}
}
while true {
}
- qos_class_self 数值:9
- 主线程 Preempted + Runnable 占比:1.3%
⬇️ 不指定 QoS 下,一个极端 Demo,发动期间主线程长时刻处于 preempted 状况,一向无法得到 running 的时机

从中咱们能看到几个定论:
- 不指定 QoS 时,自行创建的 GCD queue 的 QoS 是 User-Initiated
- User-Initiated 及以上优先级,对主线程会有严重抢占现象;而 Utility 和 Background 则简直不会抢占主线程。
另外,咱们也做测验验证了,pthread_create 创建的线程,也有类似的抢占现象。
QoS 和 Priority
看到 iPhone 7p 上主线程被其他线程抢占,咱们或许会有疑问:主线程不该该是优先级最高的么?怎么还会被其他线程抢占?
这儿,咱们需求了解一下 QoS 和线程 priority 两个概念。
QoS(quality of service)意指服务质量,它影响线程优先级(priority),也影响 I/O 吞吐、 CPU 吞吐等目标[3]。开发者能够用 qos_class_self() 接口获得当时线程 / 行列的 QoS。
苹果对于每个使命应该选用哪个 QoS,也有一些指导意见[4]:

QoS 和 priority 的确有对应关系,参阅 xnu 源码和试验成果,对应关系为:
QoS | Priority |
---|---|
User-Interactive | 46,对于 UI 线程是 47 |
User-Initiated | 37 |
Utility | 20 |
Background | 4 |
同时,线程的 priority 会随着履行动态调整。测验中咱们会发现,主线程的 priority 在运行开始时是 QoS User-Interactive 对应的 47,但随着运行会出现下降的情况。

官方文档[5]中解说了线程 priority 改变的原因,priority 由 Mach scheduler 控制,为了避免核算密布的线程独占资源,各个线程的 priority 会实时调整。
All of these mechanisms are operating continually in the Mach scheduler. This means that threads are frequently moving up or down in priority based upon their behavior and the behavior of other threads in the system.
进一步阅览 xnu 内核的源码[6],咱们发现,线程 priority 的改变,是由各个 Mach scheduler 完成的 compute_timeshare_priority 接口控制的。在 iOS 运用的 Mach scheduler 中,compute_timeshare_priority 为同一个完成 sched_compute_timeshare_priority。线程调度时的 priority,会在线程固有 priority 的基础上,结合当时线程的 CPU 占用情况和当时设备的全体负载进行调整。
在这个完成中,咱们能看到 Mach scheduler 对 priority 的调整会有一个极限:对于原先 priority = 47 的线程来说,向下调整的极限是 47 – ((BASEPRI_FOREGROUND – BASEPRI_DEFAULT) + 2) = 29。这和咱们用多个设备测验到的成果符合:主线程履行时,priority 的最低值是 29,仍然高于 Utility 对应的 priority 20。
这也解说了,为什么 Demo 中当异步线程的 QoS 是 Utility 时,就简直无法对主线程形成抢占。
优化落地
通过 Demo 试验,一个发动优化思路产生了:在飞书中,很多异步行列的 QoS 是 User-Initiated,尽管这一 QoS 低于主线程的 User-Interactive,但仍然或许对主线程形成抢占;那么,假如将异步行列的 QoS 调低到 Utility,是不是就能够优先保障主线程履行,让首屏更早展现出来?
通过一些粗犷的试验,咱们证明了飞书在这个思路上存在优化空间。但另一个问题随之而来:怎么统筹首屏、音讯列表首刷等多个目标?
考虑音讯列表首刷的场景:获取到最新的音讯,不仅仅需求主线程构建 UI,还需求依靠数据库读取、网络恳求等异步操作。假如咱们粗犷地将所有异步行列的 QoS 调低,首屏的确能更快展现,但音讯列表的首刷则随着异步操作的变慢更劣化了。这对用户体会反而带来了负向影响。
整理出哪些异步操作是首刷依靠的,保证这些行列的 QoS ,是优化中非常重要的一环。咱们首要通过不断用 Instruments 测验、阅览代码整理出了首版白名单行列,并在线下和线上验证了首屏、首刷等要害目标的优化收益。在后来的迭代中,咱们又开发了线下东西,通过在线下 hook dispatch_async 等函数,记载下首刷等时机依靠的 GCD 行列,达成了白名单行列主动生成的能力。
作用剖析
这一优化在线上产生了不错的体会优化作用:
-
发动首屏展现时刻优化 100ms
通过调整异步线程的 QoS,发动期间主线程 CPU 抢占现象有显着下降。更多核算资源会集到主线程,使得首屏展现速度显着加速。
-
音讯列表首刷时刻优化 1500ms
通过对音讯列表首刷依靠的使命的剖析,咱们调低了无关线程的 QoS,这也让首刷依靠的数据库读取、网络恳求等使命得到了更多资源,加速了它们的履行。
总结
“添加并发”在必定范围内能够作为发动优化的方案,但在低端机上,CPU 已经成为瓶颈,并发时异步线程对主线程的抢占也需求引起重视。
GCD 供给了四种 QoS 给开发者运用,官方也为这四种 QoS 供给了最佳实践主张。
通过评测和源码推理,User-Interactive 和 User-Initiated 对主线程有显着抢占,Utility 和 Background 对主线程的抢占很少。开发者创建的 GCD 行列,默许的 QoS 实际为 User-Initiated。因此在发动期间(或者任何耗时敏感期间),与发动无直接关系的 queue,应该主动设置为 Utility 或 Background,减少对主线程的抢占。
通过飞书上落地优化,咱们能得出定论:对线程或 GCD queue 调整 QoS,能在不改变发动事务逻辑的情况下取得明显收益。
当然,比过后优化更好的操作,是在编码时就充分了解不同 QoS 的行为特性,选用最适合的 QoS。
参阅文献
[1] Apple A10
en.wikipedia.org/wiki/Apple_…
[2] Apple A14
en.wikipedia.org/wiki/Apple_…
[3] 《*OS Internals》Chapter 6
[4] Prioritize Work with Quality of Service developer.apple.com/library/arc…
[5]Why Did My Thread Priority Change?
developer.apple.com/library/arc…
[6] xnu 源码 sched_compute_timeshare_priority
github.com/apple-oss-d…
加入咱们
字节跳动 APM 中台现在致力于提升整个集团内全系产品的功能和稳定性体现,技术栈覆盖 iOS/Android/Server/Web/Hybrid/PC/游戏/小程序等,作业内容包含但不限于功能稳定性监控,问题排查,深度优化,防劣化等。长期希望为业界输出更多更有建设性的问题发现和深度优化手段。欢迎对字节APM 团队职位感兴趣的同学投递简历到邮箱fengyadong@bytedance.com。