iOS觸摸事件的流動
當指肚輕觸屏幕,整個系統像沉睡的生靈突然被驚醒,然后經歷過腥風血雨的一段奇幻旅行,最終又歸于沉寂。
整個iOS觸摸事件從產生到寂滅大致如下圖:
起始階段
—-> cpu處于睡眠狀態,等待事件發生
—-> 手指觸摸屏幕
系統響應階段
—-> 屏幕硬件感應到輸入,并將感應到的事件傳遞給輸入輸出驅動IOKit
—-> IOKit.framework封裝整個觸摸事件為IOHIDEvent對象
—-> IOKit.framework通過IPC將事件轉發給SpringBoard.app
以上是系統層的響應。系統感應到外界的輸入,并將相應的輸入封裝成比較概括的 IOHIDEvent 對象,然后UIKit通過IOHIDEvent的類型,判斷出相應事件應該由 SpringBoard .app 處理,直接通過mach port(IPC進程間通信)轉發給SpringBoard.app。
SpringBoard.app就是iOS的系統桌面,當觸摸事件發生時,也只有負責管理桌面的SpringBoard.app才知道如何正確的響應。因為觸摸發生時,有可能用戶正在桌面翻頁找App,也有可能正處于在微信中刷朋友圈。
桌面響應階段
—-> SpringBoard.app主線程Runloop收到 IOKit.framework 轉發來的消息蘇醒,并觸發對應Mach Port的Source1回調 __IOHIDEventSystemClientQueueCallback() 。
—-> 如果SpringBoard.app監測到有App在前臺(記為xxxx.app),SpringBoard.app通過mach port(IPC進程間通信)轉發給xxxx.app,如果SpringBoard.app監測到監測無前臺App,則SpringBoard.app進入App內部響應階段的第二段,記觸發Source0回調。
App內部響應階段
—-> 前臺App主線程Runloop收到SpringBoard.app轉發來的消息蘇醒,并觸發對應Mach Port的Source1回調 __IOHIDEventSystemClientQueueCallback() 。
—-> Source1回調內部觸發Source0回調 __UIApplicationHandleEventQueue()
—-> Soucre0回調內部,封裝 IOHIDEvent 為 UIEvent
—-> Soucre0回調內部調用 UIApplication 的 sendEvent: 方法,將 UIEvent 傳給 UIWindow
—-> 平時開發熟悉的觸摸事件響應鏈從這開始了
—-> 通過遞歸調用UIView層級的 hitTest(_:with:) ,結合 point(inside:with:) 找到 UIEvent 中每一個 UITouch 所屬的 UIView (其實是想找到離觸摸事件點最近的那個 UIView )。這個過程是從 UIView 層級的最頂層往最底層遞歸查詢,但這不是 UIResponder 響應鏈,事件響應是在 UIEvent 中每一個 UITouch 所屬的 UIView 都確定之后方才開始。
但需要注意,以下三種情況 UIView 的 hitTest(_:with:) 不會被調用,也導致其子 UIView 的 hitTest(_:with:) 不會被調用,而之后響應事件是下向上傳遞的,這直接導致以下三種情況的 UIView 及其子 UIView 不接收任何觸摸事件:
- userInteractionEnabled = NO
- hidden = YES
- alpha = 0.0~0.01之間
提示: UIImageView的userInteractionEnabled默認為NO,因此UIImageView以及它的子控件默認是不接收觸摸事件的。
當把斷點打在某個UIView hitTest(_:with:) 中時,對應的調用堆棧如下:
—-> 根據圍繞 UITouch 所屬的 UIView 及其祖先 UIView 的gesture recognizers,來確定一個UITouch的gestureRecognizers
—-> UITouch所屬的UIView和gestureRecognizers收到此UITouch和相應的UIEvent,并按照UITouch所處的狀態調用四大UITouch方法 touchesBegan(_:with:) touchesMoved(_:with:) touchesEnded(_:with:) touchesCancelled(_:with:) 中的一個。(事件響應開始)
—-> 對于UIView收到的UITouches事件(四大UITouch事件都是如此),則會按照UIResponder響應鏈一直往上傳遞,直到某個UIResponder因為主動響應觸摸事件,切斷了響應鏈(即不調用下一個UIResponder的響應方法),如果一直沒有UIResponder做響應處理,則這些UITouches到達最后的響應者即UIApplication后,就被吃掉了,消失了。
—-> 如果在事件響應過程中,有UIGestureRecognizer成功識別,則此UIGestureRecognizer將獨自占有所需要的UITouches,這些UITouches所屬的UIView及其他的UIGestureRecognizer的 touchesCancelled(_:with:) 方法將調用(如果在手勢的代理中設置可以同時識別兩個手勢,則允許同時識別的手勢均可以收到所需要的UITouches事件)。但與識別成功的UIGestureRecognizer無關的UITouches則會繼續按照上述傳遞邏輯傳遞。也即允許兩個手勢同時識別,只要所占有的UITouches不相同。
—-> 如果UIGestureRecognizer識別成功,則調用相應的action,處理對應的邏輯。如果某個UIResponder主動響應了觸摸事件,則根據其本身的響應邏輯處理對應的業務,UIControl都是主動響應并切斷UITouch的向上傳遞的。
—-> UITouches事件流動完畢,整個系統重新進入睡眠等待下一個事件
來自:http://shellhue.github.io/2017/03/04/FlowOfUITouch/