前言
iPhone擁有很好的用戶交互體驗败玉,這源于iOS系統(tǒng)對交互事件的高效處理和高優(yōu)響應(yīng)年叮;
App開發(fā)者處理用戶交互非常便捷闻察,這源于iOS系統(tǒng)和UIKit對用戶操作做了封裝和默認(rèn)處理奔誓;
本文圍繞iOS的事件傳遞和處理,探究其具體過程站粟。
正文
什么是事件黍图?
這里講的事件是用戶交互的抽象,像IOHIDEvent和UIEvent都是不同處理階段的封裝奴烙。
IOHIDEvent是iOS系統(tǒng)對事件的封裝助被,感興趣可以看源碼IOHIDEvent.h和IOHIDEvent.cpp(HID是Human Interface Device的縮寫)。
UIEvent是UIKit封裝的描述用戶操作類型的對象切诀,可能有touch事件揩环、motion事件、remote-control事件幅虑、press事件等检盼。不同事件在響應(yīng)鏈中處理方式不同,這里我們主要分析touch事件的傳遞和處理翘单。
用戶點(diǎn)擊手機(jī)屏幕的過程
App外:用戶點(diǎn)擊->硬件響應(yīng)->參數(shù)量化->數(shù)據(jù)轉(zhuǎn)發(fā)->App接收吨枉。
在用戶觸摸屏幕之后,屏幕硬件會接受用戶的操作哄芜,并采集關(guān)鍵的參數(shù)傳遞給IOKit貌亭,而IOKit將這些數(shù)據(jù)打包并傳給SpringBoard.app,繼而轉(zhuǎn)發(fā)給前臺App认臊。
App內(nèi):子線程接收事件->主線程封裝事件->UIWindow啟動hitTest確定目標(biāo)視圖->UIApplication開始發(fā)送事件->touch事件開始回調(diào)圃庭。
App啟動時便會啟動一個com.apple.uikit.eventfetch-thread子線程,負(fù)責(zé)接收SpringBoard.app轉(zhuǎn)發(fā)過來的數(shù)據(jù)(通過runloop監(jiān)聽source1,查看堆棧中有__CFRunLoopDoSource1)剧腻,數(shù)據(jù)會被封裝成IOHIDEvent對象拘央,然后轉(zhuǎn)發(fā)給主線程;
主線程同樣在啟動時監(jiān)聽source0书在,接收eventfetch-thread線程發(fā)送的IOHIDEvent數(shù)據(jù)灰伟,再封裝成UIEvent,根據(jù)UIEvent的類型判斷是否需要啟動hitTest儒旬。motion事件不需要hitTest栏账,touch事件也有部分不需要hitTest,比如說touch結(jié)束觸發(fā)的事件栈源。
確定目標(biāo)視圖之后挡爵,UIApplication便會發(fā)送事件,將UITouch和UIEvent發(fā)送給目標(biāo)視圖甚垦,觸發(fā)其touches系列的方法茶鹃。
UIKit尋找目標(biāo)視圖的過程
尋找的過程主要依賴兩個UIView的方法:-hitTest:withEvent方法和-pointInsdie:withEvent方法。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
hitTest方法返回point和event對應(yīng)的視圖艰亮;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
pointInside方法返回point和event是否在自己當(dāng)前視圖上前计;
這兩個方法UIView都提供了默認(rèn)實現(xiàn),hitTest方法默認(rèn)會調(diào)用所有子視圖的hitTest方法垃杖,如果有一個返回。
UIKit會從UIWindow開始尋找目標(biāo)視圖丈屹,先調(diào)用UIWindow的hitTest方法詢問是否有響應(yīng)的視圖调俘,hitTest方法首先會先調(diào)用UIWindow的pointInside方法詢問是否在點(diǎn)擊范圍內(nèi)。
a.如果pointInside方法返回NO旺垒,則證明UIWindow無法響應(yīng)該事件彩库,hitTest方法會馬上返回nil;
b.如果pointInside方法返回YES先蒋,則證明UIWindow可以響應(yīng)該事件骇钦,hitTest方法會接著調(diào)用UIWindow子視圖的hitTest方法。
- b1.如果子視圖hitTest方法如果有返回視圖竞漾,則UIWindow的hitTest方法會返回該視圖眯搭;
- b2.如果所有子視圖hitTest方法都沒有返回視圖,則UIWindow的hitTest方法會返回自己业岁。
UIWindow是UIView的子類鳞仙,UIView的hitTest方法實現(xiàn)和上述過程一致。
思考:
UIView在調(diào)用子視圖hitTest時笔时,是先調(diào)用哪些子視圖棍好?
從subview數(shù)組的末尾開始調(diào)用hitTest,subview數(shù)組下標(biāo)越小,視圖層級越低借笙。
UIKit確定目標(biāo)視圖后的過程
當(dāng)UIKit確定目標(biāo)視圖之后扒怖,就會創(chuàng)建UITouch,UITouch的window屬性和view屬性就是上面過程中的UIWindow和目標(biāo)視圖业稼。
接著UIApplication就會調(diào)用sendEvent:方法盗痒,接著UIWindow在sendEvent:方法中會調(diào)用sendTouchesForEvent:方法,如下圖:
UIWindow的sendTouchesForEvent:方法調(diào)用的是我們熟悉的touches四大方法:
-touchesBegan:withEvent:
-touchesMoved:withEvent:
-touchesEnded:withEvent:
-touchesCancelled:withEvent:
從上一步尋找到的目標(biāo)視圖開始盼忌,目標(biāo)視圖會首先被調(diào)用touches方法积糯,接著是目標(biāo)視圖的父視圖,再是父視圖的父視圖谦纱,如果某個視圖是ViewController的.view屬性看成,還會調(diào)用ViewController的方法,直到UIWindow跨嘉、UIApplication川慌、UIApplicationDelegate(我們創(chuàng)建的AppDelegate)。
下面是官方文檔給出的回調(diào)順序:(Responder chains in an app)
手勢處理發(fā)生在哪一步
手勢(UIGestureRecognizer)是iPhone的重要交互方式祠乃,手勢識別 介紹了手勢是如何識別梦重,甚至可以添加自定義手勢。
UIGestureRecognizer同樣有touches系列方法:
手勢處理的發(fā)生時機(jī)我們可以通過手勢的touchesBegan:withEvent:方法來看亮瓷,當(dāng)我們斷點(diǎn)在手勢的touchesBegan方法時琴拧,我們看到堆棧:
注意到堆棧中的UIApplication的sendEvent:方法,sendEvent是發(fā)生在UIKit尋找目標(biāo)視圖過程之后嘱支。從另外一種角度來思考蚓胸,touchesBegan方法中會用到UITouch,而UITouch中的view屬性是目標(biāo)視圖除师,所以手勢的處理應(yīng)該也放在UIKit尋找目標(biāo)視圖之后沛膳。
當(dāng)手勢的touchesBegan:withEvent:處理完成之后,便會觸發(fā)目標(biāo)視圖的touchesBegan方法汛聚。
但是當(dāng)手勢識別成功之后锹安,默認(rèn)會cancel后續(xù)touch操作,從目標(biāo)視圖開始的響應(yīng)鏈都會收到touchesCancelled方法倚舀,而不是正常的touchesEnded方法叹哭,堆棧如下:
這個行為也可以通過設(shè)置下面的cancelsTouchesInView=NO來避免觸發(fā)touchesCancelled方法。
注意到不管是手勢處理開始的touchesBegan方法痕貌,還是手勢識別成功后觸發(fā)touchesCancelled方法话速,堆棧中都有一個UIGestureEnvironment類。這是一個UIKit的私有類芯侥,在網(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;
...
從頭文件的方法聲明泊交,我們可以大概知道這是一個手勢管理類乳讥,手勢的添加、移除廓俭、響應(yīng)都在內(nèi)部完成云石。
思考:
1、UIButton的點(diǎn)擊回調(diào)是怎么實現(xiàn)的研乒?
2汹忠、如果給UIButton添加Tap手勢,點(diǎn)擊UIButton的時候是觸發(fā)UIButton的Tap手勢雹熬,還是觸發(fā)UIButton的點(diǎn)擊回調(diào)宽菜?
總結(jié)
所以綜上三步,我們可以知道整個流程大概是:
- 尋找目標(biāo)視圖:UIApplication->UIWindow->ViewController->View->targetView
- 手勢識別:UIGestureEnvironment-> UIGestureRecognizer
- 響應(yīng)鏈回調(diào):targetView->Viewd->ViewController->UIWindow->UIApplication
iOS的用戶交互相關(guān)非常復(fù)雜竿报。由于時間有限铅乡,這里僅僅從事件的傳遞和處理出發(fā),來建立一個基礎(chǔ)的認(rèn)知烈菌。
附錄
參考文獻(xiàn)
思考題
1阵幸、UIButton的點(diǎn)擊回調(diào)是怎么實現(xiàn)的?
UIButton是UIControl的子類芽世,通過追蹤touch事件的變化得到一些UIControl定義的事件(UIControlEvents)挚赊;UIButton的點(diǎn)擊操作是通過UIControlEvents的事件變化回調(diào)來觸發(fā),本質(zhì)依賴的是響應(yīng)鏈回調(diào)過程中的touches系列方法济瓢。
2荠割、如果給UIButton添加Tap手勢,點(diǎn)擊UIButton的時候是觸發(fā)UIButton的Tap手勢旺矾,還是觸發(fā)UIButton的點(diǎn)擊回調(diào)蔑鹦?
上文分析了手勢的識別是發(fā)生在響應(yīng)鏈回調(diào)之前,也就是tap手勢是發(fā)生在touches系列方法回調(diào)之前宠漩,那么Tap手勢應(yīng)該是在UIButton的touches方法之前。如果UIButton監(jiān)聽的是常用的UIControlEventTouchUpInside事件懊直,則不會回調(diào)扒吁;如果監(jiān)聽的是UIControlEventTouchCancel事件,則在觸發(fā)完Tap手勢之后室囊,還會收到回調(diào)雕崩。