當(dāng)手指輕觸屏幕,整個系統(tǒng)像沉睡的生靈突然被驚醒,然后經(jīng)歷過腥風(fēng)血雨的一段奇幻旅行,最終又歸于沉寂。
整個iOS觸摸事件從產(chǎn)生到寂滅大致如下圖:

系統(tǒng)響應(yīng)階段
- 手指觸摸屏幕,屏幕硬件感應(yīng)到輸入事件并交由IOKit驅(qū)動處理;
I/O Kit是用于創(chuàng)建設(shè)備驅(qū)動程序的系統(tǒng)框架、庫、工具和其它資源的集合,基于受限的c++形式(主要是繼承和重載)實現(xiàn)面向?qū)ο蟮木幊棠P停喕嗽O(shè)備驅(qū)動傳給你續(xù)開發(fā)的過程。相關(guān)的驅(qū)動開發(fā)命令行工具:
kextload/kextunload、kextstat、kextcache、iostat(顯示終端、磁盤和cpu操作的內(nèi)核i/o統(tǒng)計信息)、ioalloccount、gcc/gdb等。
- IOKit用戶空間框架IOKit.framework將觸摸事件封裝成一個IOHIDEvent對象,并通過mach port傳遞給SpringBoard.app進程;
SpringBoard.app 是 iOS 和 iPadOS 負責(zé)管理主屏幕的基礎(chǔ)程序,并在設(shè)備啟動時啟動 WindowServer、開啟應(yīng)用程序(實現(xiàn)該功能等程序稱為應(yīng)用啟動器)和對設(shè)備進行某些設(shè)置。有時候主屏幕也被作為 SpringBoard 的代稱。主要處理按鍵(鎖屏/靜音等)、觸摸、加速、距離傳感器等幾種事件,隨后通過
mac port進程間通信轉(zhuǎn)發(fā)至需要的APP。
Mac OSX中使用的是Launchpad,能讓用戶以從類似于iOS的SpringBoard的界面按一下圖示來啟動應(yīng)用程式。在啟動臺推出之前,用戶能以Dock、Finder、Spotlight或終端啟動應(yīng)用。不過 Launchpad 并不會占據(jù)整個主屏幕,而更像是一個 Space(類似于儀表板)。
桌面響應(yīng)階段
- SpringBoard.app進程主線程RunLoop收到IOKit.framework傳遞來的消息蘇醒,并觸發(fā)對應(yīng)mach port的
Source1回調(diào)__IOHIDEventSystemClientQueueCallback(); - SpringBoard.app進程判斷桌面是否存在前臺應(yīng)用,若有則直接轉(zhuǎn)發(fā)給前臺應(yīng)用;若無(如處于桌面翻頁),則觸發(fā)SpringBoard.app應(yīng)用內(nèi)部主線程RunLoop的
Source0事件回調(diào),由桌面應(yīng)用內(nèi)部消耗;
APP響應(yīng)階段
- 應(yīng)用啟動時會開啟
com.apple.uikit.eventfetch-thread線程RunLoop并注冊souce1類型事件,用于接收SpringBoard.app發(fā)送的mach port source1消息; -
com.apple.uikit.eventfetch-thread線程接收到source1消息后,執(zhí)行__IOHIDEventSystemClientQueueCallback回調(diào),并將main runloop中__handleEventQueue所對應(yīng)的source0事件設(shè)置為signalled=Yes狀態(tài),同時喚醒主線程runloop,主線程則調(diào)用__handleEventQueue來進行事件隊列的處理,如下圖所示:
UITouch調(diào)用棧
主線程RunLoop中與事件相關(guān)的關(guān)鍵事件源為:
<CFRunLoopSource 0x600001608240 [0x7fff8062ce40]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x6000018101a0, callout = __handleEventQueue (0x7fff48c64d04)}}
其回調(diào)函數(shù)為__handleEventQueue,類型為source0,因此主線程不處理SpringBoard.app進程的發(fā)送的事件接收;
com.apple.uikit.eventfetch-thread線程runloop如下:

該線程DefaultMode與CommonMode均包含source1其回調(diào)為__IOHIDEventSystemClientQueueCallback;
- 事件隊列處理是將觸摸事件添加到
UIApplication對象的事件隊列中,事件出隊后,UIApplication開始尋找最佳響應(yīng)者的過程Hit-Testing,過程如下:
大致的流程即是事件自下往上傳遞遞歸詢問子視圖能否響應(yīng)事件的過程,其中UIWindow繼承自UIView也可作為視圖,且若同一層級則后添加的子視圖優(yōu)先級高(對于UIWindow而言后顯示的UIWindow優(yōu)先級高),具體的流程如下:

-
UIApplication將UIEvent事件傳遞給窗口對象UIWindow,若存在多個同層級的UIWindow,則后顯示的優(yōu)先級高,即頂層的窗口優(yōu)先級高,視圖優(yōu)先級等同; - 若窗口不能響應(yīng)事件,則將事件傳遞給其他窗口;若窗口能響應(yīng)事件,則從窗口子視圖自下往上詢問能否響應(yīng)事件;
- 若能響應(yīng)事件就繼續(xù)子視圖自下往上傳遞詢問,直至沒有能響應(yīng)的子視圖為止,則自身就是最適合的響應(yīng)者;
對于上述能否響應(yīng)事件是通過UIView對象的hitTest:withEvent方法來判定,具體的規(guī)則如下:
-
若當(dāng)前視圖無法響應(yīng)事件,則返回nil;
無法響應(yīng)事件的幾種狀態(tài)如下:
- 不允許交互:
userInteractionEnabled = NO; - 隱藏:
hidden = YES如果父視圖隱藏,那么子視圖也會隱藏,隱藏的視圖無法接收時間; - 透明度:
alphs < 0.01如果設(shè)置的視圖透明度<0.01,會直接影響子視圖的透明度,即子視圖也透明不會接收事件;
- 不允許交互:
若當(dāng)前視圖可以響應(yīng)事件,但子視圖可以響應(yīng)事件,則返回自身作為當(dāng)前視圖層次中的事件接收者;
若當(dāng)前視圖可以響應(yīng)事件,同時有子視圖可以響應(yīng),則返回子視圖層次中的事件響應(yīng)者;
hitTest:withEvent調(diào)用棧如下圖:

大致的代碼邏輯如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//3種狀態(tài)無法響應(yīng)事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
//觸摸點若不在當(dāng)前視圖上則無法響應(yīng)事件
if ([self pointInside:point withEvent:event] == NO) return nil;
//從后往前遍歷子視圖數(shù)組
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--)
{
// 獲取子視圖
UIView *childView = self.subviews[i];
// 坐標(biāo)系的轉(zhuǎn)換,把觸摸點在當(dāng)前視圖上坐標(biāo)轉(zhuǎn)換為在子視圖上的坐標(biāo)
CGPoint childP = [self convertPoint:point toView:childView];
//詢問子視圖層級中的最佳響應(yīng)視圖
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView)
{
//如果子視圖中有更合適的就返回
return fitView;
}
}
//沒有在子視圖中找到更合適的響應(yīng)視圖,那么自身就是最合適的
return self;
}
其中pointInside:withEvent方法用于判定觸摸點是否在自身坐標(biāo)范圍內(nèi),默認實現(xiàn)是若在坐標(biāo)范圍內(nèi)則返回YES,否則返回NO。因此,可通過重寫UIView的hitTest:withEvent和pointInside:withEvent方法來修改事件的流向。
- 尋找到最佳響應(yīng)者后,
UIApplication會通過sendEvent:將事件傳遞給事件所屬的UIWindow,UIWindow同樣通過sendEvent:再將事件傳遞給hit-tested view最佳響應(yīng)者,過程如下:
事件響應(yīng)流動
緊接著就是事件的響應(yīng),具體就是如下的方法調(diào)用:
//手指觸碰屏幕,觸摸開始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移動
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指離開屏幕,觸摸結(jié)束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//觸摸結(jié)束前,某個系統(tǒng)事件中斷了觸摸,例如電話呼入,手勢識別成功后取消
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
其中每個響應(yīng)觸摸事件的方法都會接收兩個參數(shù),分別對應(yīng)觸摸對象集合touches和事件對象UIEvent;
對于hit-tested view最佳響應(yīng)者對象擁有響應(yīng)事件的最高優(yōu)先級及絕對控制權(quán):可以獨占該事件,也可以將該事件往下傳遞,即事件的傳遞(響應(yīng)鏈),具體的響應(yīng)鏈操作方式如下:
-
不攔截,默認操作
事件會自動沿著默認的響應(yīng)鏈往下傳遞
-
攔截,不再往下分發(fā)事件
重寫
touchesBegan:withEvent:進行事件處理,不調(diào)用父類的touchesBegan:withEvent:; -
攔截,繼續(xù)往下分發(fā)事件
重寫
touchesBegan:withEvent:進行事件處理,同時調(diào)用父類的touchesBegan:withEvent將事件往下傳遞;
需要注意的是,事件自下往上的傳遞與此處事件往下傳遞不同,此處事件往下傳遞為事件的響應(yīng),而前面事件自下往上傳遞為查找最佳響應(yīng)者,前者為“尋找”,后者為“響應(yīng)”。
響應(yīng)鏈關(guān)系如下圖所示:

繼承自UIResponder的響應(yīng)者對象都可以響應(yīng)事件,如UIView、UIViewController、UIWindow、UIApplication,每個響應(yīng)者對象都有一個nextResponder方法,用于獲取響應(yīng)鏈中當(dāng)前對象的下一個響應(yīng)者對象,默認的nextResponder實現(xiàn)如下:
UIView
若視圖是控制器的根視圖,則其nextResponder為控制器對象;否則,其nextResponder為父視圖。UIViewController
若控制器的視圖是window的根視圖,則其nextResponder為窗口對象;若控制器是從別的控制器present出來的,則其nextResponder為presenting view controller。UIWindow
nextResponder為UIApplication對象。UIApplication
若當(dāng)前應(yīng)用的app delegate是一個UIResponder對象,且不是UIView、UIViewController或app本身,則UIApplication的nextResponder為app delegate。
打印響應(yīng)鏈對象可通過如下實現(xiàn):
- (void)printResponderChain
{
UIResponder *responder = self;
printf("%s",[NSStringFromClass([responder class]) UTF8String]);
while (responder.nextResponder) {
responder = responder.nextResponder;
printf(" --> %s",[NSStringFromClass([responder class]) UTF8String]);
}
}
-
若視圖存在手勢識別器,由于手勢識別器比
UIResponder對象具有更高的事件響應(yīng)優(yōu)先級,則UIWindow優(yōu)先將事件傳遞給手勢識別器,再傳給hit-tested view,一旦手勢識別器成功識別了手勢,UIApplication就會取消hit-tesed view對事件的響應(yīng),且后續(xù)不再收到事件;若手勢識別器未能識別手勢且觸摸并未結(jié)束,則停止向手勢識別器發(fā)送事件,僅向hit-tested view發(fā)送事件;若手勢識別器選項
cancelsTouchesInView = NO(默認為YES),則表示手勢識別器成功識別手勢后事件依舊會傳遞給hit-tested view;delayTouchesBegan = YES(默認為NO),則表示手勢識別器在識別手勢期間,截斷事件,即不會將事件發(fā)送給hit-tested view;delayTouchesEnded = NO(默認為YES),則表示手勢識別器失敗時會立即通知UIApplication對象發(fā)送狀態(tài)為end的UITouch事件給hit-tested view以調(diào)用touchEnded:withEvent結(jié)束事件響應(yīng); -
若視圖中存在繼承自
UIView的UIControl對象,如UIButton、UISegmentedControl、UISwitch等控件,當(dāng)UIControl跟蹤到觸摸事件時,會向其上添加的target發(fā)送事件以執(zhí)行action。因
UIControl繼承于UIView,故也具備UIResponder的事件處理,但其方法跟蹤有所不同,如下:- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event; - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event; - (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event; - (void)cancelTrackingWithEvent:(nullable UIEvent *)event;事實上,
UIControl的上述方法是在UITouch方法內(nèi)部調(diào)用的,比如beginTrackingWithTouch是在touchesBegan方法內(nèi)部調(diào)用。當(dāng)UIControl跟蹤事件的過程中,識別出事件交互符合響應(yīng)條件,就會觸發(fā)target-action進行響應(yīng)。事實上,
UIControl監(jiān)聽到需要處理的交互事件時,會調(diào)用sendAction:to:forEvent:將target、action、event對象發(fā)送給UIApplication對象,UIApplication對象再通過sendAction:to:from:forEvent:向target發(fā)送action,因此,可以重寫上述方法來自定義事件執(zhí)行的target及action。對于
UIControl添加手勢識別器的情況,無法響應(yīng)target-action事件;

