前言
iPhone擁有很好的用戶交互體驗,這源于iOS系統對交互事件的高效處理和高優響應;
App開發者處理用戶交互非常便捷,這源于iOS系統和UIKit對用戶操作做了封裝和默認處理;
本文圍繞iOS的事件傳遞和處理,探究其具體過程。
正文
什么是事件?
這里講的事件是用戶交互的抽象,像IOHIDEvent和UIEvent都是不同處理階段的封裝。
IOHIDEvent是iOS系統對事件的封裝,感興趣可以看源碼IOHIDEvent.h和IOHIDEvent.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的點擊回調?
總結
所以綜上三步,我們可以知道整個流程大概是:
- 尋找目標視圖:UIApplication->UIWindow->ViewController->View->targetView
- 手勢識別:UIGestureEnvironment-> UIGestureRecognizer
- 響應鏈回調:targetView->Viewd->ViewController->UIWindow->UIApplication
iOS的用戶交互相關非常復雜。由于時間有限,這里僅僅從事件的傳遞和處理出發,來建立一個基礎的認知。
附錄
參考文獻
思考題
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手勢之后,還會收到回調。