古詩詞大全網 - 四字成語 - 事件傳遞和響應機制

事件傳遞和響應機制

響應者對象

在iOS中不是任何對象都能處理事件,只有繼承了UIResponder的對象才能接受並處理事件,我們稱之為“響應者對象”。例如常見的 :UIApplication ? UIViewController ? UIView

UIResponder 可以處理觸摸事件、按壓事件(3D touch)、遠程控制事件、硬件運動事件。

事件的傳遞

1. 發生觸摸事件後,系統會將該事件加入到壹個由UIApplication 管理的事件隊列中。因為隊列的特點是FIFO,即先進先出,先產生的事件先處理(首先接收到事件的是UIApplication)。

2. UIApplication 會從事件隊列中取出最前面的事件,並將事件分發下去以便處理,通常先發送事件給應用程序的主窗口(keyWindow)。

3. 主窗口會在視圖層次結構中找到壹個最合適的視圖來處理觸摸事件。找到合適的視圖控件後,就會調用視圖控件的touches方法來作具體的事件處理。

觸摸事件的傳遞是從父控件傳遞到子控件: ?UIApplication->window->尋找處理事件最合適的view

UIView不能接收觸摸事件的 4 種情況:

1. 不允許交互 :userInteractionEnabled = NO,當前視圖不可交互,該視圖上面的子視圖也不可與用戶交互。用戶觸發的事件都會被該視圖忽略(其他視圖照常響應),並且該視圖對象也會從事件響應隊列中被移除。

2. 隱藏 :如果把父控件隱藏,那麽子控件也會隱藏,隱藏的控件不能接收事件

3. 透明度 :如果設置壹個控件的透明度<0.01,會直接影響子控件的透明度。alpha:0.0~0.01為透明。

4. 子視圖的部分區域超過父視圖,也不會接收觸摸事件,因為父視圖在調用?pointInside方法時會返回NO。說明觸摸點不在自己範圍內,則當前 view 的hitTest: withEvent:方法返回 nil,當前 view上 的所有 subview 都不做判斷。

註意:如果 Touch 位置超過視圖邊界,hitTest:withEvent 方法將忽略這個視圖和它的所有子視圖。結果就是,當視圖的ciipsToBounds屬性為NO,子視圖超過視圖邊界也不會接收到事件 ,即使 觸摸點在它上面。

不管視圖能不能處理事件,只要點擊了視圖就都會產生事件,關鍵在於該事件最終是由誰來處理。 系統通過 hitTest:(CGPoint)point withEvent:(UIEvent*)even 找到最適合處理該事件的view。?

應用如何找到最合適的控件來處理事件

1. 首先判斷主窗口(keyWindow)自己是否能接受觸摸事件 hitTest 方法。

2. 判斷觸摸點是否在自己身上,通過pointInside 方法來判斷。

3. 如果上面 2 步都滿足條件,會把這個事件交給 view處理,會對 view的 subviews子控件數據 進行遍歷,直至沒有更合適的view為止。

註意:采取從數組最後面往前遍歷子控件的方式,因為後添加的view在最上面,最上層的響應者能最先接受響應,阻斷事件繼續傳遞,從而降低遍歷循環次數。

5. 如果沒有符合條件的子控件,那麽就認為自己最合適處理這個事件,return self 。

尋找最合適的view底層剖析 ?兩個重要的方法:

piontinside方法使用場景 : IOS 增加按鈕點擊區域 - 使按鈕的點擊反應區域變大

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event?什麽時候調用?

?事件傳遞給誰,就會調用誰的hitTest:withEvent:方法。

作用

尋找並返回能夠響應事件, ?最合適的view,不管點擊哪裏,最合適的view都是 hitTest 方法中返回的那個view。

註 意:不管這個控件能不能處理事件,也不管觸摸點在不在這個控件上,事件都會先傳遞給這個控件,通過調用 hitTest 方法來判斷是否可以處理事件。

攔截事件的處理

通過重寫 hitTest ?方法,返回指定的view 。就可以攔截事件的傳遞過程,想讓誰處理事件誰就處理事件。

註 意:如果 hitTest ?方法中返回 nil,那麽調用該方法的控件本身和其子控件都不是最合適的view,也就是在自己身上沒有找到更合適的view。如果同壹層級的其他控件也沒有合適的view,那麽最合適的 view 就是父控件。

不管子控件是不是最合適的view,系統默認都要先把事件傳遞給子控件,子控件調用自己的hitTest:withEvent: 方法後才知道有沒有更合適的view。即便父控件是最合適的view了,子控件的hitTest:withEvent:方法還是會調用。?

技巧: 想讓誰成為最合適的view 就重寫誰父控件的hitTest:withEvent:方法返回指定的子控件,或者重寫自己的hitTest:withEvent:方法 return self。但是, 建議在父控件的hitTest:withEvent:中返回子控件作為最合適的view?

原因呢:當遍歷子控件時,如果觸摸點不在子控件A自己身上而是在子控件B身上,想要返回子控件A作為最合適的view,采用返回自己的方法可能會導致還沒有來得及遍歷A自己,就有可能已經遍歷了點真正所在的view B。這就導致了返回的不是自己而是觸摸點真正所在的view。所以建議在父控件的hitTest:withEvent:中返回子控件作為最合適的view。

找到最合適的view 後,就會調用該view的 touches 方法處理具體的事件。

觸摸事件由觸屏生成後如何傳遞到當前應用?

系統響應階段

用戶觸摸屏幕,系統硬件進程會獲取到這個點擊事件,將事件簡單處理封裝後存到系統中,由於硬件檢測進程和當前App進程是兩個進程,所以進程兩者之間傳遞事件用的是端口通信。

1. 指觸碰屏幕,屏幕感應到觸碰後,將事件交由IOKit處理。

2. IOKit 將觸摸事件封裝成壹個IOHIDEvent 對象,並通過mach port傳遞給SpringBoad進程。mach port 進程端口,各進程之間通過它進行通信。

3. SpringBoad 是壹個系統進程,統壹管理和分發系統接收到的觸摸事件。將觸摸事件交給前臺app進程來處理。

參考: RunLoop原理學習 -

APP響應階段

1. APP進程的mach port 接收到 SpringBoard 進程傳遞來的觸摸事件,主線程的 runloop被喚醒,觸發了source1回調。

2. source1回調又觸發了壹個source0回調,將接收到的 IOHIDEvent 對象封裝成 UIEvent 對象。

3. source0 回調內部將觸摸事件添加到 UIApplication 對象的事件隊列中。事件出隊後,UIApplication開始尋找最佳響應者,這個過程又稱hit-testing。?

3. 系統判斷本次觸摸是否導致了壹個新的事件。如果是,系統會先從響應網中尋找響應鏈。如果不是,說明該事件是當前正在進行中的事件產生的壹個Touch message, 也就是說已經有保存好的響應鏈

4. 尋找到最佳響應者後,事件就在響應鏈中的傳遞及響應了。

響應者鏈條:由多個響應者對象連接起來的鏈條

在iOS程序中無論是最後面的UIWindow還是最前面的某個按鈕,它們的擺放是有前後關系的,壹個控件可以放到另壹個控件上面或下面,那麽用戶點擊某個控件時是觸發上面的控件還是下面的控件呢,這種先後關系構成壹個鏈條就叫“響應者鏈”。

事件在 響應者 鏈上傳遞,最終結果是事件被處理或被拋棄。響應者鏈條能很清楚的看見每個響應者之間的聯系,並且可以讓壹個事件多個對象處理。

每壹個響應者對象(UIResponder對象)都有壹個nextResponder方法,用於獲取響應鏈中當前對象的下壹個響應者。因此,壹旦事件的第壹響應者確定了,這個事件所處的響應鏈就確定了。

響應者對象默認的 nextResponder 如下:

1. UIView 的 nextResponder 屬性,如果有管理此 view 的 UIViewController 對象,則為此 2. UIViewController 對象;否則 nextResponder 即為其 superview。

3. UIViewController 的 nextResponder 屬性為其管理 view 的 superview.

若 VC 是window的根視圖rootVC,則其 nextResponder 為 UIWindow ;

若 VC 是從別的控制器present出來的,則其nextResponder為presenting view controller。

4. UIWindow 的 nextResponder 屬性為 UIApplication 對象。

5. UIApplication 的 nextResponder 屬性為 nil。

若當前應用的app delegate是壹個UIResponder對象,且不是UIView、UIViewController或app本身,則UIApplication的nextResponder為app delegate。?

響應者鏈的事件傳遞過程:

1. ?如果當前view是控制器的view,那麽控制器就是上壹個響應者,事件就傳遞給控制器;如果當前view不是控制器的view,那麽父視圖就是當前view的上壹個響應者,事件就傳遞給它的父視圖

2. 在視圖層次結構的最頂級視圖,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對象進行處理

3. 如果window對象也不處理,則其將事件或消息傳遞給UIApplication對象

4. 如果UIApplication也不能處理該事件或消息,則將其丟棄

響應者對於接收到的事件有3種操作:

1. 不攔截,默認操作. ?事件會自動沿著默認的響應鏈向上傳遞,(touch方法默認不處理事件,只傳遞事件),將事件交給上壹個響應者進行處理.

UIResponder中的默認實現是什麽都不做,但UIKit中UIResponder的直接子類(UIView,UIViewController…) 的默認實現是將事件沿著responder chain繼續向上傳遞到下壹個responder, ?即nextResponder。

2. 攔截,不再往下分發事件, 重寫 touchesBegan:withEvent:進行事件處理,不調用父類的 touchesBegan:withEvent, 事件到這裏就結束傳遞進行處理。

3. 攔截,繼續往下分發事件, 重寫自己的 touchesBegan:withEvent: 進行事件處理,同時調用 [super ?touchesBegan:withEvent:] 將事件往下傳遞,達到 壹個事件多個對象處理 的目的。?

建議使用:[super touchesBegan:touches withEvent:event];?

super 的touches對應方法中默認將事件繼續向上傳遞給?next responder。?

不建議直接向nextResponder發送消息,這樣可能會漏掉父類對這壹事件的其他處理。

[self.nextResponder? touchesBegan:touches withEvent:event];

手勢事件會打斷響應鏈的傳遞。因為手勢比響應鏈擁有更高的優先級,添加了手勢的View 會阻止子View響應鏈,手勢會最先響應,並對事件進行處理。此事件不再響應鏈中向上傳遞。

_UIApplicationHandleEventQueue() 識別了壹個手勢時,首先調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記為待處理。

蘋果註冊了壹個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記為待處理的 GestureRecognizer,並執行GestureRecognizer的回調。當UIGestureRecognizer 變化(創建/銷毀/狀態改變)時,回調都會進行處理。

事件的傳遞和響應的區別:

事件的傳遞是從上到下(父控件到子控件),事件的響應是從下到上(順著響應者鏈條向上傳遞:子控件到父控件。

可根據壹個view 找到它對應的VC控制:?

參考文章:

史上最詳細的iOS之事件的傳遞和響應機制-原理篇 -

iOS 響應者及響應者鏈 -

iOS - 為什麽要在主線程中操作UI -