iOS 事件傳遞和處理

前言

iPhone擁有很好的用戶交互體驗,這源于iOS系統對交互事件的高效處理和高優響應;
App開發者處理用戶交互非常便捷,這源于iOS系統和UIKit對用戶操作做了封裝和默認處理;
本文圍繞iOS的事件傳遞和處理,探究其具體過程。

正文

什么是事件?

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

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

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

用戶點擊手機屏幕的過程

App外:用戶點擊->硬件響應->參數量化->數據轉發->App接收。

在用戶觸摸屏幕之后,屏幕硬件會接受用戶的操作,并采集關鍵的參數傳遞給IOKit,而IOKit將這些數據打包并傳給SpringBoard.app,繼而轉發給前臺App。

App內:子線程接收事件->主線程封裝事件->UIWindow啟動hitTest確定目標視圖->UIApplication開始發送事件->touch事件開始回調。

App啟動時便會啟動一個com.apple.uikit.eventfetch-thread子線程,負責接收SpringBoard.app轉發過來的數據(通過runloop監聽source1,查看堆棧中有__CFRunLoopDoSource1),數據會被封裝成IOHIDEvent對象,然后轉發給主線程;

主線程同樣在啟動時監聽source0,接收eventfetch-thread線程發送的IOHIDEvent數據,再封裝成UIEvent,根據UIEvent的類型判斷是否需要啟動hitTest。motion事件不需要hitTest,touch事件也有部分不需要hitTest,比如說touch結束觸發的事件。

確定目標視圖之后,UIApplication便會發送事件,將UITouch和UIEvent發送給目標視圖,觸發其touches系列的方法。

UIKit尋找目標視圖的過程

尋找的過程主要依賴兩個UIView的方法:-hitTest:withEvent方法和-pointInsdie:withEvent方法。

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

hitTest方法返回point和event對應的視圖;

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

pointInside方法返回point和event是否在自己當前視圖上;

這兩個方法UIView都提供了默認實現,hitTest方法默認會調用所有子視圖的hitTest方法,如果有一個返回。

UIKit會從UIWindow開始尋找目標視圖,先調用UIWindow的hitTest方法詢問是否有響應的視圖,hitTest方法首先會先調用UIWindow的pointInside方法詢問是否在點擊范圍內。

a.如果pointInside方法返回NO,則證明UIWindow無法響應該事件,hitTest方法會馬上返回nil;
b.如果pointInside方法返回YES,則證明UIWindow可以響應該事件,hitTest方法會接著調用UIWindow子視圖的hitTest方法。

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

UIWindow是UIView的子類,UIView的hitTest方法實現和上述過程一致。

思考:
UIView在調用子視圖hitTest時,是先調用哪些子視圖?

從subview數組的末尾開始調用hitTest,subview數組下標越小,視圖層級越低。

UIKit確定目標視圖后的過程

當UIKit確定目標視圖之后,就會創建UITouch,UITouch的window屬性和view屬性就是上面過程中的UIWindow和目標視圖。

接著UIApplication就會調用sendEvent:方法,接著UIWindow在sendEvent:方法中會調用sendTouchesForEvent:方法,如下圖:

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

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

手勢處理發生在哪一步

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

UIGestureRecognizer同樣有touches系列方法:

手勢處理的發生時機我們可以通過手勢的touchesBegan:withEvent:方法來看,當我們斷點在手勢的touchesBegan方法時,我們看到堆棧:

注意到堆棧中的UIApplication的sendEvent:方法,sendEvent是發生在UIKit尋找目標視圖過程之后。從另外一種角度來思考,touchesBegan方法中會用到UITouch,而UITouch中的view屬性是目標視圖,所以手勢的處理應該也放在UIKit尋找目標視圖之后。

當手勢的touchesBegan:withEvent:處理完成之后,便會觸發目標視圖的touchesBegan方法。

但是當手勢識別成功之后,默認會cancel后續touch操作,從目標視圖開始的響應鏈都會收到touchesCancelled方法,而不是正常的touchesEnded方法,堆棧如下:

這個行為也可以通過設置下面的cancelsTouchesInView=NO來避免觸發touchesCancelled方法。

注意到不管是手勢處理開始的touchesBegan方法,還是手勢識別成功后觸發touchesCancelled方法,堆棧中都有一個UIGestureEnvironment類。這是一個UIKit的私有類,在網上搜到相關代碼介紹:

@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;
...

從頭文件的方法聲明,我們可以大概知道這是一個手勢管理類,手勢的添加、移除、響應都在內部完成。

思考:

1、UIButton的點擊回調是怎么實現的?
2、如果給UIButton添加Tap手勢,點擊UIButton的時候是觸發UIButton的Tap手勢,還是觸發UIButton的點擊回調?

總結

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

  1. 尋找目標視圖:UIApplication->UIWindow->ViewController->View->targetView
  2. 手勢識別:UIGestureEnvironment-> UIGestureRecognizer
  3. 響應鏈回調:targetView->Viewd->ViewController->UIWindow->UIApplication

iOS的用戶交互相關非常復雜。由于時間有限,這里僅僅從事件的傳遞和處理出發,來建立一個基礎的認知。

附錄

參考文獻

手勢識別 https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/implementing_a_custom_gesture_recognizer/about_the_gesture_recognizer_state_machine

響應鏈介紹 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的點擊回調是怎么實現的?

UIButton是UIControl的子類,通過追蹤touch事件的變化得到一些UIControl定義的事件(UIControlEvents);UIButton的點擊操作是通過UIControlEvents的事件變化回調來觸發,本質依賴的是響應鏈回調過程中的touches系列方法。

2、如果給UIButton添加Tap手勢,點擊UIButton的時候是觸發UIButton的Tap手勢,還是觸發UIButton的點擊回調?

上文分析了手勢的識別是發生在響應鏈回調之前,也就是tap手勢是發生在touches系列方法回調之前,那么Tap手勢應該是在UIButton的touches方法之前。如果UIButton監聽的是常用的UIControlEventTouchUpInside事件,則不會回調;如果監聽的是UIControlEventTouchCancel事件,則在觸發完Tap手勢之后,還會收到回調。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容

  • 前言 iPhone擁有很好的用戶交互體驗,這源于iOS系統對交互事件的高效處理和高優響應;App開發者處理用戶交互...
    落影loyinglin閱讀 2,145評論 4 18
  • 在開發過程中,大家或多或少的都會碰到令人頭疼的手勢沖突問題,正好前兩天碰到一個類似的bug,于是借著這個機會了解了...
    閆仕偉閱讀 5,382評論 2 23
  • 該文章屬于劉小壯原創,轉載請注明:劉小壯[http://www.lxweimin.com/u/2de707c93d...
    劉小壯閱讀 32,119評論 32 209
  • 事件的生命周期 當指尖觸碰屏幕的那一刻,一個觸摸事件就在系統中生成了。經過IPC進程間通信,事件最終被傳遞到了合適...
    HughKaun閱讀 1,198評論 0 4
  • 1.3事件的傳遞和處理 (一)事件的產生和傳遞 事件傳遞的作用就是找到合適的view來處理事件 1.當發生觸摸事件...
    劉2傻閱讀 325評論 0 2