iOS 事件傳遞和處理

前言

iPhone擁有很好的用戶交互體驗,這源於iOS系統對交互事件的高效處理和高優響應;
App開發者處理用戶交互很是便捷,這源於iOS系統和UIKit對用戶操做作了封裝和默認處理;
本文圍繞iOS的事件傳遞和處理,探究其具體過程。php

正文

什麼是事件?

這裏講的事件是用戶交互的抽象,像IOHIDEvent和UIEvent都是不一樣處理階段的封裝。html

IOHIDEvent是iOS系統對事件的封裝,感興趣能夠看源碼IOHIDEvent.hIOHIDEvent.cpp(HID是Human Interface Device的縮寫)。node

UIEvent是UIKit封裝的描述用戶操做類型的對象,可能有touch事件、motion事件、remote-control事件、press事件等。不一樣事件在響應鏈中處理方式不一樣,這裏咱們主要分析touch事件的傳遞和處理。git

用戶點擊手機屏幕的過程

App外:用戶點擊->硬件響應->參數量化->數據轉發->App接收。github

在用戶觸摸屏幕以後,屏幕硬件會接受用戶的操做,並採集關鍵的參數傳遞給IOKit,而IOKit將這些數據打包並傳給SpringBoard.app,繼而轉發給前臺App。數組

App內:子線程接收事件->主線程封裝事件->UIWindow啓動hitTest肯定目標視圖->UIApplication開始發送事件->touch事件開始回調。markdown

App啓動時便會啓動一個com.apple.uikit.eventfetch-thread子線程,負責接收SpringBoard.app轉發過來的數據(經過runloop監聽source1,查看堆棧中有__CFRunLoopDoSource1),數據會被封裝成IOHIDEvent對象,而後轉發給主線程;app

主線程一樣在啓動時監聽source0,接收eventfetch-thread線程發送的IOHIDEvent數據,再封裝成UIEvent,根據UIEvent的類型判斷是否須要啓動hitTest。motion事件不須要hitTest,touch事件也有部分不須要hitTest,好比說touch結束觸發的事件。iphone

肯定目標視圖以後,UIApplication便會發送事件,將UITouch和UIEvent發送給目標視圖,觸發其touches系列的方法。ide

UIKit尋找目標視圖的過程

尋找的過程主要依賴兩個UIView的方法:-hitTest:withEvent方法和-pointInsdie:withEvent方法。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
複製代碼

hitTest方法返回point和event對應的視圖;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
複製代碼

pointInside方法返回point和event是否在本身當前視圖上;

這兩個方法UIView都提供了默認實現,hitTest方法默認會調用全部子視圖的hitTest方法,若是有一個返回。

UIKit會從UIWindow開始尋找目標視圖,先調用UIWindow的hitTest方法詢問是否有響應的視圖,hitTest方法首先會先調用UIWindow的pointInside方法詢問是否在點擊範圍內。

a.若是pointInside方法返回NO,則證實UIWindow沒法響應該事件,hitTest方法會立刻返回nil;
b.若是pointInside方法返回YES,則證實UIWindow能夠響應該事件,hitTest方法會接着調用UIWindow子視圖的hitTest方法。
=> b1.若是子視圖hitTest方法若是有返回視圖,則UIWindow的hitTest方法會返回該視圖;
=> b2.若是全部子視圖hitTest方法都沒有返回視圖,則UIWindow的hitTest方法會返回本身。

UIWindow是UIView的子類,UIView的hitTest方法實現和上述過程一致。

思考: UIView在調用子視圖hitTest時,是先調用哪些子視圖?

從subview數組的末尾開始調用hitTest,subview數組下標越小,視圖層級越低。

UIKit肯定目標視圖後的過程

當UIKit肯定目標視圖以後,就會建立UITouch,UITouch的window屬性和view屬性就是上面過程當中的UIWindow和目標視圖。

接着UIApplication就會調用sendEvent:方法,接着UIWindow在sendEvent:方法中會調用sendTouchesForEvent:方法,以下圖:

UIWindow的sendTouchesForEvent:方法調用的是咱們熟悉的touches四大方法:
-touchesBegan:withEvent:
-touchesMoved:withEvent:
-touchesEnded:withEvent:
-touchesCancelled:withEvent:
從上一步尋找到的目標視圖開始,目標視圖會首先被調用touches方法,接着是目標視圖的父視圖,再是父視圖的父視圖,若是某個視圖是ViewController的.view屬性,還會調用ViewController的方法,直到UIWindow、UIApplication、UIApplicationDelegate(咱們建立的AppDelegate)。

下面是官方文檔給出的回調順序:(Responder chains in an app)

手勢處理髮生在哪一步

手勢(UIGestureRecognizer)是iPhone的重要交互方式,手勢識別 介紹了手勢是如何識別,甚至能夠添加自定義手勢。

UIGestureRecognizer一樣有touches系列方法:

手勢處理的發生時機咱們能夠經過手勢的touchesBegan:withEvent:方法來看,當咱們斷點在手勢的touchesBegan方法時,咱們看到堆棧:

注意到堆棧中的UIApplication的sendEvent:方法,sendEvent是發生在UIKit尋找目標視圖過程以後。從另一種角度來思考,touchesBegan方法中會用到UITouch,而UITouch中的view屬性是目標視圖,因此手勢的處理應該也放在UIKit尋找目標視圖以後。

當手勢的touchesBegan:withEvent:處理完成以後,便會觸發目標視圖的touchesBegan方法。

可是當手勢識別成功以後,默認會cancel後續touch操做,從目標視圖開始的響應鏈都會收到touchesCancelled方法,而不是正常的touchesEnded方法,堆棧以下:

這個行爲也能夠經過設置下面的cancelsTouchesInView=NO來避免觸發touchesCancelled方法。

注意到無論是手勢處理開始的touchesBegan方法,仍是手勢識別成功後觸發touchesCancelled方法,堆棧中都有一個UIGestureEnvironment類。這是一個UIKit的私有類,在網上搜到相關代碼介紹:

@interface UIGestureEnvironment : NSObject {
    NSMutableArray * _delayedPresses;
    NSMutableArray * _delayedPressesToSend;
    NSMutableArray * _delayedTouches;
    NSMutableArray * _delayedTouchesToSend;
    UIGestureGraph * _dependencyGraph;
    NSMutableArray * _dirtyGestureRecognizers;
    bool  _dirtyGestureRecognizersUnsorted;
    struct __CFRunLoopObserver { } * _gestureEnvironmentUpdateObserver;
    NSMutableSet * _gestureRecognizersNeedingRemoval;
    NSMutableSet * _gestureRecognizersNeedingReset;
    NSMutableSet * _gestureRecognizersNeedingUpdate;
    NSMapTable * _nodesByGestureRecognizer;
    bool  _updateExclusivity;
}

- (void)addGestureRecognizer:(id)arg1;
- (void)addRequirementForGestureRecognizer:(id)arg1 requiringGestureRecognizerToFail:(id)arg2;
- (bool)gestureRecognizer:(id)arg1 requiresGestureRecognizerToFail:(id)arg2;
- (id)init;
- (void)removeGestureRecognizer:(id)arg1;
...

複製代碼

從頭文件的方法聲明,咱們能夠大概知道這是一個手勢管理類,手勢的添加、移除、響應都在內部完成。

思考:

一、UIButton的點擊回調是怎麼實現的?
二、若是給UIButton添加Tap手勢,點擊UIButton的時候是觸發UIButton的Tap手勢,仍是觸發UIButton的點擊回調?

總結

因此綜上三步,咱們能夠知道整個流程大概是:

  1. 尋找目標視圖:UIApplication->UIWindow->ViewController->View->targetView
  2. 手勢識別:UIGestureEnvironment-> UIGestureRecognizer
  3. 響應鏈回調:targetView->Viewd->ViewController->UIWindow->UIApplication

iOS的用戶交互相關很是複雜。因爲時間有限,這裏僅僅從事件的傳遞和處理出發,來創建一個基礎的認知。

附錄

參考文獻

手勢識別 developer.apple.com/documentati…

響應鏈介紹 developer.apple.com/documentati…

思考題

一、UIButton的點擊回調是怎麼實現的?

UIButton是UIControl的子類,經過追蹤touch事件的變化獲得一些UIControl定義的事件(UIControlEvents);UIButton的點擊操做是經過UIControlEvents的事件變化回調來觸發,本質依賴的是響應鏈回調過程當中的touches系列方法。

二、若是給UIButton添加Tap手勢,點擊UIButton的時候是觸發UIButton的Tap手勢,仍是觸發UIButton的點擊回調?

上文分析了手勢的識別是發生在響應鏈回調以前,也就是tap手勢是發生在touches系列方法回調以前,那麼Tap手勢應該是在UIButton的touches方法以前。若是UIButton監聽的是經常使用的UIControlEventTouchUpInside事件,則不會回調;若是監聽的是UIControlEventTouchCancel事件,則在觸發完Tap手勢以後,還會收到回調。

相關文章
相關標籤/搜索