天天看點

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

調試iOS使用者互動事件響應流程一、響應鍊1.1 Next Responder1.1.1 調試nextResponder1.2 Target-Action和響應鍊1.2.1 注冊UIControlEvents1.2.2 調試UIControlEvents的傳遞結論一:Action不會在同級視圖層級中傳遞結論二:Target為空時Action仍可以被響應結論三:Target為空時Action沿響應鍊傳遞1.3 手勢識别和響應鍊1.4 修改響應鍊二、Touch事件傳遞2.1 碰撞檢測2.2 調試Touch事件傳遞步驟零:準備工作步驟一:下斷點步驟二:簡單分析 touch 事件在 Window 層的分發步驟三:分析 Touch 事件的産生步驟四:分析 touch 事件開始後的傳遞情況一:點選 Button 控件時情況二:點選 Label 視圖步驟五:分析 touch 事件結束後的傳遞三、RunLoop與事件(TODO)四、總結

調試iOS使用者互動事件響應流程

2020-03-19

通常 iOS 界面開發中處理各種使用者互動事件。其中,

UIControlEvent

以注冊的 Target-Action 的方式綁定到控件;

UIGestureRecognizer

通過

addGestureRecognizer:

添加到

UIView

gestureRecognizers

屬性中;

UIResponder

提供了

touchesBegin/Moved/Ended/Canceled/:withEvent:

motionsXXX:withEvent:

pressXX:withEvent:

系列接口,将使用者裝置的觸摸、運動、按壓事件通知到

UIResponder

對象等等。以上都是常用開發者處理使用者互動事件的方式,那麼隐藏在這些接口之下,從驅動層封裝互動事件對象到 UI 控件接收到使用者事件的流程是怎樣的呢?本文主要探讨的就是這個問題。

一、響應鍊

Apple Documentation 官方文檔Using Responders and the Responder Chain to Handle Events介紹了利用

UIResponder

的響應鍊來處理使用者事件。

UIResponder

實作了

touchesXXX

pressXXX

motionXXX

分别用于響應使用者的觸摸、按壓、運動(例如

UIEventSubtypeMotionShake

)互動事件。

UIResponder

包含

nextResponder

屬性。

UIView

UIWindow

UIController

UIApplication

都是

UIResponder

的派生類,是以都能響應以上事件。

1.1 Next Responder

響應鍊結構如下圖所示,基本上是通過

UIResponder

nextResponder

成員串聯而成,基本上是按照 view 的層級,從前向後由子視圖向父視圖傳遞,且另外附加其他規則。總的響應鍊的規則如下:

  • View 的

    nextResponder

    是其父視圖;
  • 當 View 為 Controller 的根視圖時,

    nextResponder

    是 Controller;
  • Controller 的

    nextResponder

    是 present Controller 的控制器;
  • 當 Controller 為根控制器時,

    nextResponder

    是 Window;
  • Window 的

    nextResponder

    是 Application;
  • Application 的

    nextResponder

    是 App Delegate(僅當 App Delegate 為

    UIResponder

    類型);
android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

響應鍊

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。在後面的調試過程會有展現。

1.1.1 調試nextResponder

使用一個簡單的 Demo 調試

nextResponder

。界面如下圖所示,包含三個 Label,從顔色可以判斷其層次從後往前的順序是:A >> B >> C。下面兩個按鈕另做他用,先忽略。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

運作 Demo,檢視各個元素的

nextResponder

,确實如前面所述。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

1.2 Target-Action和響應鍊

UIControl

控件與關聯的 target 對象通信,直接通過向 target 對象發送 action 消息。雖然 Action 消息雖然不是事件,但是 Action 消息的傳遞是要經過響應鍊的。當接收到使用者互動事件的控件的 target 為

nil

時,會沿着控件的響應鍊向下搜尋,直到找到實作該 action 方法的對象為止。UIKit 的編輯菜單就是通過這個機制實作的,UIKit 會沿着控件的響應鍊搜尋實作了

cut:

copy:

paste:

等方法的對象。

1.2.1 注冊UIControlEvents

UIControl

控件調用

addTarget:action:forControlEvents:

方法注冊事件時,會将建構

UIControlTargetAction

對象并将其添加到

UIControl

控件的

(NSMutableArray*)_targetActions

私有成員中,

addTarget:action:forControlEvents:

方法的 Apple Documentation 注釋中有聲明調用該方法時

UIControl

并不會持有 target 對象,是以無需考慮循環引用的問題。UIControl Events 注冊過程的簡單調試過程如下:

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

UIControl Target Action

附注: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.

1.2.2 調試UIControlEvents的傳遞

前面内容提到,控件的 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 實作):

結論一:Action不會在同級視圖層級中傳遞

一切準備就緒,運作 Demo,點選“點我前Button”,抓取到了如下日志。注意框①中指定的 target 是

self

,也就是 Controller。可以發現點選事件産生,調用了若幹次碰撞檢測(框②),若幹次

nextResponder

(框③),最終隻調用了 Controller 中“點我前Button”的 action 方法。這是因為:

  • Target-Action 消息在傳遞時,永遠不會在同級視圖層級中傳遞;
  • Target 非空,則 UIKit 在确認控件響應某個事件後,會直接給控件的 target 對象發送 action 消息,這個過程不存在任何視圖層級傳遞 或 響應鍊傳遞的過程;
android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程
結論二:Target為空時Action仍可以被響應

接下來将

addTarget:action:

中指定的 target 設為

nil

。然後在

TestEventsButton

中也添加 action 的響應代碼,如下所示。

點選“點我前Button”,抓取到了如下日志。這次,由

TestEventsButton

處理了 action 消息。說明當控件注冊 action 時指定的 target 為

nil

時,action 消息仍然可以被響應,且 action 隻響應一次。請記住,此時

nextResponder

被調用了 5 次。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程
結論三:Target為空時Action沿響應鍊傳遞

再進一步修改代碼,将結論二中

TestEventsButton

的新增代碼删除,仍然将

addTarget:action:

中指定的 target 設為

nil

。點選“點我前Button”,抓取到了如下日志。這次,處理 action 消息的是 Controller。而且從日志中我們發現,這次

nextResponder

調用了 6 次,确切地說,是在 Button

touchBegin

之後,Controller 處理 action 消息之前(如圖中紅框所示)。這是因為,target 為

nil

時,action 消息會沿着響應鍊傳遞,直到找到可以響應 action 的對象為止。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

可以繼續嘗試給“點我後Button”,直接将

self.btnFront

的注冊 Target-Action 的代碼删掉。運作 Demo,再次點選“點我前Button”,此時

didClickBtnBack

仍然不觸發。這其實隻是進一步印證了“結論一”的結論,這裡不再示範。

整個調試過程下來,可以發現,被 ButtonA 覆寫的 ButtonB,所有 action 都會被 ButtonA 攔截,被覆寫的 ButtonB 不會獲得任何觸發 action 的機會。

1.3 手勢識别和響應鍊

Gesture Recognizer 會在 View 之前接收 Touch 和 Press 事件,當 Gesture Recognizer 對一連串的 Touch 事件手勢識别失敗時,UIKit 才将這些 Touch 事件發送給 View。若 View 不處理這些 Touch 事件,UIKit 将其遞交到響應鍊。

1.4 修改響應鍊

響應鍊主要通過

nextResponder

方法串聯,是以重新實作

UIResponder

派生類的

nextResponder

方法可以實作響應鍊修改的效果。

二、Touch事件傳遞

當 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

2.1 碰撞檢測

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 的視圖。

2.2 調試Touch事件傳遞

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”就直接被跳過了。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

為驗證上面的推測。繼續在 Demo 中引入繼承自

UIView

TestEventsView

類型,套路和前面的 Button、Label 一緻,就是為了列印關鍵日志。然後将 Controller 的根視圖,也就是

self.view

的類型設定為

TestEventsView

。然後再在 Controller 的

viewDidLoad

中增加列印 Button 資訊的代碼以作對照。

準備就緒,運作 Demo,點選“點我前Button”,得到以下日志,幹擾資訊變多了,遮擋掉其中一部分。關注到紅色框中的内容,發現

self.view

hitTest:forEvent:

傳回的正是“點我前Button”,而且“點我前Button”的

hitTest:forEvent:

傳回了自身。與前面的推測完全符合。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

步驟零:準備工作

前一小節的調試過程其實已經可以證明改結論,但是由于隻是通過對有限的相關共有方法,譬如

hitTest:forEvent:

nextResponder

的調用次序的列印似乎還不夠深入。接下來用 lldb 下斷點的方式,進行調試。

在這之前需要做一些準備工作,這次是使用 lldb 調試主要通過檢視函數調用棧、寄存器資料、記憶體資料等方式分析,是以不需要列印日志的操作,況且新增的

hitTest:withEvent

nextResponder

touchesXXX

方法會徒增調用棧的層數,是以将

TestEventsLabel

TestEventsButton

TestEventsView

ViewController

的這些方法悉數屏蔽。去掉一切不必要的日志列印邏輯。

準備就緒,運作 Demo,先不急着開始,首先檢視 Demo 的視圖層級,先記住這個

UIWindow

執行個體,它是應用的主視窗,它的記憶體位址是

0x7fa8f10036b0

,後面會用到。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程
注意:從 iOS 13 開始,引入了

UIWindowScene

統一管理應用的視窗和螢幕,

UIWindowScene

包含

windows

screen

屬性。上圖所展示

UIWindowScene

隻包含了一個子 Window,實際真的如此嗎?

步驟一:下斷點

首先使用

break point -n

指令在四個關鍵方法處下斷點:

  • hitTest:withEvent:

  • nextResponder

  • touchesBegan:withEvent:

  • touchesEnded:withEvent:

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程
注意:彙編代碼中的函數通常以

pushq %rbp

movq %rsp, %rbp

開頭,其中

bp

是基位址寄存器(base pointer),

sp

是堆棧寄存器(stack pointer),

bp

儲存目前函數棧幀的基位址(棧底),

sp

儲存目前函數棧幀的下一個可配置設定位址(棧頂),函數每配置設定一個單元的棧空間,

sp

自動遞增,而

bp

保持不變。相應地,函數傳回前都會有

popq %rbp

操作。

步驟二:簡單分析 touch 事件在 Window 層的分發

點選“點我前Button”,很快觸發了第一個

hitTest:withEvent:

的斷點。先用

bt

指令檢視目前調用棧,發現第 0 幀調用了

UIAutoRotatingWindow

hitTest:withEvent:

,列印寄存器資料擷取到

r14

r15

都傳遞了

UIWindow

參數,但實際上調用該方法的是一個

UITextEffectsWindow

執行個體,

UITextEffectsWindow

UIAutoRotatingWindow

。它的記憶體位址是

0x00007fa8ebe05050

,顯然不是 main window。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

r14

傳遞的位址是

0x00007fa8f10036b0

,正是 main window。之是以是

UITextEffectsWindow

接收到

hitTest:withEvent:

是因為Window 層中的碰撞檢測是使用上圖中紅色框中的私有方法進行處理。接下來一步步弄清紅框中的碰撞檢測處理的 touch 事件的傳遞具體經由哪些 Window 執行個體。

frame select 8

跳到第 8 幀,跟蹤到了一個

UIWindow

對象

0x7fa8f10036b0

。是以,Window 層級中最先接收到 touch 事件的确實是 main window。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

依次類推列印出所有棧幀的目前對象如下(有些層級到斷點行時寄存器已經被修改,會找不到目标類型的執行個體,此時可以回到上一層列印需要傳入下一層的所有寄存器的值即可):

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。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

圖一:WindowScene與Window之間的關系

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

UITextEffectsWindow視圖層級

圖二:UITextEffectsWindow的視圖層級

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

圖三:Main Window的視圖層級

為什麼新版本 iOS 的 touch 事件傳遞過程,需要分離出 Window 層和 View 層階段?是因為自 iOS 13 起引入

UIWindowScene

後,

UITextEffectsWindow

和 main window 有各自的視圖層級,且兩者都沒有

superview

,是以必須修改 touch 的傳遞政策,讓事件都能分發到兩個 window 中。

注意:原本猜想,C 語言轉化為彙編語言時,遵循聲明一個局部變量就要配置設定一個棧空間的,調用函數時需要将形參和傳回值位址推入堆棧,然而從調試過程中檢視 Objective-C 的彙編代碼,其實作并不是如此。由于現代處理器包含了大量的高效率存儲器,是以 clang 編譯時會最大限量地合理利用起這些寄存器(通常是通用寄存器)以提高程式執行效率。通常傳遞參數用到最多的是

r12

r13

r14

r15

寄存器,但絕不僅限于以上列舉的幾個。這給源代碼調試增加了很大的難度。

步驟三:分析 Touch 事件的産生

注意這裡的 touch 事件并不是指 UIKit 的 touch event,UIKit 的 touch event 在 UIKit 接收到來自驅動層的點選事件信号後就建構了 touch 事件的

UIEvent

對象。這裡的 touch 事件是指經過碰撞檢測确定了 touch event 的響應者從

touchesBegan:withEvent:

開始傳遞之前産生的

UITouch

對象。

1、現在正式開始追蹤 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 仍然沒有命中任何視圖。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

2、繼續運作收集斷點資訊:

  • {TestEventsLabel: 0x7fd8d48071a0; baseClass = UILabel; frame = (121 162; 250 166); text = 'C'; opaque = NO; autoresize = RM+BM; layer = <_uilabellayer:>}:(Hit-Test)(調用超類的實作)
  • {TestEventsLabel: 0x7fd8d4806df0; baseClass = UILabel; frame = (82 116; 250 166); text = 'B'; opaque = NO; autoresize = RM+BM; layer = <_uilabellayer:>}:(Hit-Test)(調用超類的實作)
  • {TestEventsLabel: 0x7fd8d4805aa0; baseClass = UILabel; frame = (44 75; 250 166); text = 'A'; opaque = NO; autoresize = RM+BM; layer = <_uilabellayer:>}:(Hit-Test)(調用超類的實作)
  • {TestEventsButton: 0x7fd8d48056c0; baseClass = UIButton; frame = (121 478; 173 79); opaque = NO; autoresize = RM+BM; layer =

    }:(Hit-Test)(調用 UIControl 的實作)

Hit-Test 斷點終于命中了 Demo 的自定義 Label 和 Button 控件。根據收集的資訊,命中順序是 LabelC -> LabelB -> LabelA -> 點我前Button。此時,不急着繼續,在調試視窗中使用

bt

指令,觀察到調用棧深度已經來到了 43 層之多,如下圖所示。但是注意到一點,以上每次斷點命中,其調用棧深度都是 43 層,也就是說上面幾個同層視圖的碰撞檢測過程是循環疊代,而不是遞歸,三個

TestEventsLabel

調用

hitTest:withEvent:

都可以直接傳回

nil

不需要遞歸。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

3、繼續運作收集斷點資訊:

  • TestEventsButton

    :(Hit-Test)(調用 UIView 的實作)
  • UIButtonLabel

    :(Hit-Test)(調用超類的實作)

調用棧到達了第一個高峰 49 層,如下圖一所示。此時若點選繼續,會發現調用棧回落到 13 層,如下圖二所示。說明 Hit-Test 斷點在命中

UIButtonLabel

後,本次 Hit-Test 遞歸就傳回了。至于具體傳回什麼對象,實際上在 1.2.2 的調試日志中已經列印出來了,正是“點我前Button”。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

圖一:Hit-Test調用棧到達頂峰

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

圖二:Hit-Test調用棧回落

4、繼續運作,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 層如下圖所示。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

此時先不急着繼續。因為以上是 Hit-Test 在本次調試中的最後一次斷點命中,點選繼續 Hit-Test 遞歸必然傳回“點我前Button”,表示碰撞檢測命中了該按鈕控件。第二輪 Hit-Test 的調用棧明顯淺許多,不難發現其原因是該輪碰撞檢測沒有經過

UITextEffectsWindow

而直接從

UIWindow

開始(個中原因不太确定)。

總結 Hit-Test 的處理過程的要點是:

  • 優先檢測自己是否命中,不命中則直接忽略所有 subviews;
  • 若自己命中,則對所有子視圖按同層級視圖順序從前向後的順序依次進行碰撞檢測,是以碰撞檢測也是 superview 到 subview 的按視圖層級從後向前遞歸的過程;
  • 若所有子視圖均未命中,自己的碰撞檢測才傳回 nil。

文字表述似乎有點不太直覺,還是用咱們程式員的語言吧,僞代碼如下:

步驟四:分析 touch 事件開始後的傳遞

情況一:點選 Button 控件時

步驟三執行完成,UIKit 産生了

UITouch

事件并開始傳遞該事件。緊接在之前的基礎上繼續調試。再點選 continue,收集斷點資訊:

  • _UISystemGestureGateGestureRecognizer

    :(Touches-Began)
  • _UISystemGestureGateGestureRecognizer

    :(Touches-Began)
  • TestEventsButton

    :(Touches-Began)(調用 UIControl 的實作)

此時 Button 嘗試觸發 touchesBegan,開始

UITouch

事件傳遞。調用棧如下,是由 UIWindow 發送過來的 touch 事件。注意上面

TestEventsButton

調用的是UIControl 的實作,記住這個“存在某種問題或陰謀”,後面的部分會再次提到。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程
  • TestEventsButton

    :(Next-Responder)(調用 UIView 的實作)

終于命中了 Next-Responder 斷點,從上下兩個調用棧可以發現,

nextResponder

是在

touchBegan

方法内調用的。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

再點選 continue,繼續運作收集斷點資訊:

  • TestEventsView

    :(Next-Responder)(調用 UIView 的實作)

nextResponder

是在

touchBegan

方法内調用的,且增加了調用棧深度,說明

nextResponder

也觸發了遞歸的過程。但是遞歸的不是

nextResponder

而是

UIResponder

裡面的一個私有方法

_controlTouchBegan:withEvent:

。該方法似乎隻簡單周遊了一輪響應鍊,其他的什麼都沒做。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

再點選 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

層,調用棧達到頂峰,如下圖所示。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

在調試過程中,發現響應鍊上除了第一響應者“點我前Button”外的所有對象都沒有調用

touchesBegan:withEvent:

響應該 touch 事件。那麼這就是對 touch 事件該有的處理麼?其實不然,由于調試時點選的是 Button 控件,是以上述是對

UIControl

控件作為第一響應者的情況的,通過定制

UIControl

touchesBegan:withEvent:

方法實作的,特殊處理。上面提到的私有方法

_controlTouchBegan:withEvent:

就是為了告訴後面響應鍊後面的響應者這個 touch 事件已經被前面的 UIControl 處理了,請您不要處理該事件。

那麼

UIResponder

原始的響應流程是怎樣的呢?繼續調試情況二。

情況二:點選 Label 視圖

流程漸漸明朗的情況下,可以先

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:

也是遞歸的過程:

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

總結上面收集的資訊,

UIResponder

作為第一響應者和

UIControl

作為第一響應者的差別已經相當明顯了。當

UIResponder

作為第一響應者時,是沿着響應鍊傳遞,經過的每個對象都會觸發

touchesBegan:withEvents:

方法。

步驟五:分析 touch 事件結束後的傳遞

Touch 事件事件結束會觸發第一響應者的

touchesEnded:withEvent:

方法,具體傳遞過程和步驟四中一緻。同樣要區分

UIControl

UIResponder

的處理。

最後,無論是

UIControl

還是

UIResponder

,在完成所有

touchesEnded:withEvent:

處理後,都要額外再從第一響應者開始周遊一次響應鍊。從調用棧可以看到是為了傳遞

UIResponder

_completeForwardingTouches:phase:event

消息。具體原因不太清楚。

android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

三、RunLoop與事件(TODO)

行文至此,文章篇幅已經有點長,是以在下一篇文章中在調試這部分内容。

四、總結

  • 無論是使用

    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 消息;
  • 響應鍊是以 View 為起始,向 superview 延伸的一個反向樹型結構,通過

    UIResponder

    nextResponder

    串聯而成;
  • 當 View 作為 Controller 的根 view 時,

    nextResponder

    是 Controller;
  • 當 Controller 是由其他 Controller present 而來,則

    nextResponder

    是其 present controller;
  • 當 Controller 是 Window 的根 Controller,則

    nextResponder

    是 Window,注意調試中 Controller 的

    nextResponder

    是傳回

    nil

    ,但實際上它們确實有這層關系;
  • Window 的

    nextResponder

    是 Window Scene;
  • Window Scene 的

    nextResponder

    是 Application;
  • Application 的

    nextResponder

    是 AppDelegate(當 AppDelegate 是

    UIResponder

    類型時);
  • Hit-Test 優先檢測自己是否命中,不命中則直接忽略所有 subviews;
  • Hit-Test 若自己命中,則對所有子視圖按同層級視圖順序從前向後的順序依次進行碰撞檢測,是以碰撞檢測也是 superview 到 subview 的按視圖層級從後向前遞歸的過程;
  • Hit-Test 若未命中任何子視圖,自己的碰撞檢測才傳回 nil;
  • Hit-Test 命中目标後,産生

    UITouch

    事件,

    UITouch

    事件會沿着響應鍊傳遞到後面的所有響應者;
  • UIResponder

    作為第一響應者響應了 touch 事件,響應鍊後面的所有響應者也會觸發

    touchesXXX

    系列方法;
  • UIControl

    控件作為第一響應者響應了 touch 事件,響應鍊後面的所有響應者均不再處理該 touch 事件;
android控件的touch事件_調試iOS使用者互動事件響應流程調試iOS使用者互動事件響應流程

繼續閱讀