事件的生命周期
當(dāng)指尖觸碰屏幕的那一刻宵凌,一個觸摸事件就在系統(tǒng)中生成了郊供。經(jīng)過IPC進程間通信,事件最終被傳遞到了合適的應(yīng)用须揣。在應(yīng)用內(nèi)歷經(jīng)峰回路轉(zhuǎn)的奇幻之旅后盐股,最終被釋放。大致經(jīng)過如下圖
系統(tǒng)響應(yīng)階段
手指觸碰屏幕耻卡,屏幕感應(yīng)到觸碰后疯汁,將事件交由IOKit處理。
-
IOKit將觸摸事件封裝成一個IOHIDEvent對象卵酪,并通過mach port傳遞給SpringBoad進程幌蚊。
mach port 進程端口谤碳,各進程之間通過它進行通信。
SpringBoad.app 是一個系統(tǒng)進程溢豆,可以理解為桌面系統(tǒng)蜒简,可以統(tǒng)一管理和分發(fā)系統(tǒng)接收到的觸摸事件。 -
SpringBoard進程因接收到觸摸事件漩仙,觸發(fā)了主線程runloop的source1事件源的回調(diào)搓茬。
此時SpringBoard會根據(jù)當(dāng)前桌面的狀態(tài),判斷應(yīng)該由誰處理此次觸摸事件队他。因為事件發(fā)生時卷仑,你可能正在桌面上翻頁,也可能正在刷微博麸折。若是前者(即前臺無APP運行)锡凝,則觸發(fā)SpringBoard本身主線程runloop的source0事件源的回調(diào),將事件交由桌面系統(tǒng)去消耗磕谅;若是后者(即有app正在前臺運行)私爷,則將觸摸事件通過IPC傳遞給前臺APP進程,接下來的事情便是APP內(nèi)部對于觸摸事件的響應(yīng)了膊夹。
APP響應(yīng)階段
- APP進程的mach port接受到SpringBoard進程傳遞來的觸摸事件,主線程的runloop被喚醒捌浩,觸發(fā)了source1回調(diào)放刨。
- source1回調(diào)又觸發(fā)了一個source0回調(diào),將接收到的IOHIDEvent對象封裝成UIEvent對象尸饺,此時APP將正式開始對于觸摸事件的響應(yīng)进统。
- source0回調(diào)內(nèi)部將觸摸事件添加到UIApplication對象的事件隊列中。事件出隊后浪听,UIApplication開始一個尋找最佳響應(yīng)者的過程螟碎,這個過程又稱hit-testing,細節(jié)將在[尋找事件的最佳響應(yīng)者]一節(jié)闡述迹栓。另外掉分,此處開始便是與我們平時開發(fā)相關(guān)的工作了。
- 尋找到最佳響應(yīng)者后克伊,接下來的事情便是事件在響應(yīng)鏈中的傳遞及響應(yīng)了酥郭,關(guān)于響應(yīng)鏈相關(guān)的內(nèi)容詳見[事件的響應(yīng)及在響應(yīng)鏈中的傳遞]一節(jié)。事實上愿吹,事件除了被響應(yīng)者消耗不从,還能被手勢識別器或是target-action模式捕捉并消耗掉。其中涉及對觸摸事件的響應(yīng)優(yōu)先級犁跪,詳見[事件的三徒弟UIResponder椿息、UIGestureRecognizer歹袁、UIControl]一節(jié)。
- 觸摸事件歷經(jīng)坎坷后要么被某個響應(yīng)對象捕獲后釋放寝优,要么致死也沒能找到能夠響應(yīng)的對象条舔,最終釋放。至此倡勇,這個觸摸事件的使命就算終結(jié)了逞刷。runloop若沒有其他事件需要處理,也將重歸于眠妻熊,等待新的事件到來后喚醒
一. 觸摸夸浅、事件、響應(yīng)者
1. UITouch
源起觸摸
一個手指一次觸摸屏幕扔役,就對應(yīng)生成一個UITouch對象帆喇。多個手指同時觸摸屏幕,生成多個UITouch對象亿胸。
多個手指先后觸摸坯钦,系統(tǒng)會根據(jù)觸摸的位置判斷是否更新同一個UITouch對象。若兩個手指一前一后觸摸同一個位置(即雙擊)侈玄,那么第一次觸摸時生成一個UITouch對象婉刀,第二次觸摸會更新這個UITouch對象,這是該UITouch對象的Tap Count屬性值從1變成2序仙,若兩個手指一前一后觸摸的位置不同突颊,將會生成兩個UITouch對象,兩者之間沒有聯(lián)系潘悼。
每個UITouch對象記錄了觸摸的一些信息律秃,包括觸摸時間、位置治唤、階段棒动、所處的視圖、窗口等信息宾添。
// 觸摸的各個階段狀態(tài)
// 例如當(dāng)手指移動時船惨,會更新phase屬性到UITouchPhaseMoved;
// 手指離屏后辞槐,更新到UITouchPhaseEnded
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
手指離開屏幕一段時間后掷漱,確定該UITouch對象不會再被更新,就釋放榄檬。
2. UIEvent
事件的真身
觸摸的目的是生成觸摸事件供響應(yīng)者響應(yīng)卜范,一個觸摸事件對應(yīng)一個UIEvent對象,其中的type屬性標識了事件的類型鹿榜,事件有如下幾種類型:
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),
};
這里我們所說的事件具體指的是觸摸事件海雪。
UIEvent對象中包含了觸發(fā)該對象的觸摸對象集合锦爵,因為一個觸摸事件可能是由多個手指同時觸摸產(chǎn)生的。觸摸對象集合通過allTouches屬性獲取奥裸。
3.UIResponder
UIResponder是iOS中用于處理用戶事件的API险掀,可以處理觸摸事件、按壓事件(3D touch)湾宙、遠程控制事件樟氢、硬件運動事件∠丽可以通過touchesBegan埠啃、pressesBegan、motionBegan伟恶、remoteControlReceivedWithEvent等方法碴开,獲取到對應(yīng)的回調(diào)消息。UIResponder不只用來接收事件博秫,還可以處理和傳遞對應(yīng)的事件潦牛,如果當(dāng)前響應(yīng)者不能處理,則轉(zhuǎn)發(fā)給其他合適的響應(yīng)者處理挡育。
應(yīng)用程序通過響應(yīng)者來接收和處理事件巴碗,響應(yīng)者可以是繼承自UIResponder的任何子類,例如UIView即寒、UIViewController良价、UIApplication等。當(dāng)事件來到時蒿叠,系統(tǒng)會將事件傳遞給合適的響應(yīng)者,并且將其成為第一響應(yīng)者蚣常。
第一響應(yīng)者未處理的事件市咽,將會在響應(yīng)者鏈中進行傳遞,傳遞規(guī)則由UIResponder的nextResponder決定抵蚊,可以通過重寫該屬性來決定傳遞規(guī)則施绎。當(dāng)一個事件到來時,第一響應(yīng)者沒有接收消息贞绳,則順著響應(yīng)者鏈向后傳遞谷醉。
在iOS中不是任何對象都能處理事件, 只有繼承了UIResponder的對象才能接收并處理事件,我們稱為響應(yīng)者對象.UIApplication,UIViewController,UIView都繼承自UIResponder,因此他們都是響應(yīng)者對象, 都能夠接收并處理事件.繼承自UIResponder的類能處理事件是由于UIResponder內(nèi)部提供了以下方法:
觸摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
傳感器事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
遠程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
二. iOS中事件的產(chǎn)生和傳遞
應(yīng)用程序接收到觸摸事件后,將事件放入UIApplication的事件隊列冈闭,等到處理該事件時俱尼,將該事件出隊列,UIApplication將事件傳遞給窗口對象(UIWindow)萎攒,如果存在多個窗口遇八,則優(yōu)先詢問后顯示的窗口
如果窗口UIWindow不能響應(yīng)事件矛绘,則將事件傳遞給其他窗口;若窗口能響應(yīng)事件刃永,則從后往前詢問窗口的子視圖货矮。
以此類推,如果視圖不能響應(yīng)事件斯够,則將事件傳遞給同級的上一個子視圖囚玫;如果能響應(yīng),就從后往前遍歷當(dāng)前視圖的子視圖读规。
如果當(dāng)前視圖的子視圖都不能響應(yīng)事件抓督,則當(dāng)前視圖就是最合適的響應(yīng)者。
一般事件的傳遞是從父控件傳遞到子控件的.
例如:
點擊了綠色的View掖桦,傳遞過程如下:UIApplication->Window->白色View->綠色View
點擊藍色的View本昏,傳遞過程如下:UIApplication->Window->白色View->橙色View->藍色View
如果父控件接受不到觸摸事件,那么子控件就不可能接收到觸摸事件.
如何尋找最合適的view?
應(yīng)用如何找到最合適的控件來處理事件枪汪?有以下準則:
1.首先判斷主窗口(keyWindow)自己是否能接受觸摸事件;
2.觸摸點是否在自己身上;
3.從后往前遍歷子控件涌穆,重復(fù)前面的兩個步驟(首先查找數(shù)組中最后一個元素);
4.如果沒有符合條件的子控件,那么就認為自己最合適處理.
詳述:
1.主窗口接收到應(yīng)用程序傳遞過來的事件后雀久,首先判斷自己能否接手觸摸事件宿稀。如果能,那么在判斷觸摸點在不在窗口自己身上;
2.如果觸摸點也在窗口身上赖捌,那么窗口會從后往前遍歷自己的子控件(遍歷自己的子控件只是為了尋找出來最合適的view);
3.遍歷到每一個子控件后祝沸,又會重復(fù)上面的兩個步驟(傳遞事件給子控件,1.判斷子控件能否接受事件越庇,2.點在不在子控件上);
4.如此循環(huán)遍歷子控件罩锐,直到找到最合適的view,如果沒有更合適的子控件卤唉,那么自己就成為最合適的view涩惑。
注意:之所以會采取從后往前遍歷子控件的方式尋找最合適的view只是為了做一些循環(huán)優(yōu)化。因為相比較之下桑驱,后添加的view在上面竭恬,降低循環(huán)次數(shù)。
在事件傳遞尋找最合適的View時熬的,底層到底干了哪些事痊硕?
尋找合適的View用到兩個重要方法:
hitTest:withEvent:
pointInside
1.hitTest:withEvent:方法
什么時候調(diào)用?
只要事件一傳遞給一個控件,這個控件就會調(diào)用他自己的hitTest:withEvent:方法尋找合適的View
- pointInside方法
作用
尋找并返回最合適的view(能夠響應(yīng)事件的那個最合適的view)
注 意:不管這個控件能不能處理事件押框,也不管觸摸點在不在這個控件上岔绸,
事件都會先傳遞給這個控件,隨后再調(diào)用hitTest:withEvent:方法
底層具體實現(xiàn)如下 :
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判斷當(dāng)前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判斷點在不在當(dāng)前控件
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.從后往前遍歷自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];
// 把當(dāng)前控件上的坐標系轉(zhuǎn)換成子控件上的坐標系
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 尋找到最合適的view
return fitView;
}
}
// 循環(huán)結(jié)束,表示沒有比自己更合適的view
return self;
}
三. iOS中事件的響應(yīng)
經(jīng)過Hit-Testing的過程后,UIApplication已經(jīng)知道了第一響應(yīng)者是誰亭螟,接下來要做的事情就是:
- 將事件傳遞給第一響應(yīng)者
- 將事件沿著響應(yīng)鏈傳遞
- 將事件傳遞給第一響應(yīng)者:
由于第一響應(yīng)者具有處理事件的最高優(yōu)先級挡鞍,因此UIApplication會先將事件傳遞給它供其處理。首先预烙,UIApplication將事件通過 sendEvent: 傳遞給事件所屬的window墨微,window同樣通過 sendEvent: 再將事件傳遞給hit-tested view,即第一響應(yīng)者扁掸。過程如下:
UIApplication ——> UIWindow ——> hit-tested view
以點擊EView視圖為例翘县,在EView的 touchesBegan:withEvent:上斷點查看調(diào)用棧就能看清這一過程:
從這調(diào)用堆棧我們可以看出,UIApplication對于將事件傳遞給那個UIWindow是很明確的谴分,UIWindow對于將事件傳遞給哪個視圖也是很明確的锈麸。因為這些信息都放在了UIEvent的Touch事件里面。
但是這些信息又是什么時候放入到UIEvent內(nèi)部的呢牺蹄?
可想而知因為Hit-Testing和SendEvent兩者中的UIEvent是同一個UIEvent,所以這應(yīng)該是在Hit-Testing尋找第一響應(yīng)者的過程中忘伞,填入UIEvent內(nèi)部的。
- 將事件沿著響應(yīng)鏈傳遞:
因為每個響應(yīng)者必定都是UIResponder對象沙兰,通過4個響應(yīng)觸摸事件的方法來響應(yīng)事件氓奈。每個UIResponder對象默認都已經(jīng)實現(xiàn)了這4個方法,但是默認不對觸摸事件做任何處理鼎天,單純只是將事件沿著響應(yīng)鏈傳遞舀奶。若要截獲事件進行自定義的響應(yīng)操作,就要重寫相關(guān)的方法斋射。
第一響應(yīng)者接收到觸摸事件后育勺,就具有對觸摸事件的處理權(quán),它可以選擇自己處理這個事件罗岖,也可以將這個事件沿著響應(yīng)鏈傳遞給下一個響應(yīng)者涧至,這個由響應(yīng)者之間構(gòu)成的視圖鏈就稱之為響應(yīng)鏈。
需要注意的是桑包,上一節(jié)所說的事件傳遞的目的是為尋找事件的最佳響應(yīng)者化借,是自下而上的傳遞;這里的事件傳遞目的是響應(yīng)者做出對事件的響應(yīng)捡多,這個過程是自上而下的。前者為“尋找”铐炫,后者為“響應(yīng)”垒手。
響應(yīng)者對于事件的操作方式:
響應(yīng)者對于事件的攔截以及傳遞都是通過 touchesBegan:withEvent:方法控制的狰域,該方法的默認實現(xiàn)是將事件沿著默認的響應(yīng)鏈往下傳遞梆暮。
響應(yīng)者對于接收到的事件有3種操作:
不攔截,默認操作
事件會自動沿著默認的響應(yīng)鏈往下傳遞
攔截武翎,不再往下分發(fā)事件
重寫 touchesBegan:withEvent:進行事件處理,不調(diào)用父類的 touchesBegan:withEvent:
攔截榜掌,繼續(xù)往下分發(fā)事件
重寫 touchesBegan:withEvent:進行事件處理优妙,同時調(diào)用父類的 touchesBegan:withEvent:將事件往下傳遞
響應(yīng)鏈中的事件傳遞規(guī)則:
每一個響應(yīng)者對象(UIResponder對象)都有一個nextResponder方法,用于獲取響應(yīng)鏈中當(dāng)前對象的下一個響應(yīng)者憎账。因此套硼,一旦事件的第一響應(yīng)者確定了,這個事件所處的響應(yīng)鏈就確定了胞皱。
對于響應(yīng)者對象邪意,默認的 nextResponder 實現(xiàn)如下:
UIView
若視圖是控制器的根視圖,則其nextResponder為控制器對象反砌;否則雾鬼,其nextResponder為父視圖。
UIViewController
若控制器的視圖是window的根視圖宴树,則其nextResponder為窗口對象策菜;若控制器是從別的控制器present出來的,則其nextResponder為presenting view controller酒贬。
UIWindow
nextResponder為UIApplication對象又憨。
UIApplication
若當(dāng)前應(yīng)用的app delegate是一個UIResponder對象,且不是UIView同衣、UIViewController或app本身竟块,則UIApplication的nextResponder為app delegate。
例如:
響應(yīng)者鏈如下:
如果點擊UITextField后其會成為第一響應(yīng)者耐齐。
如果textField未處理事件浪秘,則會將事件傳遞給下一級響應(yīng)者鏈,也就是其父視圖埠况。
父視圖未處理事件則繼續(xù)向下傳遞耸携,也就是UIViewController的View。
如果控制器的View未處理事件辕翰,則會交給控制器處理夺衍。
控制器未處理則會交給UIWindow。
然后會交給UIApplication喜命。
最后交給UIApplicationDelegate沟沙,如果其未處理則丟棄事件
UITextField ——> UIView ——> UIView ——> UIViewController
——> UIWindow ——> UIApplication ——> UIApplicationDelegation
圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的,
則其nextResponder為UIViewController對象壁榕;
若是直接add在UIWindow上的矛紫,則其nextResponder為UIWindow對象。
可以用以下方式打印一個響應(yīng)鏈中的每一個響應(yīng)對象牌里,
在第一響應(yīng)者的 touchBegin:withEvent: 方法中調(diào)用即可(別忘了調(diào)用父類的方法)
例如某View的touch Begin:WithEvent:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
[self printResponderChain];
[super touchesBegan:touches withEvent:event];
}
- (void)printResponderChain {
UIResponder *responder = self;
printf("%s",[NSStringFromClass([responder class]) UTF8String]);
while (responder.nextResponder) {
responder = responder.nextResponder;
printf(" --> %s",[NSStringFromClass([responder class]) UTF8String]);
}
}
四.UIGestureRecognizer颊咬、UIControl
上面我們講述了UIResponder響應(yīng)觸摸事件的過程,但除了UIResponder之外,UIGestureRecognizer喳篇、UIControl同樣具備對事件的處理能力敞临。
以下將通過結(jié)合具體的示例來講解UIGestureRecognizer和UIControl是如何處理觸摸事件的。
#pragma mark -------------------------- Life Circle
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"分類";
// view tap
FJFTapView *tmpContainerView = [[FJFTapView alloc] initWithFrame:CGRectMake(50, 80, 260, 300)];
tmpContainerView.backgroundColor = [UIColor redColor];
FJFTapGestureRecognizer *tapGesture = [[FJFTapGestureRecognizer alloc] initWithTarget:self action:@selector(viewTap:)];
[tmpContainerView addGestureRecognizer:tapGesture];
[self.view addSubview:tmpContainerView];
// view longPress
FJFLongPressView *tmpLongPressView = [[FJFLongPressView alloc] initWithFrame:CGRectMake(50, 400, 260, 200)];
tmpLongPressView.backgroundColor = [UIColor grayColor];
FJFLongPressGestureRecognizer *longPressGesture = [[FJFLongPressGestureRecognizer alloc] initWithTarget:self action:@selector(viewlongPress:)];
[tmpLongPressView addGestureRecognizer:longPressGesture];
[self.view addSubview:tmpLongPressView];
// button
FJFButton *tmpButton = [[FJFButton alloc] initWithFrame:CGRectMake(100, 50, 120, 80)];
tmpButton.backgroundColor = [UIColor greenColor];
[tmpButton setTitle:@"UIButton" forState:UIControlStateNormal];
[tmpButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[tmpButton addTarget:self action:@selector(tmpButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[tmpContainerView addSubview:tmpButton];
// imageControl
FJFImageControl *imageControl = [[FJFImageControl alloc] initWithFrame:CGRectMake(100, 150, 120, 80) title:@"imageControl" iconImageName:@"ic_red_box.png"];
imageControl.backgroundColor = [UIColor blueColor];
[imageControl addTarget:self action:@selector(imageControlTouch:) forControlEvents:UIControlEventTouchUpInside];
[tmpContainerView addSubview:imageControl];
}
#pragma mark -------------------------- Response Event
// tap
- (void)viewTap:(UITapGestureRecognizer *)tap {
NSLog(@"%s", __FUNCTION__);
}
// longPress
- (void)viewlongPress:(UILongPressGestureRecognizer *)longPress {
NSLog(@"%s", __FUNCTION__);
}
// buttonClicked
- (void)tmpButtonClicked:(UIButton *)sender {
NSLog(@"%s", __FUNCTION__);
}
// controlTouch
- (void)imageControlTouch:(FJFImageControl *)imageControl {
NSLog(@"%s", __FUNCTION__);
}
如代碼所示:
FJFTapView 添加了繼承自UITapGestureRecognizer的FJFTapGestureRecognizer 單擊手勢
FJFLongPressView 添加了繼承自UILongPressGestureRecognizer的FJFLongPressGestureRecognizer 長按手勢
UIButton 添加 點擊事件
FJFImageControl 繼承自UIControl麸澜,也添加了點擊事件挺尿,
且UIButton和FJFImageControl都是FJFTapView的子視圖。
觀察各種情況的日志:
1.點擊FJFTapView:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapView touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFTapView touchesCancelled:withEvent:]
2.長按FJFLongPressView:
[FJFLongPressGestureRecognizer touchesBegan:withEvent:]
[FJFLongPressView touchesBegan:withEvent:]
[FJFThreeViewController viewlongPress:]
[FJFLongPressView touchesCancelled:withEvent:]
[FJFLongPressGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewlongPress:]
3.點擊UIButton:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFButton touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFButton touchesEnded:withEvent:]
[FJFThreeViewController tmpButtonClicked:]
4.點擊FJFImageControl:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFImageControl touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFImageControl touchesCancelled:withEvent:]
接下來我們一一解釋這些現(xiàn)象:
- UIGestureRecognizer:
手勢分為離散型手勢(discrete gestures)和持續(xù)型手勢(continuous gesture)痰憎。系統(tǒng)提供的離散型手勢包括點按手勢(UITapGestureRecognizer)和輕掃手勢(UISwipeGestureRecognizer)票髓,其余均為持續(xù)型手勢。
兩者主要區(qū)別在于狀態(tài)變化過程:
離散型:
識別成功:Possible —> Recognized
識別失斚吃拧:Possible —> Failed
持續(xù)型:
完整識別:Possible —> Began —> [Changed] —> Ended
不完整識別:Possible —> Began —> [Changed] —> Cancel
A. 離散型手勢
從點擊FJFTapView
的日志可以分析:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapView touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFTapView touchesCancelled:withEvent:]
UIWindow
在將事件傳遞給第一響應(yīng)者FJFTapView
之前洽沟,先將事件傳遞給相關(guān)的手勢識別器FJFTapGestureRecognizer
,若手勢成功識別事件蜗细,就會取消
第一響應(yīng)者FJFTapView
對事件的響
應(yīng)裆操;若手勢沒能識別事件,
第一響應(yīng)者FJFTapView
就會接手事件的處理炉媒。
這里我們可以得出:
UIGestureRecognizer
比UIResponder
具有更高的事件響應(yīng)的優(yōu)先級
這個結(jié)論我們也可以從官方文檔中得出:
A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties.
還有一點需要注意的是:
UIGestureRecognizer對事件的響應(yīng)也是通過touch相關(guān)的4個方法來實現(xiàn)的踪区,而這4個方法聲明在UIGestureRecognizerSubclass.h中。
而這里UIWindow之所以知道要把事件傳遞給哪些手勢識別器吊骤,主要還是通過UIEvent里面的gestureRecognizers數(shù)組來獲取的缎岗,而數(shù)組里面的手勢識別器是在Hit-Test View尋找第一響應(yīng)者過程中填充的。
因此我們可以分析日志:
UIWindow 先將事件傳遞給gestureRecognizers數(shù)組里的手勢識別器白粉,然后再傳遞給第一響應(yīng)者FJFTapView.
因為手勢識別器識別事件传泊,需要一定時間,因此FJFTapView先調(diào)用了touchesBegan鸭巴,這是因為FJFTapGestureRecognizer成功識別了事件眷细,UIApplication就會取消FJFTapView對事件的響應(yīng)。
B. 持續(xù)型手勢
從點擊FJFLongPressView日志分析:
[FJFLongPressGestureRecognizer touchesBegan:withEvent:]
[FJFLongPressView touchesBegan:withEvent:]
[FJFThreeViewController viewlongPress:]
[FJFLongPressView touchesCancelled:withEvent:]
[FJFLongPressGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewlongPress:]
從日志我們可以看出長按手勢回調(diào)了兩次,我們通過分析兩次調(diào)用的堆棧:
我們可以看出第一次調(diào)用是在runloop中通知監(jiān)聽的手勢識別器的觀察者鹃祖,來通知長按手勢識別器對長按事件進行響應(yīng)溪椎,此時手勢識別器的state為UIGestureRecognizerStateBegan。
第二次調(diào)用是UIWindow 先將事件傳遞給UIEvent的gestureRecognizers數(shù)組里的手勢識別器恬口,然后長按手勢識別器FJFLongPressGestureRecognizer識別成功進行回調(diào),此時手勢識別器的state為UIGestureRecognizerStateEnded校读。
這里的調(diào)用邏輯其實跟單擊手勢識別器FJFTapGestureRecognizer相似,主要區(qū)別在于長按手勢識別器FJFLongPressGestureRecognizer調(diào)用了兩次祖能。
C. 總結(jié)
當(dāng)觸摸發(fā)生或者觸摸的狀態(tài)發(fā)生變化時地熄,UIWindow都會傳遞事件尋求響應(yīng)。
UIWindow先將觸摸事件傳遞給響應(yīng)鏈上綁定的手勢識別器芯杀,再發(fā)送給觸摸對象對應(yīng)的第一響應(yīng)者。
手勢識別器識別手勢期間,若觸摸對象的觸摸狀態(tài)發(fā)生變化揭厚,事件都是先發(fā)送給手勢識別器却特,再發(fā)送給第一響應(yīng)者。
手勢識別器如果成功識別手勢筛圆,則通知UIApplication取消第一響應(yīng)者對于事件的響應(yīng)裂明,并停止向第一響應(yīng)者發(fā)送事件。
如果手勢識別器未能識別手勢太援,而此時觸摸并未結(jié)束闽晦,則停止向手勢識別器發(fā)送事件,僅向第一響應(yīng)者發(fā)送事件提岔。
如果手勢識別器未能識別手勢仙蛉,且此時觸摸已經(jīng)結(jié)束,則向第一響應(yīng)者發(fā)送end狀態(tài)的touch事件碱蒙,以停止對事件的響應(yīng)荠瘪。
D. 拓展
手勢識別器的3個屬性:
@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;
a. cancelsTouchesInView:
默認為YES。表示當(dāng)手勢識別器成功識別了手勢之后赛惩,會通知Application取消響應(yīng)鏈對事件的響應(yīng)哀墓,并不再傳遞事件給第一響應(yīng)者。若設(shè)置成NO喷兼,表示手勢識別成功后不取消響應(yīng)鏈對事件的響應(yīng)篮绰,事件依舊會傳遞給第一響應(yīng)者。
以點擊FJFTapView為例季惯,將tapGesture.cancelsTouchesInView = NO;輸出日志如下:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapView touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFTapView touchesEnded:withEvent:]
從日志我們可以看出吠各,即便FJFTapGestureRecognizer識別了點擊手勢,UIApplication也依舊將事件發(fā)送給FJFTapView.
b. delaysTouchesBegan:
默認為NO星瘾。默認情況下手勢識別器在識別手勢期間走孽,當(dāng)觸摸狀態(tài)發(fā)生改變時,Application都會將事件傳遞給手勢識別器和第一響應(yīng)者琳状;若設(shè)置成YES磕瓷,則表示手勢識別器在識別手勢期間,截斷事件念逞,即不會將事件發(fā)送給第一響應(yīng)者困食。
以點擊FJFTapView為例,將tapGesture.delaysTouchesBegan = YES;輸出日志如下:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
從日志可以看出翎承,手勢識別器識別手勢期間硕盹,事件不會傳遞給FJFTapView,因此FJFTapView的touchesBegan:withEvent:不會被調(diào)用叨咖;而手勢識別器成功識別手勢后瘩例,獨吞了事件啊胶,不會再傳遞給FJFTapView,因此只打印手勢識別器識別成功后手勢的綁定函數(shù)。
c. delaysTouchesEnded:
默認為YES垛贤。當(dāng)手勢識別失敗時焰坪,若此時觸摸已經(jīng)結(jié)束,會延遲一小段時間(0.15s)再調(diào)用響應(yīng)者的touchesEnded:withEvent:聘惦;若設(shè)置成NO某饰,則在手勢識別失敗時會立即通知Application發(fā)送狀態(tài)為end的touch事件給第一響應(yīng)者以調(diào)用 touchesEnded:withEvent:結(jié)束事件響應(yīng)。
2.UIControl
UIControl是系統(tǒng)提供的能夠以target-action模式處理觸摸事件的控件善绎,iOS中UIButton黔漂、UISegmentedControl、UISwitch等控件都是UIControl的子類禀酱。
值得注意的是炬守,UIConotrol是UIView的子類,因此本身也具備UIResponder應(yīng)有的身份比勉。
UIControl作為控件類的基類劳较,它是一個抽象基類,我們不能直接使用UIControl類來實例化控件浩聋,它只是為控件子類定義一些通用的接口观蜗,并提供一些基礎(chǔ)實現(xiàn),以在事件發(fā)生時衣洁,預(yù)處理這些消息并將它們發(fā)送到指定目標對象上墓捻。
關(guān)于UIControl,此處介紹兩點:
- target-action機制
- 觸摸事件優(yōu)先級
Target-Action機制
Target-action是一種設(shè)計模式坊夫,直譯過來就是”目標-行為”砖第。當(dāng)我們通過代碼為一個按鈕添加一個點擊事件時,通常是如下處理:
[button addTarget:self action:@selector(tapButton:) forControlEvents:UIControlEventTouchUpInside];
即當(dāng)事件發(fā)生時环凿,事件會被發(fā)送到控件對象中梧兼,然后再由這個控件對象去觸發(fā)target對象上的action行為,來最終處理事件智听。因此羽杰,Target-Action機制由兩部分組成:即目標對象Target和行為Selector。目標對象指定最終處理事件的對象到推,而行為Selector則是處理事件的方法考赛。
UIControl作為能夠響應(yīng)事件的控件,必然也需要待事件交互符合條件時才去響應(yīng)莉测,因此也會跟蹤事件發(fā)生的過程颜骤。不同于UIResponder以及UIGestureRecognizer通過touches系列方法跟蹤,UIControl有其獨特的跟蹤方式:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
return YES;
}
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
}
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
}
這4個方法和UIResponder的那4個方法幾乎吻合捣卤,只不過UIControl只能接收單點觸控忍抽,因此接收的參數(shù)是單個UITouch對象八孝。這幾個方法的職能也和UIResponder一致,用來跟蹤觸摸的開始鸠项、滑動唆阿、結(jié)束、取消锈锤。不過,UIControl本身也是UIResponder闲询,因此同樣有touches系列的4個方法久免。事實上,UIControl的 Tracking 系列方法是在touch 系列方法內(nèi)部調(diào)用的扭弧。比如 beginTrackingWithTouch 是在 touchesBegan 方法內(nèi)部調(diào)用的阎姥, 因此它雖然也是UIResponder,但touches 系列方法的默認實現(xiàn)和UIResponder本類還是有區(qū)別的鸽捻。
我們來分析下FJFButton的日志輸出以及調(diào)用堆棧:
日志輸出:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFButton touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFButton touchesEnded:withEvent:]
[FJFThreeViewController tmpButtonClicked:]
從以上信息呼巴,我們可以分析:
UIWindow 首先將事件傳遞給響應(yīng)鏈上綁定的手勢識別器FJFTapGestureRecognizer,再傳遞給第一響應(yīng)者FJFButton
手勢識別器FJFTapGestureRecognizer和第一響應(yīng)者FJFButton分別調(diào)用touch相關(guān)方法對事件進行識別御蒲,
最終第一響應(yīng)者FJFButton對事件進行響應(yīng)調(diào)用 sendAction:to:forEvent:將target衣赶、action以及event對象發(fā)送給UIApplication,UIApplication對象再通過 sendAction:to:from:forEvent:向target發(fā)送action厚满。
通過這個結(jié)果府瞄,我們會疑問:UIControl比其父視圖上的手勢識別器具有更高的事件響應(yīng)優(yōu)先級?
接下來我們看下繼承自UIControl的FJFImageControl的日志和調(diào)用堆棧:
日志輸出:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFImageControl touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFImageControl touchesCancelled:withEvent:]
從以上信息,我們又可以得出::UIControl比其父視圖上的手勢識別器的優(yōu)先級來的低碘箍?
經(jīng)驗證系統(tǒng)提供的有默認action操作的UIControl遵馆,例如UIbutton、UISwitch等的單擊丰榴,UIControl的響應(yīng)優(yōu)先級比手勢識別器高,而對于自定義的UIControl货邓,響應(yīng)的優(yōu)先級比手勢低。
我們在一個含有 tap gesture 的 view 上添加一個 UIButton 四濒,點擊按鈕時響應(yīng)的是 UIButton 换况。為什么呢?事實上峻黍,這個 tap gesture 并沒有獲得響應(yīng)權(quán)复隆。問題出在 UIGestureRecognizerDelegate 的 [gestureRecognizerShouldBegin:] 階段。在 [gestureRecognizerShouldBegin:] 階段首先被調(diào)用的是被觸摸視圖的 [gestureRecognizerShouldBegin:] 方法姆涩,其參數(shù)是我們的 tap gesture 挽拂。而 UIButton 的 [gestureRecognizerShouldBegin:] 實現(xiàn)中,指定對非添加在自己身上的 tap gesture 骨饿,返回 false 亏栈,即不可響應(yīng)台腥。所以點擊最終響應(yīng)的是 UIButton ,其下面視圖的 tap gesture 得不到響應(yīng)绒北。如果讀者重寫 UIButton 的 [gestureRecognizerShouldBegin:] 方法黎侈,讓其返回 true ,會發(fā)現(xiàn)點擊 UIButton 時闷游, UIButton 沒有響應(yīng)峻汉,響應(yīng)的卻是其父視圖的 tap gesture 脐往。這也說明了 UIGesture 比 UIControl 的優(yōu)先級更高。另外业簿, UIButton 的 [gestureRecognizerShouldBegin:] 實現(xiàn)中沒有對其他手勢做限制,即返回的 true 梅尤,所以你在 UIButton 上雙擊、滑動時巷燥,這些手勢都能得到其父視圖的識別。
Target-Action的管理:
UIControl通過addTarget方法和removeTarget方法來添加和刪除Target-Action的操作矾湃。
// 添加
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
// 刪除
- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
如果想獲取控件對象所有相關(guān)的target對象亡脑,則可以調(diào)用allTargets方法邀跃,該方法返回一個集合拍屑。集合中可能包含NSNull對象,表示至少有一個nil目標對象喷斋。
而如果想獲取某個target對象及事件相關(guān)的所有action蒜茴,則可以調(diào)用actionsForTarget:forControlEvent:方法粉私。
不過,這些都是UIControl開放出來的接口抄肖。我們還是想要探究一下漓摩,UIControl是如何去管理Target-Action的呢?
實際上腿椎,我們在程序某個合適的位置打個斷點來觀察UIControl的內(nèi)部結(jié)構(gòu)夭咬,可以看到這樣的結(jié)果
從圖中我們可以看出皱埠,UIControl內(nèi)部實際上是有一個可變數(shù)組(_targetActions)來保存Target-Action咖驮,數(shù)組中的每個元素是一個UIControlTargetAction對象托修。UIControlTargetAction類是一個私有類睦刃,內(nèi)部維護
@interface UIControlTargetAction : NSObject {
SEL _action;
BOOL _cancelled;
unsigned int _eventMask;// 事件類型,比如:UIControlEventTouchUpInside
id _target;
}
這四個變量,UIControl正是依據(jù)UIControlTargetAction來對事件進行處理际长。
五.事件完整響應(yīng)鏈
系統(tǒng)通過 IOKit.framework來處理硬件操作工育,其中屏幕處理也通過IOKit完成(IOKit可能是注冊監(jiān)聽了屏幕輸出的端口)
當(dāng)用戶操作屏幕如绸,IOKit收到屏幕操作旭贬,會將這次操作封裝為IOHIDEvent對象稀轨。通過mach port(IPC進程間通信)將事件轉(zhuǎn)發(fā)給SpringBoard來處理。SpringBoard是iOS系統(tǒng)的桌面程序谎势。SpringBoard收到mach port發(fā)過來的事件脏榆,喚醒main runloop來處理须喂。
SpringBoard的main runloop將事件交給當(dāng)前應(yīng)用程序的source1處理,這時候會喚醒當(dāng)前應(yīng)用程序的runloop仔役,當(dāng)前應(yīng)用程序的source1會調(diào)用__IOHIDEventSystemClientQueueCallback()函數(shù),該函數(shù)內(nèi)部會判斷又兵,是否有程序在前臺顯示沛厨,如果有則通過mach port將IOHIDEvent事件轉(zhuǎn)發(fā)給這個程序摔认。
如果前臺沒有程序在顯示参袱,則表明SpringBoard的桌面程序在前臺顯示抹蚀,也就是用戶在桌面進行了操作。
__IOHIDEventSystemClientQueueCallback()函數(shù)會將事件交給source0處理牢贸,source0會調(diào)用__UIApplicationHandleEventQueue()函數(shù)潜索,函數(shù)內(nèi)部會做具體的處理操作懂酱。例如用戶點擊了某個應(yīng)用程序的icon列牺,會將這個程序啟動。
應(yīng)用程序接收到SpringBoard傳來的消息随夸,會喚醒main runloop并將這個消息交給source1處理震放,source1調(diào)用__IOHIDEventSystemClientQueueCallback()函數(shù)殿遂,在函數(shù)內(nèi)部會將事件交給source0處理墨礁,并調(diào)用source0的__UIApplicationHandleEventQueue()函數(shù)恩静。
在__UIApplicationHandleEventQueue()函數(shù)中,會將傳遞過來的IOHIDEvent轉(zhuǎn)換為UIEvent對象咬荷。在函數(shù)內(nèi)部,將事件放入UIApplication的事件隊列唇牧,等到處理該事件時聚唐,將該事件出隊列杆查,UIApplication將事件傳遞給窗口對象(UIWindow)亲桦,如果存在多個窗口,則從后往前詢問最上層顯示的窗口
窗口UIWindow通過hitTest和pointInside操作豫领,判斷是否可以響應(yīng)事件等恐,如果窗口UIWindow不能響應(yīng)事件课蔬,則將事件傳遞給其他窗口二跋;若窗口能響應(yīng)事件,則從后往前詢問窗口的子視圖样傍。
以此類推衫哥,如果當(dāng)前視圖不能響應(yīng)事件撤逢,則將事件傳遞給同級的上一個子視圖粮坞;如果能響應(yīng)莫杈,就從后往前遍歷當(dāng)前視圖的子視圖筝闹。
如果當(dāng)前視圖的子視圖都不能響應(yīng)事件关顷,則當(dāng)前視圖就是第一響應(yīng)者。找到第一響應(yīng)者痘番,事件的傳遞的響應(yīng)鏈也就確定的汞舱。
如果第一響應(yīng)者非UIControl子類且響應(yīng)鏈上也沒有綁定手勢UIGestureRecognizer;
那么由于第一響應(yīng)者具有處理事件的最高優(yōu)先級兵拢,因此UIApplication會先將事件傳遞給它供其處理说铃。首先,UIApplication將事件通過 sendEvent: 傳遞給事件所屬的window债热,window同樣通過 sendEvent: 再將事件傳遞給hit-tested view窒篱,即第一響應(yīng)者,第一響應(yīng)者具有對事件的完全處理權(quán)墙杯,默認對事件不進行處理高镐,傳遞給下一個響應(yīng)者(nextResponder)嫉髓;如果響應(yīng)鏈上的對象一直沒有處理該事件邑闲,則最后會交給UIApplication苫耸,如果UIApplication實現(xiàn)代理褪子,會交給UIApplicationDelegate褐筛,如果UIApplicationDelegate沒處理渔扎,則該事件會被丟棄信轿。
如果第一響應(yīng)者非UIControl子類但響應(yīng)鏈上也綁定了手勢識別器UIGestureRecognizer;
UIWindow會將事件先發(fā)送給響應(yīng)鏈上綁定的手勢識別器UIGestureRecognizer财忽,再發(fā)送給第一響應(yīng)者即彪,如果手勢識別器能成功識別事件,UIApplication默認會向第一響應(yīng)者發(fā)送cancel響應(yīng)事件的命令;如果手勢識別器未能識別手勢蛹锰,而此時觸摸并未結(jié)束铜犬,則停止向手勢識別器發(fā)送事件癣猾,僅向第一響應(yīng)者發(fā)送事件纷宇。如果手勢識別器未能識別手勢龙屉,且此時觸摸已經(jīng)結(jié)束转捕,則向第一響應(yīng)者發(fā)送end狀態(tài)的touch事件五芝,以停止對事件的響應(yīng)枢步。
如果第一響應(yīng)者是自定義的UIControl的子類同時響應(yīng)鏈上也綁定了手勢識別器UIGestureRecognizer;這種情況跟第一響應(yīng)者非UIControl子類但響應(yīng)鏈上也綁定了手勢識別器UIGestureRecognizer`處理邏輯一樣;
如果第一響應(yīng)者是UIControl的子類且是系統(tǒng)類(UIButton醉途、UISwitch)同時響應(yīng)鏈上也綁定了手勢識別器UIGestureRecognizer;
UIWindow會將事件先發(fā)送給響應(yīng)鏈上綁定的手勢識別器UIGestureRecognizer隘擎,再發(fā)送給第一響應(yīng)者货葬,如果第一響應(yīng)者能響應(yīng)事件震桶,UIControl調(diào)用調(diào)用sendAction:to:forEvent:將target蹲姐、action以及event對象發(fā)送給UIApplication,UIApplication對象再通過 sendAction:to:from:forEvent:向target發(fā)送action顷扩。
參考鏈接:
http://www.reibang.com/p/df86508e2811
http://www.reibang.com/p/c294d1bd963d
http://www.reibang.com/p/c294d1bd963d