iOS 事件傳遞和處理

前言

iPhone擁有很好的用戶交互體驗(yàn),這源于iOS系統(tǒng)對(duì)交互事件的高效處理和高優(yōu)響應(yīng);
App開(kāi)發(fā)者處理用戶交互非常便捷,這源于iOS系統(tǒng)和UIKit對(duì)用戶操作做了封裝和默認(rèn)處理;
本文圍繞iOS的事件傳遞和處理,探究其具體過(guò)程。

正文

什么是事件?

這里講的事件是用戶交互的抽象,像IOHIDEvent和UIEvent都是不同處理階段的封裝。

IOHIDEvent是iOS系統(tǒng)對(duì)事件的封裝,感興趣可以看源碼IOHIDEvent.hIOHIDEvent.cpp(HID是Human Interface Device的縮寫(xiě))。

UIEvent是UIKit封裝的描述用戶操作類(lèi)型的對(duì)象,可能有touch事件、motion事件、remote-control事件、press事件等。不同事件在響應(yīng)鏈中處理方式不同,這里我們主要分析touch事件的傳遞和處理。

用戶點(diǎn)擊手機(jī)屏幕的過(guò)程

App外:用戶點(diǎn)擊->硬件響應(yīng)->參數(shù)量化->數(shù)據(jù)轉(zhuǎn)發(fā)->App接收。

在用戶觸摸屏幕之后,屏幕硬件會(huì)接受用戶的操作,并采集關(guān)鍵的參數(shù)傳遞給IOKit,而IOKit將這些數(shù)據(jù)打包并傳給SpringBoard.app,繼而轉(zhuǎn)發(fā)給前臺(tái)App。

App內(nèi):子線程接收事件->主線程封裝事件->UIWindow啟動(dòng)hitTest確定目標(biāo)視圖->UIApplication開(kāi)始發(fā)送事件->touch事件開(kāi)始回調(diào)。

App啟動(dòng)時(shí)便會(huì)啟動(dòng)一個(gè)com.apple.uikit.eventfetch-thread子線程,負(fù)責(zé)接收SpringBoard.app轉(zhuǎn)發(fā)過(guò)來(lái)的數(shù)據(jù)(通過(guò)runloop監(jiān)聽(tīng)source1,查看堆棧中有__CFRunLoopDoSource1),數(shù)據(jù)會(huì)被封裝成IOHIDEvent對(duì)象,然后轉(zhuǎn)發(fā)給主線程;

主線程同樣在啟動(dòng)時(shí)監(jiān)聽(tīng)source0,接收eventfetch-thread線程發(fā)送的IOHIDEvent數(shù)據(jù),再封裝成UIEvent,根據(jù)UIEvent的類(lèi)型判斷是否需要啟動(dòng)hitTest。motion事件不需要hitTest,touch事件也有部分不需要hitTest,比如說(shuō)touch結(jié)束觸發(fā)的事件。

確定目標(biāo)視圖之后,UIApplication便會(huì)發(fā)送事件,將UITouch和UIEvent發(fā)送給目標(biāo)視圖,觸發(fā)其touches系列的方法。

UIKit尋找目標(biāo)視圖的過(guò)程

尋找的過(guò)程主要依賴兩個(gè)UIView的方法:-hitTest:withEvent方法和-pointInsdie:withEvent方法。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

hitTest方法返回point和event對(duì)應(yīng)的視圖;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

pointInside方法返回point和event是否在自己當(dāng)前視圖上;

這兩個(gè)方法UIView都提供了默認(rèn)實(shí)現(xiàn),hitTest方法默認(rèn)會(huì)調(diào)用所有子視圖的hitTest方法,如果有一個(gè)返回。

UIKit會(huì)從UIWindow開(kāi)始尋找目標(biāo)視圖,先調(diào)用UIWindow的hitTest方法詢問(wèn)是否有響應(yīng)的視圖,hitTest方法首先會(huì)先調(diào)用UIWindow的pointInside方法詢問(wèn)是否在點(diǎn)擊范圍內(nèi)。

a.如果pointInside方法返回NO,則證明UIWindow無(wú)法響應(yīng)該事件,hitTest方法會(huì)馬上返回nil;
b.如果pointInside方法返回YES,則證明UIWindow可以響應(yīng)該事件,hitTest方法會(huì)接著調(diào)用UIWindow子視圖的hitTest方法。

  • b1.如果子視圖hitTest方法如果有返回視圖,則UIWindow的hitTest方法會(huì)返回該視圖;
  • b2.如果所有子視圖hitTest方法都沒(méi)有返回視圖,則UIWindow的hitTest方法會(huì)返回自己。

UIWindow是UIView的子類(lèi),UIView的hitTest方法實(shí)現(xiàn)和上述過(guò)程一致。

思考:
UIView在調(diào)用子視圖hitTest時(shí),是先調(diào)用哪些子視圖?

從subview數(shù)組的末尾開(kāi)始調(diào)用hitTest,subview數(shù)組下標(biāo)越小,視圖層級(jí)越低。

UIKit確定目標(biāo)視圖后的過(guò)程

當(dāng)UIKit確定目標(biāo)視圖之后,就會(huì)創(chuàng)建UITouch,UITouch的window屬性和view屬性就是上面過(guò)程中的UIWindow和目標(biāo)視圖。

接著UIApplication就會(huì)調(diào)用sendEvent:方法,接著UIWindow在sendEvent:方法中會(huì)調(diào)用sendTouchesForEvent:方法,如下圖:

UIWindow的sendTouchesForEvent:方法調(diào)用的是我們熟悉的touches四大方法:
-touchesBegan:withEvent:
-touchesMoved:withEvent:
-touchesEnded:withEvent:
-touchesCancelled:withEvent:
從上一步尋找到的目標(biāo)視圖開(kāi)始,目標(biāo)視圖會(huì)首先被調(diào)用touches方法,接著是目標(biāo)視圖的父視圖,再是父視圖的父視圖,如果某個(gè)視圖是ViewController的.view屬性,還會(huì)調(diào)用ViewController的方法,直到UIWindow、UIApplication、UIApplicationDelegate(我們創(chuàng)建的AppDelegate)。

下面是官方文檔給出的回調(diào)順序:(Responder chains in an app)

手勢(shì)處理發(fā)生在哪一步

手勢(shì)(UIGestureRecognizer)是iPhone的重要交互方式,手勢(shì)識(shí)別 介紹了手勢(shì)是如何識(shí)別,甚至可以添加自定義手勢(shì)。

UIGestureRecognizer同樣有touches系列方法:

手勢(shì)處理的發(fā)生時(shí)機(jī)我們可以通過(guò)手勢(shì)的touchesBegan:withEvent:方法來(lái)看,當(dāng)我們斷點(diǎn)在手勢(shì)的touchesBegan方法時(shí),我們看到堆棧:

注意到堆棧中的UIApplication的sendEvent:方法,sendEvent是發(fā)生在UIKit尋找目標(biāo)視圖過(guò)程之后。從另外一種角度來(lái)思考,touchesBegan方法中會(huì)用到UITouch,而UITouch中的view屬性是目標(biāo)視圖,所以手勢(shì)的處理應(yīng)該也放在UIKit尋找目標(biāo)視圖之后。

當(dāng)手勢(shì)的touchesBegan:withEvent:處理完成之后,便會(huì)觸發(fā)目標(biāo)視圖的touchesBegan方法。

但是當(dāng)手勢(shì)識(shí)別成功之后,默認(rèn)會(huì)cancel后續(xù)touch操作,從目標(biāo)視圖開(kāi)始的響應(yīng)鏈都會(huì)收到touchesCancelled方法,而不是正常的touchesEnded方法,堆棧如下:

這個(gè)行為也可以通過(guò)設(shè)置下面的cancelsTouchesInView=NO來(lái)避免觸發(fā)touchesCancelled方法。

注意到不管是手勢(shì)處理開(kāi)始的touchesBegan方法,還是手勢(shì)識(shí)別成功后觸發(fā)touchesCancelled方法,堆棧中都有一個(gè)UIGestureEnvironment類(lèi)。這是一個(gè)UIKit的私有類(lèi),在網(wǎng)上搜到相關(guān)代碼介紹:

@interface UIGestureEnvironment : NSObject {
    NSMutableArray * _delayedPresses;
    NSMutableArray * _delayedPressesToSend;
    NSMutableArray * _delayedTouches;
    NSMutableArray * _delayedTouchesToSend;
    UIGestureGraph * _dependencyGraph;
    NSMutableArray * _dirtyGestureRecognizers;
    bool  _dirtyGestureRecognizersUnsorted;
    struct __CFRunLoopObserver { } * _gestureEnvironmentUpdateObserver;
    NSMutableSet * _gestureRecognizersNeedingRemoval;
    NSMutableSet * _gestureRecognizersNeedingReset;
    NSMutableSet * _gestureRecognizersNeedingUpdate;
    NSMapTable * _nodesByGestureRecognizer;
    bool  _updateExclusivity;
}

- (void)addGestureRecognizer:(id)arg1;
- (void)addRequirementForGestureRecognizer:(id)arg1 requiringGestureRecognizerToFail:(id)arg2;
- (bool)gestureRecognizer:(id)arg1 requiresGestureRecognizerToFail:(id)arg2;
- (id)init;
- (void)removeGestureRecognizer:(id)arg1;
...

從頭文件的方法聲明,我們可以大概知道這是一個(gè)手勢(shì)管理類(lèi),手勢(shì)的添加、移除、響應(yīng)都在內(nèi)部完成。

思考:

1、UIButton的點(diǎn)擊回調(diào)是怎么實(shí)現(xiàn)的?
2、如果給UIButton添加Tap手勢(shì),點(diǎn)擊UIButton的時(shí)候是觸發(fā)UIButton的Tap手勢(shì),還是觸發(fā)UIButton的點(diǎn)擊回調(diào)?

總結(jié)

所以綜上三步,我們可以知道整個(gè)流程大概是:

  1. 尋找目標(biāo)視圖:UIApplication->UIWindow->ViewController->View->targetView
  2. 手勢(shì)識(shí)別:UIGestureEnvironment-> UIGestureRecognizer
  3. 響應(yīng)鏈回調(diào):targetView->Viewd->ViewController->UIWindow->UIApplication

iOS的用戶交互相關(guān)非常復(fù)雜。由于時(shí)間有限,這里僅僅從事件的傳遞和處理出發(fā),來(lái)建立一個(gè)基礎(chǔ)的認(rèn)知。

附錄

參考文獻(xiàn)

手勢(shì)識(shí)別 https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/implementing_a_custom_gesture_recognizer/about_the_gesture_recognizer_state_machine

響應(yīng)鏈介紹 https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events?from=from_parent_mindnote

思考題

1、UIButton的點(diǎn)擊回調(diào)是怎么實(shí)現(xiàn)的?

UIButton是UIControl的子類(lèi),通過(guò)追蹤touch事件的變化得到一些UIControl定義的事件(UIControlEvents);UIButton的點(diǎn)擊操作是通過(guò)UIControlEvents的事件變化回調(diào)來(lái)觸發(fā),本質(zhì)依賴的是響應(yīng)鏈回調(diào)過(guò)程中的touches系列方法。

2、如果給UIButton添加Tap手勢(shì),點(diǎn)擊UIButton的時(shí)候是觸發(fā)UIButton的Tap手勢(shì),還是觸發(fā)UIButton的點(diǎn)擊回調(diào)?

上文分析了手勢(shì)的識(shí)別是發(fā)生在響應(yīng)鏈回調(diào)之前,也就是tap手勢(shì)是發(fā)生在touches系列方法回調(diào)之前,那么Tap手勢(shì)應(yīng)該是在UIButton的touches方法之前。如果UIButton監(jiān)聽(tīng)的是常用的UIControlEventTouchUpInside事件,則不會(huì)回調(diào);如果監(jiān)聽(tīng)的是UIControlEventTouchCancel事件,則在觸發(fā)完Tap手勢(shì)之后,還會(huì)收到回調(diào)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在開(kāi)發(fā)過(guò)程中,大家或多或少的都會(huì)碰到令人頭疼的手勢(shì)沖突問(wèn)題,正好前兩天碰到一個(gè)類(lèi)似的bug,于是借著這個(gè)機(jī)會(huì)了解了...
    閆仕偉閱讀 5,662評(píng)論 2 23
  • 該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請(qǐng)注明:劉小壯[http://www.itdecent.cn/u/2de707c93d...
    劉小壯閱讀 32,350評(píng)論 32 209
  • 事件的生命周期 當(dāng)指尖觸碰屏幕的那一刻,一個(gè)觸摸事件就在系統(tǒng)中生成了。經(jīng)過(guò)IPC進(jìn)程間通信,事件最終被傳遞到了合適...
    HughKaun閱讀 1,289評(píng)論 0 4
  • 1.3事件的傳遞和處理 (一)事件的產(chǎn)生和傳遞 事件傳遞的作用就是找到合適的view來(lái)處理事件 1.當(dāng)發(fā)生觸摸事件...
    劉2傻閱讀 353評(píng)論 0 2
  • 在使用手機(jī)的過(guò)程中,會(huì)產(chǎn)生很多交互事件,如觸摸屏幕、搖晃、按下按鍵、使用耳機(jī)操控設(shè)備等。這些事件都需要系統(tǒng)去響應(yīng)并...
    pro648閱讀 833評(píng)論 1 2

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