[TOC]程序員
2020-03-19windows
一般 iOS 界面開發中處理各類用戶交互事件。其中,UIControlEvent
以註冊的 Target-Action 的方式綁定到控件;UIGestureRecognizer
經過addGestureRecognizer:
添加到UIView
的gestureRecognizers
屬性中;UIResponder
提供了touchesBegin/Moved/Ended/Canceled/:withEvent:
、motionsXXX:withEvent:
、pressXX:withEvent:
系列接口,將用戶設備的觸摸、運動、按壓事件通知到UIResponder
對象等等。以上都是經常使用開發者處理用戶交互事件的方式,那麼隱藏在這些接口之下,從驅動層封裝交互事件對象到 UI 控件接收到用戶事件的流程是怎樣的呢?本文主要探討的就是這個問題。app
Apple Documentation 官方文檔Using Responders and the Responder Chain to Handle Events介紹了利用UIResponder
的響應鏈來處理用戶事件。UIResponder
實現了touchesXXX
、pressXXX
、motionXXX
分別用於響應用戶的觸摸、按壓、運動(例如UIEventSubtypeMotionShake
)交互事件。UIResponder
包含nextResponder
屬性。UIView
、UIWindow
、UIController
、UIApplication
都是UIResponder
的派生類,因此都能響應以上事件。ide
響應鏈結構以下圖所示,基本上是經過UIResponder
的nextResponder
成員串聯而成,基本上是按照 view 的層級,從前向後由子視圖向父視圖傳遞,且另外附加其餘規則。總的響應鏈的規則以下:函數
nextResponder
是其父視圖;nextResponder
是 Controller;nextResponder
是 present Controller 的控制器;nextResponder
是 Window;nextResponder
是 Application;nextResponder
是 App Delegate(僅當 App Delegate 爲UIResponder
類型);UIResponder
響應touchesXXX
、pressXXX
、motionXXX
事件不須要指定userInteractionEnabled
爲YES
。可是對於UIView
則須要指定userInteractionEnabled
,緣由是UIView
從新實現了這些方法。響應UIGesture
則須要指定userInteractionEnabled
,addGestureRecognizer:
是UIView
類的接口。工具
注意:新版本中,分離了 Window 和 View 的響應鏈。當 Controller 爲根控制器時,
nextResponder
其實是nil
;Windows 的nextResponder
是 Window Scene;Window Scene 的nextResponder
是 Application。在後面的調試過程會有體現。oop
使用一個簡單的 Demo 調試nextResponder
。界面以下圖所示,包含三個 Label,從顏色能夠判斷其層次從後往前的順序是:A >> B >> C。下面兩個按鈕另作他用,先忽略。ui
運行 Demo,查看各個元素的nextResponder
,確實如前面所述。spa
UIControl
控件與關聯的 target 對象通訊,直接經過向 target 對象發送 action 消息。雖然 Action 消息雖然不是事件,可是 Action 消息的傳遞是要通過響應鏈的。當接收到用戶交互事件的控件的 target 爲nil
時,會沿着控件的響應鏈向下搜索,直到找到實現該 action 方法的對象爲止。UIKit 的編輯菜單就是經過這個機制實現的,UIKit 會沿着控件的響應鏈搜索實現了cut:
、copy:
、paste:
等方法的對象。3d
當UIControl
控件調用addTarget:action:forControlEvents:
方法註冊事件時,會將構建UIControlTargetAction
對象並將其添加到UIControl
控件的(NSMutableArray*)_targetActions
私有成員中,addTarget:action:forControlEvents:
方法的 Apple Documentation 註釋中有聲明調用該方法時UIControl
並不會持有 target 對象,所以無需考慮循環引用的問題。UIControl Events 註冊過程的簡單調試過程以下:
附註:The control does not retain the object in the target parameter. It is your responsibility to maintain a strong reference to the target object while it is attached to a control.
前面內容提到,控件的 action 是沿着響應鏈傳遞的,那麼,當兩個控件在界面上存在重合的區域,那麼在重合區域觸發用戶事件時,action 消息會在哪一個控件上產生呢?在 1.1.1 中的兩個重合的按鈕就是爲了驗證這個問題。
稍微改造一下 1.1.1 的 Demo 程序,將 Label A、B、C 指定爲自定義的繼承自UILabel
的類型TestEventsLabel
,將兩個 Button 指定爲繼承自UIButton
的TestEventsButton
類型。而後在TestEventsLabel
、TestEventsButton
、ViewController
中,爲touchesXXX:
系列方法、nextResponder
方法、hitTest:withEvent:
方法添加打印日誌的代碼,以TestEventsButton
的實現爲例(固然也能夠用 AOP 實現):
@implementation TestEventsButton
-(UIResponder *)nextResponder{
UIResponder* responder = [super nextResponder];
NSLog(@"Next Responder Button %@ - return responder: %@", [self titleForState:UIControlStateNormal], responder);
return responder;
}
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView* view = [super hitTest:point withEvent:event];
NSLog(@"Hit Test Button %@ - return view: %@", [self titleForState:UIControlStateNormal], view);
return view;
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];
NSLog(@"Button %@ - %s", [self titleForState:UIControlStateNormal], __func__);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesEnded:touches withEvent:event];
NSLog(@"Button %@ - %s", [self titleForState:UIControlStateNormal], __func__);
}
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesMoved:touches withEvent:event];
NSLog(@"Button %@ - %s", [self titleForState:UIControlStateNormal], __func__);
}
-(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesCancelled:touches withEvent:event];
NSLog(@"Button %@ - %s", [self titleForState:UIControlStateNormal], __func__);
}
@end
複製代碼
一切準備就緒,運行 Demo,點擊「點我前Button」,抓取到了以下日誌。注意框①中指定的 target 是self
,也就是 Controller。能夠發現點擊事件產生,調用了若干次碰撞檢測(框②),若干次nextResponder
(框③),最終只調用了 Controller 中「點我前Button」的 action 方法。這是由於:
接下來將addTarget:action:
中指定的 target 設爲nil
。而後在TestEventsButton
中也添加 action 的響應代碼,以下所示。
-(void)didClickBtnFront:(id)sender{
NSLog(@"In Button 點我前Button Did Click Action %s", __func__);
}
-(void)didClickBtnBack:(id)sender{
NSLog(@"In Button 點我後Button Did Click Action %s", __func__);
}
複製代碼
點擊「點我前Button」,抓取到了以下日誌。此次,由TestEventsButton
處理了 action 消息。說明當控件註冊 action 時指定的 target 爲nil
時,action 消息仍然能夠被響應,且 action 只響應一次。請記住,此時nextResponder
被調用了 5 次。
再進一步修改代碼,將結論二中TestEventsButton
的新增代碼刪除,仍然將addTarget:action:
中指定的 target 設爲nil
。點擊「點我前Button」,抓取到了以下日誌。此次,處理 action 消息的是 Controller。並且從日誌中咱們發現,此次nextResponder
調用了 6 次,確切地說,是在 Button touchBegin
以後,Controller 處理 action 消息以前(如圖中紅框所示)。這是由於,target 爲nil
時,action 消息會沿着響應鏈傳遞,直到找到能夠響應 action 的對象爲止。
能夠繼續嘗試給「點我後Button」,直接將self.btnFront
的註冊 Target-Action 的代碼刪掉。運行 Demo,再次點擊「點我前Button」,此時didClickBtnBack
仍然不觸發。這其實只是進一步印證了「結論一」的結論,這裏再也不演示。
整個調試過程下來,能夠發現,被 ButtonA 覆蓋的 ButtonB,全部 action 都會被 ButtonA 攔截,被覆蓋的 ButtonB 不會得到任何觸發 action 的機會。
Gesture Recognizer 會在 View 以前接收 Touch 和 Press 事件,當 Gesture Recognizer 對一連串的 Touch 事件手勢識別失敗時,UIKit 纔將這些 Touch 事件發送給 View。若 View 不處理這些 Touch 事件,UIKit 將其遞交到響應鏈。
響應鏈主要經過nextResponder
方法串聯,所以從新實現UIResponder
派生類的nextResponder
方法能夠實現響應鏈修改的效果。
當 touch 事件發生時,UIKit 會構建一個與 view 關聯的UITouch
實例,當 touch 位置變化時,僅改變 touch 的屬性值,但不包括其view
屬性。即便 touch 移出了 view 的範圍,view
屬性仍然是不變的。UITouch
的gestureRecognizers
屬性表示正在處理該 touch 事件的全部 gesture recognizer。UITouch
的timestamp
屬性表示 touch 事件的發生時間或者上一次修改的時間。UITouch
的phase
屬性,表示 touch 事件當前所在的生命週期階段,包括UITouchPhaseMoved
、UITouchPhaseBegan
、UITouchPhaseStationary
、UITouchPhaseEnded
、UITouchPhaseCanceled
。
UIKit 經過 hit-test 碰撞檢測肯定哪些 View 須要響應 touch 事件,hit-test 經過比較 touch 的位置與 View 的 bounds 判斷 touch 是否與 View 相交。Hit-test 是在 View 的視圖層級中,取層級最深的子視圖,做爲 touch 事件的 first responder,而後從前向後遞歸地對每一個子視圖進行 Hit-test,直到子視圖命中,直接返回命中的子視圖。
Hit-test 經過UIView
的hitTest:withEvent:
方法實現,若 touch 的位置超出了 view 的 bounds 範圍,則hitTest:withEvent:
會忽略該 view 及其全部子視圖。因此,當 view 的maskToBounds
爲NO
時,即便 touch 看起來落在了某個視圖上,但只要 touch 位置超出了 view 或者其 super view 的 bounds 範圍,則該 view 仍然會接收不到 touch 事件。
碰撞檢測方法- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
中,point
參數是碰撞檢測點在事件發生的 view 的座標系中的座標;event
參數是使用本次碰撞檢測的UIEvent
事件。當目標檢測點不在當前 view 的範圍內時,該方法返回nil
,反之則返回 view 自己。hitTest:withEvent:
方法是經過調用UIView
的- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
方法實現的,該方法忽略userInteractionEnabled
爲NO
或者 alpha 值小於 0.01 的視圖。
Touch 事件傳遞過程主要調用了hitTest:withEvent:
方法,Touch 事件若未被 gesture recognizer 捕捉則最終會去到touchesXXX:
系列方法。在響應鏈的調試時,已經見到很多hitTest:withEvent:
調用的痕跡。
在第一章「結論一」的運行日誌中,發現點擊「點我前Button」時,也對 Label A、B、C 作了碰撞檢測,且並無對「點我後Button」作碰撞檢測。注意到 Label 和 Button 都是self.view
的子視圖,且 Label A、B、C 在「點我前Button」以前,「點我後Button」以後。前面提到過:Hit-test 是在 View 的視圖層級中,取層級最深的子視圖,做爲 touch 事件的 first responder,而後從前向後遞歸地對每一個子視圖進行 Hit-test。所以,self.view
調用 Hit-Test 時,首先找到的是 Label C。而後,從前向後遞歸調用hitTest:withEvent:
,所以纔會有C >> B >> A >> 點我前Button
的順序。爲何到「點我後Button」沒有遞歸到呢?這是由於self.view
的hitTest:withEvent:
在迭代到「點我前Button」時命中了目標,所以直接返回「點我前Button」。而更後面的「點我前Button」就直接被跳過了。
爲驗證上面的推測。繼續在 Demo 中引入繼承自UIView
的TestEventsView
類型,套路和前面的 Button、Label 一致,就是爲了打印關鍵日誌。而後將 Controller 的根視圖,也就是self.view
的類型設置爲TestEventsView
。而後再在 Controller 的viewDidLoad
中增長打印 Button 信息的代碼以做對照。
準備就緒,運行 Demo,點擊「點我前Button」,獲得如下日誌,干擾信息變多了,遮擋掉其中一部分。關注到紅色框中的內容,發現self.view
的hitTest:forEvent:
返回的正是「點我前Button」,並且「點我前Button」的hitTest:forEvent:
返回了自身。與前面的推測徹底符合。
前一小節的調試過程其實已經能夠證實改結論,可是因爲只是經過對有限的相關共有方法,譬如hitTest:forEvent:
、nextResponder
的調用次序的打印彷佛還不夠深刻。接下來用 lldb 下斷點的方式,進行調試。
在這以前須要作一些準備工做,此次是使用 lldb 調試主要經過查看函數調用棧、寄存器數據、內存數據等方式分析,所以不須要打印日誌的操做,何況新增的hitTest:withEvent
、nextResponder
、touchesXXX
方法會徒增調用棧的層數,所以將TestEventsLabel
、TestEventsButton
、TestEventsView
、ViewController
的這些方法悉數屏蔽。去掉一切沒必要要的日誌打印邏輯。
準備就緒,運行 Demo,先不急着開始,首先查看 Demo 的視圖層級,先記住這個UIWindow
實例,它是應用的主窗口,它的內存地址是0x7fa8f10036b0
,後面會用到。
注意:從 iOS 13 開始,引入了
UIWindowScene
統一管理應用的窗口和屏幕,UIWindowScene
包含windows
和screen
屬性。上圖所展現UIWindowScene
只包含了一個子 Window,實際真的如此嗎?
首先使用break point -n
命令在四個關鍵方法處下斷點:
hitTest:withEvent:
nextResponder
touchesBegan:withEvent:
touchesEnded:withEvent:
注意:彙編代碼中的函數一般以
pushq %rbp
、movq %rsp, %rbp
開頭,其中bp
是基地址寄存器(base pointer),sp
是堆棧寄存器(stack pointer),bp
保存當前函數棧幀的基地址(棧底),sp
保存當前函數棧幀的下一個可分配地址(棧頂),函數每分配一個單元的棧空間,sp
自動遞增,而bp
保持不變。相應地,函數返回前都會有popq %rbp
操做。
點擊「點我前Button」,很快觸發了第一個hitTest:withEvent:
的斷點。先用bt
命令查看當前調用棧,發現第 0 幀調用了UIAutoRotatingWindow
的hitTest:withEvent:
,打印寄存器數據獲取到r14
、r15
都傳遞了UIWindow
參數,但實際上調用該方法的是一個UITextEffectsWindow
實例,UITextEffectsWindow
是UIAutoRotatingWindow
。它的內存地址是0x00007fa8ebe05050
,顯然不是 main window。
而r14
傳遞的地址是0x00007fa8f10036b0
,正是 main window。之因此是UITextEffectsWindow
接收到hitTest:withEvent:
是由於Window 層中的碰撞檢測是使用上圖中紅色框中的私有方法進行處理。接下來一步步弄清紅框中的碰撞檢測處理的 touch 事件的傳遞具體經由哪些 Window 實例。frame select 8
跳到第 8 幀,跟蹤到了一個UIWindow
對象0x7fa8f10036b0
。所以,Window 層級中最早接收到 touch 事件的確實是 main window。
依次類推打印出全部棧幀的當前對象以下(有些層級到斷點行時寄存器已經被修改,會找不到目標類型的實例,此時能夠回到上一層打印須要傳入下一層的全部寄存器的值便可):
frame 0: UITextEffectsWindow 0x00007fa8ebe05050 frame 1: UITextEffectsWindow 0x00007fa8ebe05050 frame 2: UITextEffectsWindow 0x00007fa8ebe05050 frame 3: UIWindow +(類方法) frame 4: UIWindowScene -(nil不須要使用self) frame 5: UIWindowScene 0x00007fa8ebd06c50 frame 6: UIWindowScene 0x00007fa8ebd06c50 frame 7: UIWindow +(類方法) frame 8: UIWindow 0x00007fa8f10036b0
能夠進一步使用 lldb 調試命令理清上面幾個對象之間的關係。首先是圖一中 window scene 與 window 之間的關係。圖二則打印出了UITextEffectsWindow
的視圖層級。圖三是 main window 的視圖層級,注意到紅框中的對象,是否似曾相識?沒錯,到這裏追蹤到 Controller 的TestEventsView
類型的根 view。
爲何新版本 iOS 的 touch 事件傳遞過程,須要分離出 Window 層和 View 層階段?是由於自 iOS 13 起引入UIWindowScene
後,UITextEffectsWindow
和 main window 有各自的視圖層級,且二者都沒有superview
,所以必須修改 touch 的傳遞策略,讓事件都能分發到兩個 window 中。
注意:本來猜測,C 語言轉化爲彙編語言時,遵循聲明一個局部變量就要分配一個棧空間的,調用函數時須要將形參和返回值地址推入堆棧,然而從調試過程當中查看 Objective-C 的彙編代碼,其實現並非如此。因爲現代處理器包含了大量的高效率存儲器,所以 clang 編譯時會最大限量地合理利用起這些寄存器(一般是通用寄存器)以提升程序執行效率。一般傳遞參數用到最多的是
r12
、r13
、r14
、r15
寄存器,但毫不僅限於以上列舉的幾個。這給源代碼調試增長了很大的難度。
注意這裏的 touch 事件並非指 UIKit 的 touch event,UIKit 的 touch event 在 UIKit 接收到來自驅動層的點擊事件信號後就構建了 touch 事件的UIEvent
對象。這裏的 touch 事件是指通過碰撞檢測肯定了 touch event 的響應者從touchesBegan:withEvent:
開始傳遞以前產生的UITouch
對象。
一、如今正式開始追蹤 touch 事件。已知,步驟二中打斷的第一次hitTest:withEvent:
命中,其調用對象是UITextEffectsWindow
實例。此時點擊調試工具欄中的「continue」按鈕,繼續執行。
注意:因爲調試過程比較長,致使繼續運行時 lldb 被打斷須要從新運行。不過問題不大,由於前面的工做已經肯定了須要追蹤的關鍵對象。所以從新運行後,從新下斷點,再記錄一次關鍵對象的地址便可。
開始收集斷點命中(包括第一次命中):
UITextEffectsWindow
:(Hit-Test)UITextEffectsWindow
:(Hit-Test)(調用 UIView 的實現)UIInputSetContainerView
:(Hit-Test)UIInputSetContainerView
:(Hit-Test)(調用 UIView 的實現)UIEditingOverlayGestureView
:(Hit-Test)UIEditingOverlayGestureView
:(Hit-Test)(調用 UIView 的實現)UIInputSetHostView
:(Hit-Test)UIInputSetHostView
:(Hit-Test)(調用 UIView 的實現)UIWindow
:(Hit-Test)(調用 UIView 的實現)UITransitionView
:(Hit-Test)UITransitionView
:(Hit-Test)(調用 UIView 的實現)UIDropShadowView
:(Hit-Test)UIDropShadowView
:(Hit-Test)(調用 UIView 的實現)TestEventsView
:(Hit-Test)(調用 UIView 的實現)至此 Hit-Test 斷點命中了以前自定義的 Controller 的TestEventsView
類型的根類,在這裏打印一下調用棧。調用棧增長至 38 層以下圖。並且上面的層次都是在調用hitTest:withEvents
方法,這是個明顯的遞歸調用的表現。並且到此爲止,Hit-Test 仍然沒有命中任何視圖。
二、繼續運行收集斷點信息:
Hit-Test 斷點終於命中了 Demo 的自定義 Label 和 Button 控件。根據收集的信息,命中順序是 LabelC -> LabelB -> LabelA -> 點我前Button。此時,不急着繼續,在調試窗口中使用bt
指令,觀察到調用棧深度已經來到了 43 層之多,以下圖所示。可是注意到一點,以上每次斷點命中,其調用棧深度都是 43 層,也就是說上面幾個同層視圖的碰撞檢測過程是循環迭代,而不是遞歸,三個TestEventsLabel
調用hitTest:withEvent:
均可以直接返回nil
不須要遞歸。
三、繼續運行收集斷點信息:
TestEventsButton
:(Hit-Test)(調用 UIView 的實現)UIButtonLabel
:(Hit-Test)(調用超類的實現)調用棧到達了第一個高峯 49 層,以下圖一所示。此時若點擊繼續,會發現調用棧回落到 13 層,以下圖二所示。說明 Hit-Test 斷點在命中UIButtonLabel
後,本次 Hit-Test 遞歸就返回了。至於具體返回什麼對象,實際上在 1.2.2 的調試日誌中已經打印出來了,正是「點我前Button」。
四、繼續運行,Demo 會進入第二次 Hit-Test 遞歸,之因此一次點擊事件引起了兩輪遞歸,是由於 touch 事件在開始和結束時,各進行了一輪碰撞檢測。繼續收集斷點信息:
UIWindow
:(Hit-Test)(調用 UIView 的實現)UITransitionView
:(Hit-Test)UITransitionView
:(Hit-Test)(調用 UIView 的實現)UIDropShadowView
:(Hit-Test)UIDropShadowView
:(Hit-Test)(調用 UIView 的實現)TestEventsView
:(Hit-Test)(調用 UIView 的實現)TestEventsLabel
:(Hit-Test)(調用 UIView 的實現)TestEventsLabel
:(Hit-Test)(調用 UIView 的實現)TestEventsLabel
:(Hit-Test)(調用 UIView 的實現)TestEventsButton
:(Hit-Test)(調用 UIControl 的實現)TestEventsButton
:(Hit-Test)(調用 UIView 的實現)UIButtonLabel
:(Hit-Test)(調用 UIView 的實現)調用棧再次到達了高峯 41 層以下圖所示。
此時先不急着繼續。由於以上是 Hit-Test 在本次調試中的最後一次斷點命中,點擊繼續 Hit-Test 遞歸必然返回「點我前Button」,表示碰撞檢測命中了該按鈕控件。第二輪 Hit-Test 的調用棧明顯淺許多,不難發現其緣由是該輪碰撞檢測沒有通過UITextEffectsWindow
而直接從UIWindow
開始(箇中緣由不太肯定)。
總結 Hit-Test 的處理過程的要點是:
文字表述彷佛有點不太直觀,仍是用我們程序員的語言吧,僞代碼以下:
- (UIView * _Nullable)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1. 優先檢測本身,不命中則馬上排除
BOOL isHit = [self pointInside:point withEvent:event];
if(!isHit){
return nil;
}
// 2. 從前向後循環迭代全部子視圖
for(UIView* subviews in subviews){
// 跨視圖層級從 superview 向 subview 遞歸
UIView* hitView = [subviews hitTest:point withEvent:event];
if(hitView)
return hitView;
}
// 3. 全部子視圖未命中返回nil
return nil;
}
複製代碼
步驟三執行完成,UIKit 產生了UITouch
事件並開始傳遞該事件。緊接在以前的基礎上繼續調試。再點擊 continue,收集斷點信息:
_UISystemGestureGateGestureRecognizer
:(Touches-Began)_UISystemGestureGateGestureRecognizer
:(Touches-Began)TestEventsButton
:(Touches-Began)(調用 UIControl 的實現)此時 Button 嘗試觸發 touchesBegan,開始UITouch
事件傳遞。調用棧以下,是由 UIWindow 發送過來的 touch 事件。注意上面TestEventsButton
調用的是UIControl 的實現,記住這個「貓膩」,後面的部分會再次提到。
TestEventsButton
:(Next-Responder)(調用 UIView 的實現)終於命中了 Next-Responder 斷點,從上下兩個調用棧能夠發現,nextResponder
是在touchBegan
方法內調用的。
再點擊 continue,繼續運行收集斷點信息:
TestEventsView
:(Next-Responder)(調用 UIView 的實現)nextResponder
是在touchBegan
方法內調用的,且增長了調用棧深度,說明nextResponder
也觸發了遞歸的過程。可是遞歸的不是nextResponder
而是UIResponder
裏面的一個私有方法_controlTouchBegan:withEvent:
。該方法彷佛只簡單遍歷了一輪響應鏈,其餘的什麼都沒作。
再點擊 continue,繼續運行收集斷點信息:
UIViewController
:(Next-Responder)(調用 UIViewController 的實現)UIDropShadowView
:(Next-Responder)(調用 UIView 的實現)UITransitionView
:(Next-Responder)(調用 UIView 的實現)UIWindow
:(Next-Responder)UIWindowScene
:(Next-Responder)(調用 UIScene 的實現)UIApplication
:(Next-Responder)AppDelegate
:(Next-Responder)(調用 UIResponder 的實現)在AppDelegate
層,調用棧達到頂峯,以下圖所示。
在調試過程當中,發現響應鏈上除了第一響應者「點我前Button」外的全部對象都沒有調用touchesBegan:withEvent:
響應該 touch 事件。那麼這就是對 touch 事件該有的處理麼?其實否則,因爲調試時點擊的是 Button 控件,所以上述是對UIControl
控件做爲第一響應者的狀況的,經過定製UIControl
類touchesBegan:withEvent:
方法實現的,特殊處理。上面提到的私有方法_controlTouchBegan:withEvent:
就是爲了告訴後面響應鏈後面的響應者這個 touch 事件已經被前面的 UIControl 處理了,請您不要處理該事件。
那麼UIResponder
原始的響應流程是怎樣的呢?繼續調試狀況二。
流程漸漸明朗的狀況下,能夠先breakpoint disable
終止上面的斷點,而後breakpoint delete XXX
刪除掉hitTest:withEvent:
斷點,以減小打斷次數。解屏蔽掉以前屏蔽的打印日誌的代碼,由於當斷點命中 Demo 中的自定義類時,能夠直接判定nextResponder
的觸發類。
點擊界面中的 Label C。開始收集信息(省略自定義日誌打印方法只保留原始方法):
_UISystemGestureGateGestureRecognizer
:(Touches-Began)_UISystemGestureGateGestureRecognizer
:(Touches-Began)TestEventsLabel
:(Touches-Began)(調用 UIResponder 的實現)TestEventsLabel
:(Next-Responder)(調用 UIView 的實現)TestEventsView
:(Touch-Began)(調用 UIResponder 的實現)TestEventsView
:(Next-Responder)(調用 UIView 的實現)UIViewController
:(Touch-Began)(調用 UIResponder 的實現)UIViewController
:(Next-Responder)(調用 UIViewController 的實現)UIDropShadowView
:(Touch-Began)(調用 UIResponder 的實現)UIDropShadowView
:(Next-Responder)(調用 UIView 的實現)UITransitionView
:(Touch-Began)(調用 UIResponder 的實現)UITransitionView
:(Next-Responder)(調用 UIView 的實現)UIWindow
:(Touch-Began)(調用 UIResponder 的實現)UIWindow
:(Next-Responder)UIWindowScene
:(Touch-Began)(調用 UIResponder 的實現)UIWindowScene
:(Next-Responder)(調用 UIScene 的實現)UIApplication
:(Touch-Began)(調用 UIResponder 的實現)UIApplication
:(Next-Responder)AppDelegate
:(Touch-Began)(調用 UIResponder 的實現)AppDelegate
:(Next-Responder)(調用 UIResponder 的實現)至此先看一下調用棧,顯然touchesBegan:withEvent:
也是遞歸的過程:
總結上面收集的信息,UIResponder
做爲第一響應者和UIControl
做爲第一響應者的區別已經至關明顯了。當UIResponder
做爲第一響應者時,是沿着響應鏈傳遞,通過的每一個對象都會觸發touchesBegan:withEvents:
方法。
Touch 事件事件結束會觸發第一響應者的touchesEnded:withEvent:
方法,具體傳遞過程和步驟四中一致。一樣要區分UIControl
和UIResponder
的處理。
最後,不管是UIControl
仍是UIResponder
,在完成全部touchesEnded:withEvent:
處理後,都要額外再從第一響應者開始遍歷一次響應鏈。從調用棧能夠看到是爲了傳遞UIResponder
的_completeForwardingTouches:phase:event
消息。具體緣由不太清楚。
行文至此,文章篇幅已經有點長,所以在下一篇文章中在調試這部份內容。
UIControl
的 Target-Action 方式仍是UIResponder
的touchesXXX
方式處理用戶事件,都涉及到 Hit-Test 和 響應鏈的內容;UIControl
使用 Target-Action 註冊用戶事件,當後面的控件被前面的控件覆蓋時,若用戶事件(UIEvent
)被前面的控件攔截(不管前面的控件有沒有註冊 Target-Action),則後面的控件永遠得不處處理事件的機會,即便前面的控件未註冊 Target-Action;UIControl
使用 Target-Action 註冊用戶事件,指定 Target 爲空時,Action 消息會沿着響應鏈傳遞,直到找到能響應 Action 的 Responder 爲止,Action 一旦被其中一個 Responder 響應,響應鏈後面的對象就再也不處理該 Action 消息;UIResponder
的nextResponder
串聯而成;nextResponder
是 Controller;nextResponder
是其 present controller;nextResponder
是 Window,注意調試中 Controller 的nextResponder
是返回nil
,但實際上它們確實有這層關係;nextResponder
是 Window Scene;nextResponder
是 Application;nextResponder
是 AppDelegate(當 AppDelegate 是UIResponder
類型時);UITouch
事件,UITouch
事件會沿着響應鏈傳遞到後面的全部響應者;UIResponder
做爲第一響應者響應了 touch 事件,響應鏈後面的全部響應者也會觸發touchesXXX
系列方法;UIControl
控件做爲第一響應者響應了 touch 事件,響應鏈後面的全部響應者均再也不處理該 touch 事件;