淺談事件的分發與響應
顧名思義,事件就是發生的一件事,對於APP來說,就是發生的一個操作。具體的就是使用者點選一下螢幕就會出現一個 事件 (體現為一個 UIEvent
),即一個 觸控事件 。其實,對於 iOS 裝置的使用者來說,他們操作裝置的方式主要有四種方式:觸控式螢幕幕、晃動裝置、通過遙控設施控制裝置、按壓螢幕。 對應的事件型別 UIEventType
有以下三種:
- 觸屏事件(Touch Event)
- 運動事件(Motion Event)
- 遠端控制事件(Remote-Control Event)
- 按壓事件(Presses Event)
我們的主題是探索使用者用手指點選螢幕會發生什麼,所以我們將注意力放在 觸控事件
上。
響應者物件
上面我們瞭解到,當我們點選了螢幕,就會出現一個事件。既然事件出現了,那麼就需要一個一個響應和處理這個事件的物件,那就是我們的 響應者物件 。這些響應者物件都有一個共同的特徵,就是他們都繼承自 UIResponder
。我們熟知的響應者物件有 UIApplication
、 UIWindow
、 UIViewController
和所有繼承自 UIView
的 UIKit 類
UIResponder
- 所有響應物件的基類
- 定義了處理上述各種事件的介面;
第一響應者
在觸控式螢幕幕的事件中:
- 指的是當前接受觸控的響應者物件(通常是一個UIView物件);
- 即表示當前該物件正在與使用者互動,它是響應者鏈的開端;
- 整個響應者鏈和事件分發的使命都是找出第一響應者。
響應者鏈條
上面介紹了響應者物件,也知道了 UIApplication
、 UIWindow
、 UIViewController
、 UIView
這些都是響應者。那麼一個 APP 會存在很多響應者物件。由這一系列的響應者物件就構成了一個層次結構,那就是 響應者鏈條 。

從上圖中可以看到, 響應者鏈條有以下特點 :
- 響應者鏈頭部通常是由檢視(
UIView
)構成的; - 如果該檢視是屬於檢視控制器(
UIViewController
)的,那麼下一個響應者是該檢視控制器,然後再將事件響應到它的父檢視(Super View
)中; - 如果該檢視沒有檢視控制器(
UIViewController
),那麼下一個響應者就直接是它的父檢視(Super View
); - 一直響應直至其物件是單例的視窗(
UIWindow
) - 再下一個響應者就是單例的應用(
UIApplication
),也是 響應者鏈條的終點 - 下一個響應者指向 nil ,結束整個迴圈
事件分發
回到開篇的情況,當用戶點選了一下螢幕。系統檢測到使用者的觸控事件,就會將其打包成一個事件(即 UIEvent
物件),並將這個 UIEvent
物件放入 Application 的事件佇列中。這時系統只是知道有這麼一個事件發生,雖然 響應者鏈條 中有很多有處理事件能力的響應者,但是它不知道誰才是響應這個事件的最佳人選。 因此,系統會從 UIApplication
開始,順著 響應者鏈條 向上尋找那個最佳的人選。這個尋找的過程就是 事件的分發過程 。
傳遞過程
- 第一步 :
UIApplication
將這個事件從事件佇列中拿出來,從頂部開始詢問誰才是最佳人選; - 第二步 :
UIWindow
會最先獲取到事件,並開始使用hitTest:withEvent:
來判斷下面他的子控制元件中誰才是最佳人選; - 第 N - 1 步 :當前
UIView
繼續詢問他的子檢視是不是最佳人選; - 第 N 步 :當前
UIView
不是被點選的的檢視,orz,上一個UIView
就是最佳人選了。
從使用者視角來看,系統通過 hitTest:withEvent:
方法,從檢視的底部一直向表面尋找最佳人選。因為是一直查詢,只有在所有的查詢都完成了,判斷出當前檢視沒有子檢視或者他的子檢視都不適合了,那麼當前檢視就是最佳人選了。(所以你只是點了一個你一眼就看中的檢視,其實系統是從底部開始,一頓連續操作才找到你想要的東西[汗顏])
hitTest:withEvent:
上面的事件分發過程中,大量使用了 hitTest:withEvent:
這個方法,它的處理流程如下:
- 首先呼叫當前檢視的
pointInside:withEvent:
方法, 判斷觸控點是否在當前檢視內 ;- 若返回
NO
,則hitTest:withEvent:
返回nil
; - 若返回
YES
,則向當前檢視的所有子檢視傳送hitTest:withEvent:
訊息,所有子檢視的遍歷順序是從最頂層檢視一直到到最底層檢視,即從subviews
陣列的末尾向前遍歷。
- 若返回
- 若有子檢視返回非空物件,則
hitTest:withEvent:
方法返回此物件,處理結束; - 若所有子檢視都返回
nil
,則hitTest:withEvent:
方法返回自身,即self
,處理結束。
下面我們用一個圖解來理解一下這個 hitTest:withEvent:
:

假如使用者點選了 View D
,結合上圖詳細介紹一下 hitTest:withEvent:
過程: ( hitTest:withEvent:
簡稱 hitTest
, pointInside:withEvent:
簡稱 pointInside
, View X
簡稱 X
)
- A 是 UIWindow 的 根檢視 ,因此,UIWindow 物件會首先對 A 進行
hitTest
; - 顯然使用者點選的範圍是在 A 的範圍內,這時會繼續檢查 A 的子檢視;
- 這時候會有 B 和 C 兩個分支,由於 C 是後新增的子檢視,因此先對 C 進行
hitTest
。- 顯然點選的範圍在 C 內;
- 這時候有 D 和 E 兩個分支,按順序先檢查 E
hitTest:withEvent: hitTest
- 因此,D 的
hitTest
會將 D 返回,再往回回溯,就是 C 的hitTest
返回 D,A 的hitTest
返回 D。
至此,本次點選事件的 第一響應者 就通過響應者鏈的事件分發邏輯成功找到了
除了使用 pointInside:withEvent:
判斷是否是響應者,還有下面三種情況會使 hitTest:withEvent:
返回 nil:
hidden=YES userInteractionEnabled=YES alpha<0.01
因此 hitTest:withEvent:
的實現可能是:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) { return nil; } if ([self pointInside:point withEvent:event]) { for (UIView *subview in [self.subviews reverseObjectEnumerator]) { CGPoint convertedPoint = [subview convertPoint:point fromView:self]; UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event]; if (hitTestView) { return hitTestView; } } return self; } return nil; } 複製程式碼
事件響應
前面說了一大堆事件的分發,其實就是為了找到響應事件的最佳人選,這個最佳人選就是在介紹 響應者鏈條 的時候,最底下的那個 View 。從這個 View 開始我們沿著 響應者鏈條 的方向進行響應。
開篇我們的說的是使用者點選螢幕的場景,因此,響應者會按照當前 UITouch
的所處階段使用下面的方法進行響應:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event; 複製程式碼
- 在響應方法內部,我們也可以呼叫呼叫
[super touches...]
將這個觸控事件繼續分發給父控制元件的對應方法處理。然後父控制元件還可以將該事件繼續向上傳遞,直到傳遞給UIApplication物件。這一系列的響應者物件就 構成了一個響應者鏈條 。 - 如果不呼叫
[super toucher...]
事件 不會繼續沿著響應者鏈條進行響應
小結
事件的 分發 和 響應 都是在 響應者鏈條 上進行的,只不過是兩者 傳遞的方向 不同。

UIViewController
,這裡說明一下他的位置:
- 在 事件分發 過程中沒有
ViewController
的事 - 在 事件響應 的過程中,傳遞的方向如下:

至此,我們已經大概瞭解了當使用者用手指點選了一下螢幕,會發生什麼。
通過對這些的瞭解,我們可以通過使用下面兩種方式來實現一些特殊需求:
hitTest:withEvent: touches