為什么 GUI 是單線程事件驅(qū)動(dòng)的?
不是沒人嘗試多線程的GUI 框架,只是最終都由于死鎖導(dǎo)致的穩(wěn)定性問題重新回到單線程的事件列隊(duì)模型。
多線程GUI 框架中更容易發(fā)生死鎖的一部分原因,是輸入事件的處理過程與GUI 組件的面相對(duì)象模型之間會(huì)存在互相沖突的交互,導(dǎo)致鎖定順序死鎖。
另外,一般 MVC 模式中model 和 view 之間的變化消息相互傳遞,也很容易造成鎖定順序死鎖。
一般來講,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù),執(zhí)行完成后線程就會(huì)退出。如果我們需要一個(gè)機(jī)制,讓線程能隨時(shí)處理事件但并不退出
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
我們需要一個(gè)事件循環(huán),在不同系統(tǒng)中這個(gè)循環(huán)叫做不同名字,有稱呼其為 EventLoop, Looper(Android),RunLoop(iOS),我們下面拿 iOS中的 RunLoop 繼續(xù)講述。
iOS
RunLoop 實(shí)際上就是一個(gè)對(duì)象,這個(gè)對(duì)象管理了其需要處理的事件和消息,并提供了一個(gè)入口函數(shù)來執(zhí)行上面的邏輯。
線程執(zhí)行了這個(gè)函數(shù)后,就會(huì)一直處于這個(gè)函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中,直到這個(gè)循環(huán)結(jié)束(比如傳入 quit 的消息),函數(shù)返回。
線程和 RunLoop 之間是一一對(duì)應(yīng)的,其關(guān)系是保存在一個(gè)全局的 Dictionary 里。線程剛創(chuàng)建時(shí)并沒有 RunLoop,如果你不主動(dòng)獲取,那它一直都不會(huì)有。
RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時(shí),RunLoop 的自動(dòng)銷毀是發(fā)生在線程結(jié)束時(shí)。
iOS 的顯示系統(tǒng)是由 VSync 信號(hào)驅(qū)動(dòng)的,VSync 信號(hào)由硬件時(shí)鐘生成,每秒鐘發(fā)出 60 次。iOS 圖形服務(wù)接收到 VSync 信號(hào)后,會(huì)通過 IPC 通知到 App 內(nèi)。
在 VSync 信號(hào)到來后,系統(tǒng)圖形服務(wù)會(huì)通過 CADisplayLink 等機(jī)制通知 App,App 主線程開始在 CPU 中計(jì)算顯示內(nèi)容,比如視圖的創(chuàng)建、布局計(jì)算、圖片解碼、文本繪制等。隨后 CPU 會(huì)將計(jì)算好的內(nèi)容提交到 GPU 去,由 GPU 進(jìn)行變換、合成、渲染。隨后 GPU 會(huì)把渲染結(jié)果提交到幀緩沖區(qū)去,等待下一次 VSync 信號(hào)到來時(shí)顯示到屏幕上。由于垂直同步的機(jī)制,如果在一個(gè) VSync 時(shí)間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交,則那一幀就會(huì)被丟棄,等待下一次機(jī)會(huì)再顯示,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變。這就是界面卡頓的原因。
Core Animation 在 RunLoop 中注冊(cè)了一個(gè) Observer,監(jiān)聽了 BeforeWaiting 和 Exit 事件。這個(gè) Observer 的優(yōu)先級(jí)是 2000000,低于常見的其他 Observer。
當(dāng)一個(gè)觸摸事件到來時(shí),RunLoop 被喚醒,App 中的代碼會(huì)執(zhí)行一些操作,比如創(chuàng)建和調(diào)整視圖層級(jí)、設(shè)置 UIView 的 frame、修改 CALayer 的透明度、為視圖添加一個(gè)動(dòng)畫;這些操作最終都會(huì)被 CALayer 捕獲,并通過 CATransaction 提交到一個(gè)中間狀態(tài)去。當(dāng)上面所有操作結(jié)束后,RunLoop 即將進(jìn)入休眠(或者退出)時(shí),關(guān)注該事件的 Observer 都會(huì)得到通知。這時(shí) CA 注冊(cè)的那個(gè) Observer 就會(huì)在回調(diào)中,把所有的中間狀態(tài)合并提交到 GPU 去顯示;如果此處有動(dòng)畫,CA 會(huì)通過 DisplayLink 等機(jī)制多次觸發(fā)相關(guān)流程。
和安卓一樣,都是先處理 input 相關(guān)事件
系統(tǒng)實(shí)現(xiàn)中使用
事件響應(yīng)
蘋果注冊(cè)了一個(gè) Source1 (基于 mach port 的) 用來接收系統(tǒng)事件,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。
當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個(gè) IOHIDEvent 事件并由 SpringBoard 接收。這個(gè)過程的詳細(xì)情況可以參考這里。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進(jìn)程。隨后蘋果注冊(cè)的那個(gè) Source1 就會(huì)觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue() 進(jìn)行應(yīng)用內(nèi)部的分發(fā)。
_UIApplicationHandleEventQueue() 會(huì)把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā),其中包括識(shí)別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等。通常事件比如 UIButton 點(diǎn)擊、touchesBegin/Move/End/Cancel 事件都是在這個(gè)回調(diào)中完成的。
手勢(shì)識(shí)別
當(dāng)上面的 _UIApplicationHandleEventQueue() 識(shí)別了一個(gè)手勢(shì)時(shí),其首先會(huì)調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對(duì)應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理。
蘋果注冊(cè)了一個(gè) Observer 監(jiān)測(cè) BeforeWaiting (Loop即將進(jìn)入休眠) 事件,這個(gè)Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver(),其內(nèi)部會(huì)獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。
當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時(shí),這個(gè)回調(diào)都會(huì)進(jìn)行相應(yīng)處理。
界面更新
當(dāng)在操作 UI 時(shí),比如改變了 Frame、更新了 UIView/CALayer 的層次時(shí),或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個(gè) UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個(gè)全局的容器去。(取代了安卓中的 RootViewImpl角色)
蘋果注冊(cè)了一個(gè) Observer 監(jiān)聽 BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件,回調(diào)去執(zhí)行一個(gè)很長(zhǎng)的函數(shù):
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個(gè)函數(shù)里會(huì)遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整,并更新 UI 界面。這里是生產(chǎn)者,CADisplayLink 是消費(fèi)者。
函數(shù)調(diào)用棧:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
定時(shí)器
CADisplayLink 是一個(gè)和屏幕刷新率一致的定時(shí)器(其內(nèi)部實(shí)際是操作了一個(gè) Source)。如果在兩次屏幕刷新之間執(zhí)行了一個(gè)長(zhǎng)任務(wù),那其中就會(huì)有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動(dòng)TableView時(shí),即使一幀的卡頓也會(huì)讓用戶有所察覺。Facebook 開源的 AsyncDisplayLink 就是為了解決界面卡頓的問題,其內(nèi)部也用到了 RunLoop。
Android
Android 中的 VSync信號(hào)處理,是由Choreographer 來實(shí)現(xiàn)的。
Choreographer是線程單例的,而且必須要和一個(gè)Looper綁定,因?yàn)槠鋬?nèi)部有一個(gè)Handler需要和Looper綁定。
DisplayEventReceiver是一個(gè)abstract class,其JNI的代碼部分會(huì)創(chuàng)建一個(gè)IDisplayEventConnection的VSYNC監(jiān)聽者對(duì)象。這樣,來自EventThread的VSYNC中斷信號(hào)就可以傳遞給Choreographer對(duì)象了。由圖8可知,當(dāng)VSYNC信號(hào)到來時(shí),DisplayEventReceiver的onVsync函數(shù)將被調(diào)用。
另外,DisplayEventReceiver還有一個(gè)scheduleVsync函數(shù)。當(dāng)應(yīng)用需要繪制UI時(shí),將首先申請(qǐng)一次VSYNC中斷,然后再在中斷處理的onVsync函數(shù)去進(jìn)行繪制。
Choreographer定義了一個(gè)FrameCallback interface,每當(dāng)VSYNC到來時(shí),其doFrame函數(shù)將被調(diào)用。這個(gè)接口對(duì)Android Animation的實(shí)現(xiàn)起了很大的幫助作用。以前都是自己控制時(shí)間,現(xiàn)在終于有了固定的時(shí)間中斷。
Choreographer的主要功能是,當(dāng)收到VSYNC信號(hào)時(shí),去調(diào)用使用者通過postCallback設(shè)置的回調(diào)函數(shù)。目前一共定義了三種類型的回調(diào),它們分別是:
CALLBACK_INPUT:優(yōu)先級(jí)最高,和輸入事件處理有關(guān)。
CALLBACK_ANIMATION:優(yōu)先級(jí)其次,和Animation的處理有關(guān)。
CALLBACK_TRAVERSAL:優(yōu)先級(jí)最低,和UI等控件繪制有關(guān)。
優(yōu)先級(jí)高低和處理順序有關(guān)。當(dāng)收到VSYNC中斷時(shí),Choreographer將首先處理INPUT類型的回調(diào),然后是ANIMATION類型,最后才是TRAVERSAL類型。
為什么 Input 優(yōu)先級(jí)最高?因?yàn)?Input 事件最有可能引發(fā)界面變化.
Choreographer是Android 4.1中的新事物,下面將通過一個(gè)實(shí)例來簡(jiǎn)單介紹Choreographer的工作原理。
假如UI中有一個(gè)控件invalidate了,那么它將觸發(fā)ViewRootImpl的invalidate函數(shù),該函數(shù)將最終調(diào)用ViewRootImpl的scheduleTraversals
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
scheduleTraversals首先禁止了后續(xù)的消息處理功能,這是由設(shè)置Looper的postSyncBarrier來完成的。一旦設(shè)置了SyncBarrier,所有非Asynchronous的消息便將停止派發(fā)。
然后,為Choreographer設(shè)置了CALLBACK類型為TRAVERSAL的處理對(duì)象,即mTraversalRunnable。
最后調(diào)用scheduleConsumeBatchedInput,這個(gè)函數(shù)將為Choreographer設(shè)置了CALLBACK類型為INPUT的處理對(duì)象。
Choreographer的postCallback函數(shù)將會(huì)申請(qǐng)一次VSYNC中斷(通過調(diào)用DisplayEventReceiver的scheduleVsync實(shí)現(xiàn))。當(dāng)VSYNC信號(hào)到達(dá)時(shí),Choreographer doFrame函數(shù)被調(diào)用,內(nèi)部代碼會(huì)觸發(fā)回調(diào)處理。
安卓的代碼是開源的,如果感興趣,可以點(diǎn)擊這里scheduleTraversals去參看對(duì)應(yīng)的邏輯