iOS事件分發(fā)機(jī)制介紹與應(yīng)用
打開某App點(diǎn)擊登錄按鈕后彈出登錄頁面娇妓。這是一個(gè)事件分發(fā)與響應(yīng)的示例。我們來探究下該過程逗鸣。
介紹事件分發(fā)機(jī)制自然繞不開事件乍构。
iOS包含以下幾種類型事件:
- 觸摸事件
- 按壓時(shí)間
- 搖動事件
- 遠(yuǎn)程控制事件
- 編輯菜單時(shí)間
本文選擇以觸摸事件進(jìn)行介紹。因?yàn)橄鄬Χ运氖录职l(fā)機(jī)制最全面柳譬。
iOS事件分發(fā)流程
iOS應(yīng)用啟動后UIApplicationMain
函數(shù)會創(chuàng)建一個(gè)UIApplication的單例喳张。該單例會維護(hù)一個(gè)FIFO
的隊(duì)列進(jìn)行事件分發(fā)。系統(tǒng)檢測到觸摸事件后就會發(fā)給當(dāng)前Application單例美澳,由該單例進(jìn)行派發(fā)销部。派發(fā)分為下面三個(gè)過程:
一摸航、hitTest
UIWindow一旦接收到事件后就進(jìn)行hit-test以查找哪個(gè)對象接收該事件。hitTest:withEvent
方法用于發(fā)現(xiàn)在觸摸位置的視圖舅桩。pointInside:withEvent:
用于檢測該點(diǎn)擊是否在視圖的邊界內(nèi)酱虎。hitTest:withEvent
調(diào)用pointInside:withEvent:
。
hitTest以遞歸的形式調(diào)用直至找到能處理觸摸事件的最頂部葉子視圖(一般就是手指點(diǎn)中區(qū)域所屬的視圖)擂涛,那么該視圖就會被選中读串。稱為第一響應(yīng)者。
做個(gè)類比就是:老板說我這有個(gè)事誰干給干了撒妈?經(jīng)過經(jīng)理等層層指派恢暖,大家都覺得小王合適。
該過程可以通過復(fù)寫下面方法斷點(diǎn)了解狰右。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
具體情況如圖所示:
二杰捂、sendEvent
一旦確定第一響應(yīng)者后,UIApplication單例就會發(fā)送相關(guān)觸摸事件到第一響應(yīng)者棋蚌。
就好比老本拍板說那就讓小王干了嫁佳。
該過程可以斷點(diǎn)button的處理方法進(jìn)行了解。如圖所示:
三附鸽、事件處理
針對觸摸事件的處理是重載如下方法:
- (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;
第一響應(yīng)者接到事件后有以下三種選擇:
- 什么都不不處理(選擇不重載上述方法)
- 處理一部分脱拼,剩下的交由其它對象處理(重載處理后調(diào)用super)
- 獨(dú)自進(jìn)行處理(重載處理并且不調(diào)用super)
如果第一響應(yīng)者什么都不處理或者處理部分然后調(diào)用super,則事件將被發(fā)送到一個(gè)鏈?zhǔn)巾憫?yīng)路徑中坷备,即響應(yīng)鏈。該事件遵循以下路徑進(jìn)行轉(zhuǎn)發(fā):
- 第一響應(yīng)者
- 第一響應(yīng)者的父視圖
- 父視圖的父視圖情臭,直至關(guān)聯(lián)的視圖控制器
- 視圖控制器的父視圖控制器省撑,直至根視圖
- 根視圖下一響應(yīng)者是window
- window的下一響應(yīng)者是application
- 最后的響應(yīng)者是App delegate
若轉(zhuǎn)發(fā)至App delegate,且App delegate未進(jìn)行處理則該事件將被丟棄俯在。
iOS處理方式
那么事件處理的方式有哪些?我們大致可以劃分為下面三類竟秫。
- UIControl的子類
- TouchEvent
- 觸摸相關(guān)手勢。
有可能一個(gè)對象同時(shí)實(shí)現(xiàn)了兩個(gè)或者更多處理方式跷乐。那么誰的優(yōu)先級高些呢肥败?
這個(gè)我們可以做個(gè)類比,讓你實(shí)現(xiàn)判斷鼠標(biāo)單機(jī)還是雙擊愕提,那么檢測單擊的時(shí)間會比雙擊要長馒稍。因?yàn)殡p擊失效后才會進(jìn)行單擊判定。
同理手勢識別不了了浅侨,其它才有機(jī)會處理纽谒,UIControl子類也類似,那么優(yōu)先級如下所示:
觸摸相關(guān)手勢 > UIControl的子類 > TouchEvent
一般情況下如输,我們遇到的是手勢觸發(fā)了就不會繼續(xù)轉(zhuǎn)發(fā)事件了鼓黔。
這就是為什么有人在UIButton中添加個(gè)tap手勢看起來正常(別笑)央勒。
事件分發(fā)機(jī)制應(yīng)用
事件分發(fā)不同場景方法選擇
更改事件分發(fā)流程,實(shí)現(xiàn)相關(guān)類的
hitTest:withEvent:
方法為佳澳化。擴(kuò)大點(diǎn)擊范圍崔步,覆寫
pointInside:withEvent:
是個(gè)不錯(cuò)的選擇。-
解決一寫如收起鍵盤等問題缎谷,可嘗試直接發(fā)送事件井濒。如:
[[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil];
-
選擇讓父視圖或者關(guān)聯(lián)視圖控制器處理事件可以這么寫(雖然很少見有這么玩的):
[button addTarget:nil action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
具體應(yīng)用場景
實(shí)現(xiàn)自定義懸浮窗
根據(jù)響應(yīng)鏈規(guī)則,我們能更好的設(shè)計(jì)一個(gè)自定義懸浮窗慎陵。
- 懸浮窗作為UIWindow的rootController存在
- 覆寫
pointInside:withEvent:
判斷事件所屬UIWindow - 完善自己的事件命中測試眼虱,實(shí)現(xiàn)視圖層級管理
詳情參加Flex寫法。
模擬點(diǎn)擊
通過了解分析事件分發(fā)過程席纽,我們能很清楚的知道要實(shí)現(xiàn)模擬點(diǎn)擊我們只需把點(diǎn)擊事件發(fā)送給UIApplication單例的隊(duì)列就可以了捏悬。
- 創(chuàng)建UIEvent對象。
- 發(fā)送UIEvent對象
[[UIApplication sharedApplication] sendEvent:event]
润梯。
統(tǒng)計(jì)與修改UITouch
- hook UIApplication類的
- (void)sendEvent:(UIEvent *)event;
方法 - 進(jìn)行統(tǒng)計(jì)或修改
注:創(chuàng)建UITouch對象和對UITouch對象進(jìn)行修改过牙,可以參考kif-framework。