不改一行業(yè)務(wù)代碼,飛書 iOS 低端機(jī)啟動(dòng)優(yōu)化實(shí)踐(飛書ui)
引言
在啟動(dòng)優(yōu)化時(shí),我們常常通過(guò)增加并發(fā)的方式來(lái)減輕主線程的耗時(shí)。而在 iOS 中,GCD 是并發(fā)編程最常用的框架。增加并發(fā)是否是啟動(dòng)優(yōu)化的良策?開發(fā)者適合選用哪個(gè)優(yōu)先級(jí)的 GCD 隊(duì)列?本文將結(jié)合飛書啟動(dòng)優(yōu)化,給出選取 GCD 隊(duì)列的最佳實(shí)踐,也提供針對(duì)低端機(jī)的啟動(dòng)優(yōu)化思路。
應(yīng)用此思路,我們?cè)谖葱薷娘w書業(yè)務(wù)邏輯的情況下,在飛書低端機(jī)上,取得了不錯(cuò)的用戶體驗(yàn)收益:首屏展示時(shí)間優(yōu)化 100ms,消息列表首刷時(shí)間優(yōu)化 1500ms。
低端機(jī)的特性
通過(guò) Instruments 的 App Launch 功能,我們能看到 App 啟動(dòng)時(shí)的線程狀態(tài)、Time Profiler 等信息。其中,我們發(fā)現(xiàn)不同設(shè)備在啟動(dòng)時(shí)的表現(xiàn)有很大差異。
以 iPhone 7p(低端)和 iPhone 12(高端)舉例,它們的設(shè)備參數(shù)分別為:
設(shè)備 | CPU 參數(shù) | 實(shí)際核數(shù) ProcessInfo.processInfo.activeProcessorCount | 跑滿的 CPU 占比(Xcode 測(cè)試) |
iPhone 7p | A10 芯片[1],2 高性能 2 低功耗,但是只有 2 核能同時(shí)工作 | 2 | 200% |
iPhone 12 | A14 芯片[2],2 高性能 4 低功耗 | 6 | 600% |
啟動(dòng)飛書時(shí),我們通過(guò) Instruments 觀察兩個(gè)設(shè)備的線程狀態(tài),經(jīng)過(guò)統(tǒng)計(jì)發(fā)現(xiàn),iPhone 7p 上,主線程 Preempted 和 Runnable 狀態(tài)的占比高達(dá) 21%。Instruments 的圖中能看到主線程大片被搶占。
一個(gè)典型的局部,能看到主線程是 preempted 狀態(tài),CPU0 在執(zhí)行其他進(jìn)程,CPU1 在執(zhí)行 GCD 線程。
而 iPhone 12,主線程 Preempted 和 Runnable 狀態(tài)占比則只占 1%
從這里我們能發(fā)現(xiàn):對(duì)低端機(jī)來(lái)說(shuō),CPU 已經(jīng)成為了啟動(dòng)的瓶頸,“增大并發(fā)”已不是一個(gè)萬(wàn)能的啟動(dòng)優(yōu)化措施,而想辦法減少其他線程對(duì)主線程的搶占,可能會(huì)是優(yōu)化思路。
GCD queue 對(duì)主線程的搶占評(píng)測(cè)
為了評(píng)估“減少其他線程對(duì)主線程的搶占”是否是一個(gè)可行的優(yōu)化思路,我們首先需要弄明白,主線程被搶占的程度會(huì)有多大?
我們可以使用 Demo 制造一些極端場(chǎng)景,了解極端場(chǎng)景下,主線程有多少比例會(huì)被其他線程搶占,因此有了如下 Demo 實(shí)驗(yàn):
實(shí)驗(yàn)組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 數(shù)值:33
- 主線程 Preempted Runnable 占比:74%
實(shí)驗(yàn)組2:
- 異步線程 QoS:不指定 QoS 或 DispatchQoS.userInitiated
- 代碼:
for _ in 1...100 { let queue = DispatchQueue.init(label: "serialQueue") queue.async { while true { } }}while true {}
- qos_class_self 數(shù)值:25
- 主線程 Preempted Runnable 占比:73%
實(shí)驗(yàn)組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 數(shù)值:17
- 主線程 Preempted Runnable 占比:1.3%
實(shí)驗(yàn)組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 數(shù)值:9
- 主線程 Preempted Runnable 占比:1.3%
?? 不指定 QoS 下,一個(gè)極端 Demo,啟動(dòng)期間主線程長(zhǎng)時(shí)間處于 preempted 狀態(tài),一直無(wú)法得到 running 的機(jī)會(huì)
從中我們能看到幾個(gè)結(jié)論:
- 不指定 QoS 時(shí),自行創(chuàng)建的 GCD queue 的 QoS 是 User-Initiated
- User-Initiated 及以上優(yōu)先級(jí),對(duì)主線程會(huì)有嚴(yán)重?fù)屨棘F(xiàn)象;而 Utility 和 Background 則幾乎不會(huì)搶占主線程。
另外,我們也做測(cè)試驗(yàn)證了,pthread_create 創(chuàng)建的線程,也有類似的搶占現(xiàn)象。
QoS 和 Priority
看到 iPhone 7p 上主線程被其他線程搶占,我們可能會(huì)有疑問(wèn):主線程不應(yīng)該是優(yōu)先級(jí)最高的么?怎么還會(huì)被其他線程搶占?
這里,我們需要理解一下 QoS 和線程 priority 兩個(gè)概念。
QoS(quality of service)意指服務(wù)質(zhì)量,它影響線程優(yōu)先級(jí)(priority),也影響 I/O 吞吐、 CPU 吞吐等指標(biāo)[3]。開發(fā)者可以用 qos_class_self() 接口獲得當(dāng)前線程 / 隊(duì)列的 QoS。
蘋果對(duì)于每個(gè)任務(wù)應(yīng)該選用哪個(gè) QoS,也有一些指導(dǎo)意見[4]:
QoS 和 priority 確實(shí)有對(duì)應(yīng)關(guān)系,參考 xnu 源碼和實(shí)驗(yàn)結(jié)果,對(duì)應(yīng)關(guān)系為:
QoS | Priority |
User-Interactive | 46,對(duì)于 UI 線程是 47 |
User-Initiated | 37 |
Utility | 20 |
Background | 4 |
同時(shí),線程的 priority 會(huì)隨著執(zhí)行動(dòng)態(tài)調(diào)整。測(cè)試中我們會(huì)發(fā)現(xiàn),主線程的 priority 在運(yùn)行開始時(shí)是 QoS User-Interactive 對(duì)應(yīng)的 47,但隨著運(yùn)行會(huì)出現(xiàn)下降的情況。
官方文檔[5]中解釋了線程 priority 變化的原因,priority 由 Mach scheduler 控制,為了防止計(jì)算密集的線程壟斷資源,各個(gè)線程的 priority 會(huì)實(shí)時(shí)調(diào)整。
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.
進(jìn)一步閱讀 xnu 內(nèi)核的源碼[6],我們發(fā)現(xiàn),線程 priority 的變化,是由各個(gè) Mach scheduler 實(shí)現(xiàn)的 compute_timeshare_priority 接口控制的。在 iOS 使用的 Mach scheduler 中,compute_timeshare_priority 為同一個(gè)實(shí)現(xiàn) sched_compute_timeshare_priority。線程調(diào)度時(shí)的 priority,會(huì)在線程固有 priority 的基礎(chǔ)上,結(jié)合當(dāng)前線程的 CPU 占用情況和當(dāng)前設(shè)備的整體負(fù)載進(jìn)行調(diào)整。
在這個(gè)實(shí)現(xiàn)中,我們能看到 Mach scheduler 對(duì) priority 的調(diào)整會(huì)有一個(gè)極限:對(duì)于原先 priority = 47 的線程來(lái)說(shuō),向下調(diào)整的極限是 47 – ((BASEPRI_FOREGROUND – BASEPRI_DEFAULT) 2) = 29。這和我們用多個(gè)設(shè)備測(cè)試到的結(jié)果吻合:主線程執(zhí)行時(shí),priority 的最低值是 29,依然高于 Utility 對(duì)應(yīng)的 priority 20。
這也解釋了,為什么 Demo 中當(dāng)異步線程的 QoS 是 Utility 時(shí),就幾乎無(wú)法對(duì)主線程造成搶占。
優(yōu)化落地
通過(guò) Demo 實(shí)驗(yàn),一個(gè)啟動(dòng)優(yōu)化思路產(chǎn)生了:在飛書中,大量異步隊(duì)列的 QoS 是 User-Initiated,盡管這一 QoS 低于主線程的 User-Interactive,但依然可能對(duì)主線程造成搶占;那么,如果將異步隊(duì)列的 QoS 調(diào)低到 Utility,是不是就可以優(yōu)先保障主線程執(zhí)行,讓首屏更早展現(xiàn)出來(lái)?
經(jīng)過(guò)一些粗暴的實(shí)驗(yàn),我們證實(shí)了飛書在這個(gè)思路上存在優(yōu)化空間。但另一個(gè)問(wèn)題隨之而來(lái):如何兼顧首屏、消息列表首刷等多個(gè)指標(biāo)?
考慮消息列表首刷的場(chǎng)景:獲取到最新的消息,不僅僅需要主線程構(gòu)建 UI,還需要依賴數(shù)據(jù)庫(kù)讀取、網(wǎng)絡(luò)請(qǐng)求等異步操作。如果我們粗暴地將所有異步隊(duì)列的 QoS 調(diào)低,首屏確實(shí)能更快展現(xiàn),但消息列表的首刷則隨著異步操作的變慢更劣化了。這對(duì)用戶體驗(yàn)反而帶來(lái)了負(fù)向影響。
梳理出哪些異步操作是首刷依賴的,確保這些隊(duì)列的 QoS ,是優(yōu)化中非常重要的一環(huán)。我們首先通過(guò)不斷用 Instruments 測(cè)試、閱讀代碼梳理出了首版白名單隊(duì)列,并在線下和線上驗(yàn)證了首屏、首刷等關(guān)鍵指標(biāo)的優(yōu)化收益。在后來(lái)的迭代中,我們又開發(fā)了線下工具,通過(guò)在線下 hook dispatch_async 等函數(shù),記錄下首刷等時(shí)機(jī)依賴的 GCD 隊(duì)列,達(dá)成了白名單隊(duì)列自動(dòng)生成的能力。
效果分析
這一優(yōu)化在線上產(chǎn)生了不錯(cuò)的體驗(yàn)優(yōu)化效果:
- 啟動(dòng)首屏展現(xiàn)時(shí)間優(yōu)化 100ms
通過(guò)調(diào)整異步線程的 QoS,啟動(dòng)期間主線程 CPU 搶占現(xiàn)象有明顯降低。更多計(jì)算資源集中到主線程,使得首屏展示速度明顯加快。
- 消息列表首刷時(shí)間優(yōu)化 1500ms
通過(guò)對(duì)消息列表首刷依賴的任務(wù)的分析,我們調(diào)低了無(wú)關(guān)線程的 QoS,這也讓首刷依賴的數(shù)據(jù)庫(kù)讀取、網(wǎng)絡(luò)請(qǐng)求等任務(wù)得到了更多資源,加速了它們的執(zhí)行。
總結(jié)
“增加并發(fā)”在一定范圍內(nèi)可以作為啟動(dòng)優(yōu)化的方案,但在低端機(jī)上,CPU 已經(jīng)成為瓶頸,并發(fā)時(shí)異步線程對(duì)主線程的搶占也需要引起重視。
GCD 提供了四種 QoS 給開發(fā)者使用,官方也為這四種 QoS 提供了最佳實(shí)踐建議。
經(jīng)過(guò)評(píng)測(cè)和源碼推理,User-Interactive 和 User-Initiated 對(duì)主線程有明顯搶占,Utility 和 Background 對(duì)主線程的搶占極少。開發(fā)者創(chuàng)建的 GCD 隊(duì)列,默認(rèn)的 QoS 實(shí)際為 User-Initiated。因此在啟動(dòng)期間(或者任何耗時(shí)敏感期間),與啟動(dòng)無(wú)直接關(guān)系的 queue,應(yīng)該主動(dòng)設(shè)置為 Utility 或 Background,減少對(duì)主線程的搶占。
通過(guò)飛書上落地優(yōu)化,我們能得出結(jié)論:對(duì)線程或 GCD queue 調(diào)整 QoS,能在不改變啟動(dòng)業(yè)務(wù)邏輯的情況下取得顯著收益。
當(dāng)然,比事后優(yōu)化更好的操作,是在編碼時(shí)就充分了解不同 QoS 的行為特性,選用最適合的 QoS。
加入我們
字節(jié)跳動(dòng) APM 中臺(tái)目前致力于提升整個(gè)集團(tuán)內(nèi)全系產(chǎn)品的性能和穩(wěn)定性表現(xiàn),技術(shù)棧覆蓋 iOS/Android/Server/Web/Hybrid/PC/游戲/小程序等,工作內(nèi)容包括但不限于性能穩(wěn)定性監(jiān)控,問(wèn)題排查,深度優(yōu)化,防劣化等。長(zhǎng)期期望為業(yè)界輸出更多更有建設(shè)性的問(wèn)題發(fā)現(xiàn)和深度優(yōu)化手段。歡迎對(duì)字節(jié)APM 團(tuán)隊(duì)職位感興趣的同學(xué)投遞簡(jiǎn)歷到郵箱fengyadong@bytedance.com。