在開發(fā)中,如果我們在后臺(tái)線程中對(duì)UI進(jìn)行操作,比如imageView.image = image;那么編譯器就會(huì)彈出一個(gè)runtime錯(cuò)誤,這時(shí),我們只需要把這一行代碼放到主線程中執(zhí)行,那就可以解決問題了,但是為什么要在主線程中操作UI才是正確的呢?
一般分為三個(gè)原因
- UIKit是一個(gè)
線程不安全的類,UI操作涉及到渲染訪問各種View對(duì)象的屬性,如果異步操作下會(huì)存在讀寫問題,而為其加鎖則會(huì)耗費(fèi)大量資源并拖慢運(yùn)行速度。 - 另一方面因?yàn)檎麄€(gè)程序的起點(diǎn)
UIApplication是在主線程進(jìn)行初始化,所有的用戶事件都是在主線程上進(jìn)行傳遞(如點(diǎn)擊、拖動(dòng)),所以view只能在主線程上才能對(duì)事件進(jìn)行響應(yīng)。 - 而在渲染方面由于圖像的渲染需要以60幀的刷新率在屏幕上同時(shí)更新,在非主線程異步化的情況下無法確定這個(gè)處理過程能夠?qū)崿F(xiàn)同步更新。
先從UIKit線程不安全說起
在UIKit中,很多類中大部分的屬性都被修飾為nonatomic,這意味著它們不能在多線程的環(huán)境下工作,而對(duì)于UIKit這樣一個(gè)龐大的框架,將其所有屬性都設(shè)計(jì)為線程安全是不現(xiàn)實(shí)的,這可不僅僅是簡單的將nonatomic改成atomic或者是加鎖解鎖的操作,還涉及到很多的方面:
- 假設(shè)能夠異步設(shè)置view的屬性,那我們究竟是希望這些改動(dòng)能夠同時(shí)生效,還是按照各自runloop的進(jìn)度去改變這個(gè)view的屬性呢?
- 假設(shè)
UITableView在其他線程去移除了一個(gè)cell,而在另一個(gè)線程卻對(duì)這個(gè)cell所在的index進(jìn)行一些操作,這時(shí)候可能就會(huì)引發(fā)crash。 - 如果在后臺(tái)線程移除了一個(gè)view,這個(gè)時(shí)候runloop周期還沒有完結(jié),用戶在主線程點(diǎn)擊了這個(gè)“將要”消失的view,那么究竟該不該響應(yīng)事件?在哪條線程進(jìn)行響應(yīng)?
仔細(xì)思考,似乎能夠多線程處理UI并沒有給我們開發(fā)帶來更多的便利,假如你代入了這些情景進(jìn)行思考,你很容易得出一個(gè)結(jié)論: “我在一個(gè)串行隊(duì)列對(duì)這些事件進(jìn)行處理就可以了?!?蘋果也是這樣想的,所以UIKit的所有操作都要放到主線程串行執(zhí)行。
在Thread-Safe Class Design一文提到
It’s a conscious design decision from Apple’s side to not have UIKit be thread-safe. Making it thread-safe wouldn’t buy you much in terms of performance; it would in fact make many things slower. And the fact that UIKit is tied to the main thread makes it very easy to write concurrent programs and use UIKit. All you have to do is make sure that calls into UIKit are always made on the main thread.
大意為把UIKit設(shè)計(jì)成線程安全并不會(huì)帶來太多的便利,也不會(huì)提升太多的性能表現(xiàn),甚至?xí)驗(yàn)榧渔i解鎖而耗費(fèi)大量的時(shí)間。事實(shí)上并發(fā)編程也沒有因?yàn)閁IKit是線程不安全而變得困難,我們所需要做的只是要確保UI操作在主線程進(jìn)行就可以了。
Runloop 與繪圖循環(huán)
解釋上面三個(gè)原因中的第二個(gè)原因 :
- 因?yàn)檎麄€(gè)程序的起點(diǎn)
UIApplication是在主線程進(jìn)行初始化,所有的用戶事件都是在主線程上進(jìn)行傳遞(如點(diǎn)擊、拖動(dòng)),所以view只能在主線程上才能對(duì)事件進(jìn)行響應(yīng)。
UIApplication在主線程所初始化的Runloop我們稱為Main Runloop,它負(fù)責(zé)處理app存活期間的大部分事件,如用戶交互等,它一直處于不斷處理事件和休眠的循環(huán)之中,以確保能盡快的將用戶事件傳遞給GPU進(jìn)行渲染,使用戶行為能夠得到響應(yīng),畫面之所以能夠得到不斷刷新也是因?yàn)?code>Main Runloop在驅(qū)動(dòng)著。
而每一個(gè)view的變化的修改并不是立刻變化,相反的會(huì)在當(dāng)前runloop的結(jié)束的時(shí)候統(tǒng)一進(jìn)行重繪,這樣設(shè)計(jì)的目的是為了能夠在一個(gè)runloop里面處理好所有需要變化的view,包括resize、hide、reposition等等,所有view的改變都能在同一時(shí)間生效,這樣能夠更高效的處理繪制,這個(gè)機(jī)制被稱為繪圖循環(huán)(View Drawing Cycle)。
因?yàn)辄c(diǎn)擊等用戶交互事件是由系統(tǒng)傳遞給UIApplication中,并在Main Runloop中進(jìn)行處理與響應(yīng)。這時(shí)假設(shè)UI可以在后臺(tái)線程中進(jìn)行處理,而Main Runloop并沒有收集到這個(gè)UI的相關(guān)事件,那么當(dāng)刷新畫面時(shí),有可能出現(xiàn)畫面刷新完,但是UI在后臺(tái)線程中的Runloop還沒結(jié)束,即這個(gè)UI還沒有被渲染,那么就會(huì)出現(xiàn)點(diǎn)擊沒反應(yīng),這時(shí)用戶應(yīng)該是在吐槽你的app了
iOS的渲染流程
解釋第三個(gè)原因
- 而在渲染方面由于圖像的渲染需要以60幀的刷新率在屏幕上同時(shí)更新,在非主線程異步化的情況下無法確定這個(gè)處理過程能夠?qū)崿F(xiàn)同步更新。
渲染框架

- UIKit: 包含各種控件,負(fù)責(zé)對(duì)用戶操作事件的響應(yīng),本身并不提供渲染的能力
- Core Animation: 負(fù)責(zé)所有視圖的繪制、顯示與動(dòng)畫效果
- OpenGL ES: 提供2D與3D渲染服務(wù)
- Core Graphics: 提供2D渲染服務(wù)
- Graphics Hardware: 指GPU
在iOS中,所有視圖的現(xiàn)實(shí)與動(dòng)畫本質(zhì)上是由 Core Animation 負(fù)責(zé),而不是UIKit。
Core Animation Pipeline 流水線

Core Animation的繪制是通過Core Animation Pipeline實(shí)現(xiàn),它以流水線的形式進(jìn)行渲染,具體分為四個(gè)步驟:
Commit Transaction:
可以細(xì)分為
1.Layout: 構(gòu)建視圖布局如addSubview等操作
2.Display: 重載drawRect:進(jìn)行時(shí)圖繪制,該步驟使用CPU與內(nèi)存
3.Prepare: 主要處理圖像的解碼與格式轉(zhuǎn)換等操作
4.Commit: 將Layer遞歸打包并發(fā)送到Render ServerRender Server:
負(fù)責(zé)渲染工作,會(huì)解析上一步Commit Transaction中提交的信息并反序列化成渲 染樹(render tree),隨后根據(jù)layer的各種屬性生成繪制指令,并在下一次VSync信號(hào)到來時(shí)調(diào)用OpenGL進(jìn)行渲染。GPU:
GPU會(huì)等待顯示器的VSync信號(hào)發(fā)出后才進(jìn)行OpenGL渲染管線,將3D幾何數(shù)據(jù)轉(zhuǎn)化成2D的像素圖像和光柵處理,隨后進(jìn)行新的一幀的渲染,并將其輸出到緩沖區(qū)。Dispaly:
從緩沖區(qū)中取出畫面,并輸出到屏幕上。
VSync:
VSync(vertical sync)是指垂直同步,在玩游戲的時(shí)候在設(shè)置的時(shí)候應(yīng)該會(huì)看見過這個(gè)選項(xiàng),這個(gè)機(jī)制能夠讓顯卡和顯示器保持在一個(gè)相同的刷新率從而避免畫面撕裂。在iOS中,屏幕具有60Hz的刷新率,這意味著它每秒需要顯示60張不同的圖片(幀),但GPU并沒有一個(gè)確定的刷新率,在某些時(shí)候GPU可能被要求更強(qiáng)力的數(shù)據(jù)輸出來確保渲染能力,這時(shí)候他們可能比屏幕刷新率(60Hz)更快,就會(huì)導(dǎo)致屏幕不能完整的渲染所有GPU給他的數(shù)據(jù),因?yàn)樗粔蚩欤聊坏纳弦粠€沒渲染完,下一幀就已經(jīng)到來了,這就導(dǎo)致畫面的撕裂。
這個(gè)時(shí)候我們就要引入VSync了,簡單來說它就是讓顯卡保持他的輸出速率不高于屏幕的刷新率,啟用了VSync后,GPU不再會(huì)給你可憐的60Hz屏幕每秒發(fā)送100幀了,它會(huì)增加每一幀的發(fā)送間隔,確保顯示器能夠有充足的時(shí)間去處理每一幀。
相信大家都會(huì)遇到過應(yīng)用卡頓,卡頓的原因就是因?yàn)閮蓭乃⑿聲r(shí)間間隔大于60幀每秒(約16.67ms),導(dǎo)致用戶感覺點(diǎn)擊或者滑動(dòng)時(shí),界面沒有及時(shí)的響應(yīng)。
前面提到Core Animation Pipeline是以流水線的形式工作的,在理想的狀況下我們希望它能夠在1/60s內(nèi)完成圖層樹的準(zhǔn)備工作并提交給渲染進(jìn)程,而渲染進(jìn)程在下一次VSync信號(hào)到來的時(shí)候提交給GPU進(jìn)行渲染,并在1/60s內(nèi)完成渲染,這樣就不會(huì)產(chǎn)生任何的卡頓。
假設(shè)UI可以在后臺(tái)線程中操作,那么在runloop的結(jié)尾準(zhǔn)備進(jìn)行渲染的時(shí)候,不同線程提交了不同的渲染信息,于是我們就擁有了更多的繪制事務(wù),這個(gè)時(shí)候Core Animation Pipeline會(huì)不斷將信息提交,讓GPU進(jìn)行渲染,由于繪制事件的不同步導(dǎo)致了GPU渲染的不同步,可能在上一幀是需要渲染一個(gè)label消失的畫面,下一幀卻又需要渲染這個(gè)label改變了文字,最終導(dǎo)致的是界面的不同步。
參考資料
https://juejin.cn/post/6844903763011076110
https://m.mydrivers.com/newsview/623458.html