iOS事件分發(fā)機制介紹與應用
打開某App點擊登錄按鈕后彈出登錄頁面。這是一個事件分發(fā)與響應的示例。我們來探究下該過程。
介紹事件分發(fā)機制自然繞不開事件。
iOS包含以下幾種類型事件:
- 觸摸事件
- 按壓時間
- 搖動事件
- 遠程控制事件
- 編輯菜單時間
本文選擇以觸摸事件進行介紹。因為相對而言它包含的事件分發(fā)機制最全面。
iOS事件分發(fā)流程
iOS應用啟動后UIApplicationMain函數(shù)會創(chuàng)建一個UIApplication的單例。該單例會維護一個FIFO的隊列進行事件分發(fā)。系統(tǒng)檢測到觸摸事件后就會發(fā)給當前Application單例,由該單例進行派發(fā)。派發(fā)分為下面三個過程:
一、hitTest
UIWindow一旦接收到事件后就進行hit-test以查找哪個對象接收該事件。hitTest:withEvent方法用于發(fā)現(xiàn)在觸摸位置的視圖。pointInside:withEvent:用于檢測該點擊是否在視圖的邊界內(nèi)。hitTest:withEvent調(diào)用pointInside:withEvent:。
hitTest以遞歸的形式調(diào)用直至找到能處理觸摸事件的最頂部葉子視圖(一般就是手指點中區(qū)域所屬的視圖),那么該視圖就會被選中。稱為第一響應者。
做個類比就是:老板說我這有個事誰干給干了?經(jīng)過經(jīng)理等層層指派,大家都覺得小王合適。
該過程可以通過復寫下面方法斷點了解。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
具體情況如圖所示:

二、sendEvent
一旦確定第一響應者后,UIApplication單例就會發(fā)送相關(guān)觸摸事件到第一響應者。
就好比老本拍板說那就讓小王干了。
該過程可以斷點button的處理方法進行了解。如圖所示:

三、事件處理
針對觸摸事件的處理是重載如下方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
第一響應者接到事件后有以下三種選擇:
- 什么都不不處理(選擇不重載上述方法)
- 處理一部分,剩下的交由其它對象處理(重載處理后調(diào)用super)
- 獨自進行處理(重載處理并且不調(diào)用super)
如果第一響應者什么都不處理或者處理部分然后調(diào)用super,則事件將被發(fā)送到一個鏈式響應路徑中,即響應鏈。該事件遵循以下路徑進行轉(zhuǎn)發(fā):
- 第一響應者
- 第一響應者的父視圖
- 父視圖的父視圖,直至關(guān)聯(lián)的視圖控制器
- 視圖控制器的父視圖控制器,直至根視圖
- 根視圖下一響應者是window
- window的下一響應者是application
- 最后的響應者是App delegate
若轉(zhuǎn)發(fā)至App delegate,且App delegate未進行處理則該事件將被丟棄。
iOS處理方式
那么事件處理的方式有哪些?我們大致可以劃分為下面三類。
- UIControl的子類
- TouchEvent
- 觸摸相關(guān)手勢。
有可能一個對象同時實現(xiàn)了兩個或者更多處理方式。那么誰的優(yōu)先級高些呢?
這個我們可以做個類比,讓你實現(xiàn)判斷鼠標單機還是雙擊,那么檢測單擊的時間會比雙擊要長。因為雙擊失效后才會進行單擊判定。
同理手勢識別不了了,其它才有機會處理,UIControl子類也類似,那么優(yōu)先級如下所示:
觸摸相關(guān)手勢 > UIControl的子類 > TouchEvent
一般情況下,我們遇到的是手勢觸發(fā)了就不會繼續(xù)轉(zhuǎn)發(fā)事件了。
這就是為什么有人在UIButton中添加個tap手勢看起來正常(別笑)。
事件分發(fā)機制應用
事件分發(fā)不同場景方法選擇
更改事件分發(fā)流程,實現(xiàn)相關(guān)類的
hitTest:withEvent:方法為佳。擴大點擊范圍,覆寫
pointInside:withEvent:是個不錯的選擇。-
解決一寫如收起鍵盤等問題,可嘗試直接發(fā)送事件。如:
[[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil]; -
選擇讓父視圖或者關(guān)聯(lián)視圖控制器處理事件可以這么寫(雖然很少見有這么玩的):
[button addTarget:nil action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
具體應用場景
實現(xiàn)自定義懸浮窗
根據(jù)響應鏈規(guī)則,我們能更好的設(shè)計一個自定義懸浮窗。
- 懸浮窗作為UIWindow的rootController存在
- 覆寫
pointInside:withEvent:判斷事件所屬UIWindow - 完善自己的事件命中測試,實現(xiàn)視圖層級管理
詳情參加Flex寫法。
模擬點擊
通過了解分析事件分發(fā)過程,我們能很清楚的知道要實現(xiàn)模擬點擊我們只需把點擊事件發(fā)送給UIApplication單例的隊列就可以了。
- 創(chuàng)建UIEvent對象。
- 發(fā)送UIEvent對象
[[UIApplication sharedApplication] sendEvent:event]。
統(tǒng)計與修改UITouch
- hook UIApplication類的
- (void)sendEvent:(UIEvent *)event;方法 - 進行統(tǒng)計或修改
注:創(chuàng)建UITouch對象和對UITouch對象進行修改,可以參考kif-framework。