細數(shù)iOS觸摸事件流動

當(dāng)手指輕觸屏幕,整個系統(tǒng)像沉睡的生靈突然被驚醒,然后經(jīng)歷過腥風(fēng)血雨的一段奇幻旅行,最終又歸于沉寂。

整個iOS觸摸事件從產(chǎn)生到寂滅大致如下圖:


觸摸事件生命周期

系統(tǒng)響應(yīng)階段

  1. 手指觸摸屏幕,屏幕硬件感應(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/kextunloadkextstat、kextcache、iostat(顯示終端、磁盤和cpu操作的內(nèi)核i/o統(tǒng)計信息)、ioalloccount、gcc/gdb等。

  1. IOKit用戶空間框架IOKit.framework將觸摸事件封裝成一個IOHIDEvent對象,并通過mach port傳遞給SpringBoard.app進程;

SpringBoard.app 是 iOSiPadOS 負責(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)階段

  1. SpringBoard.app進程主線程RunLoop收到IOKit.framework傳遞來的消息蘇醒,并觸發(fā)對應(yīng)mach port的Source1回調(diào)__IOHIDEventSystemClientQueueCallback()
  2. 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)階段

  1. 應(yīng)用啟動時會開啟com.apple.uikit.eventfetch-thread線程RunLoop并注冊souce1類型事件,用于接收SpringBoard.app發(fā)送的mach port source1消息;
  2. 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如下:

eventfetch thread runloop對象

該線程DefaultModeCommonMode均包含source1其回調(diào)為__IOHIDEventSystemClientQueueCallback;

  1. 事件隊列處理是將觸摸事件添加到UIApplication對象的事件隊列中,事件出隊后,UIApplication開始尋找最佳響應(yīng)者的過程Hit-Testing,過程如下:

大致的流程即是事件自下往上傳遞遞歸詢問子視圖能否響應(yīng)事件的過程,其中UIWindow繼承自UIView也可作為視圖,且若同一層級則后添加的子視圖優(yōu)先級高(對于UIWindow而言后顯示的UIWindow優(yōu)先級高),具體的流程如下:

事件傳遞流動

  • UIApplicationUIEvent事件傳遞給窗口對象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)用棧如下圖:

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:withEventpointInside:withEvent方法來修改事件的流向。

  1. 尋找到最佳響應(yīng)者后,UIApplication會通過sendEvent:將事件傳遞給事件所屬的UIWindowUIWindow同樣通過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)系如下圖所示:


響應(yīng)鏈關(guān)系

繼承自UIResponder的響應(yīng)者對象都可以響應(yīng)事件,如UIViewUIViewController、UIWindowUIApplication,每個響應(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]);
    }
}
  1. 若視圖存在手勢識別器,由于手勢識別器比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)為endUITouch事件給hit-tested view以調(diào)用touchEnded:withEvent結(jié)束事件響應(yīng);

  2. 若視圖中存在繼承自UIViewUIControl對象,如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í)行的targetaction。

    對于UIControl添加手勢識別器的情況,無法響應(yīng)target-action事件;

Reference

  1. iOS觸摸事件的流動
  2. iOS 事件處理機制與圖像渲染過程
  3. iOS Rendering 渲染全解析
  4. iOS觸摸事件全家桶
  5. Using Responders and the Responder Chain to Handle Events
  6. SpringBoard
  7. /System/Library/CoreServices/SpringBoard.app
  8. IOKit Fundamentals
  9. iOS RunLoop完全指南
  10. 《OS X與iOS內(nèi)核編程》
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容