1. 程式人生 > >iOS底層原理之事件的傳遞與響應

iOS底層原理之事件的傳遞與響應

iOS中的事件

iOS的事件分為3大型別:觸控事件、加速計事件、遠端控制事件;而我們最常用到的是觸控事件。

UIResponder(響應者物件)

在iOS中不是任何物件都能處理事件,只有繼承了UIResponder的物件才能接受並處理事件,我們稱之為“響應者物件”。UIApplicationUIViewControllerUIView都繼承UIResponder

UIResponder常用API

事件的處理API

//UIResponder內部提供了以下方法來處理事件觸控事件
// 一根或者多根手指開始觸控view,系統會自動呼叫view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
// 一根或者多根手指在view上移動,系統會自動呼叫view的下面方法(隨著手指的移動,會持續呼叫該方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
// 一根或者多根手指離開view,系統會自動呼叫view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
// 觸控結束前,某個系統事件(例如電話呼入)會打斷觸控過程,系統會自動呼叫view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

//加速計事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

//遠端控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

以上方法是由系統自動呼叫的,所以可以通過重寫該方法來處理一些事件。

UITouch(觸控事件物件)

  • 當用戶用一根手指觸控式螢幕幕時,會建立一個與手指相關的UITouch物件,一根手指對應一個UITouch物件;
  • 如果兩根手指同時觸控一個檢視,那麼檢視只會呼叫一次的touchesBegan:withEvent:方法方法,觸控引數中裝著2個UITouch物件;
  • 如果這兩根手指一前一後分開觸控同一個檢視,那麼檢視會分別呼叫2次的touchesBegan:withEvent:方法方法,並且每次呼叫時的觸控引數中只包含一個UITouch物件。

UITouch的作用

  • 儲存著跟手指相關的資訊,比如觸控的位置,時間,階段;
  • 當手指移動時,系統會更新同一個UITouch物件,使之能夠一直儲存該手指在的觸控位置;
  • 當手指離開螢幕時,系統會銷燬相應的UITouch物件。

UITouch常見API

//常見屬性
//觸控產生時所處的視窗
@property(nonatomic,readonly,retain) UIWindow *window;
//觸控產生時所處的檢視
@property(nonatomic,readonly,retain) UIView *view;
//短時間內點按螢幕的次數,可以根據tapCount判斷單擊、雙擊或更多的點選
@property(nonatomic,readonly) NSUInteger tapCount;
//記錄了觸控事件產生或變化時的時間,單位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
//當前觸控事件所處的狀態
@property(nonatomic,readonly) UITouchPhase phase;

//常見方法
// 返回值表示觸控在view上的位置
// 這裡返回的位置是針對view的座標系的(以view的左上角為原點(0, 0))
// 呼叫時傳入的view引數為nil的話,返回的是觸控點在UIWindow的位置
- (CGPoint)locationInView:(UIView *)view;
// 該方法記錄了前一個觸控點的位置
- (CGPoint)previousLocationInView:(UIView *)view;

注意 : UITouch物件是當觸控時系統自動建立的,自己alloc建立是沒有意義的。

iOS事件的產生和傳遞

事件的產生

  • 發生觸控事件後,系統會將該事件加入到一個由UIApplication的管理的事件佇列中(FIFO,先進先出),先產生的事件先處理。
  • 的UIApplication會從事件佇列中取出最前面的事件,並將事件分發下去以便處理,通常,先發送事件給應用程式的主視窗keyWindow
  • 主視窗會在檢視層次結構中找到一個最合適的檢視來處理觸控事件,這也是整個事件處理過程的第一步。
  • 找到合適的檢視控制元件後,就會呼叫檢視控制元件的觸控方法來作具體的事件處理。

事件的傳遞

那麼如何找到最合適的檢視呢?這就看時間是如何傳遞的!

  • 首先判斷主視窗(keyWindow)自己是否能接受觸控事件;

  • 判斷觸控點是否在自己身上;

  • 如果上面兩部都滿足,就將子控制元件陣列中從後往前遍歷子控制元件,讓子控制元件重複前面的兩個步驟(所謂從後往前遍歷子控制元件,就是首先查詢子控制元件陣列中最後一個元素,然後執行1,2-步驟);

  • 如果一個控制元件自己滿足上面的條件,而它的所有子控制元件都不滿足上面條件,或者其沒有子控制元件,則該控制元件就是響應事件的最合適的檢視。

  • UIView的不能接收觸控事件的三種情況:
    alpha <0.01;
    userInteractionEnabled = NO;
    hidden = YES;

  • 注意:採取從後往前遍歷子控制元件的 方式尋找最合適的檢視只是為了做一些迴圈優化。因為相比較之下,後新增的檢視在上面,降低迴圈次數。

尋找最合適的檢視底層剖析

尋找響應事件的最合適的檢視,需要用到兩個關鍵的方法:

// 此方法返回的View是本次點選事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

// 判斷一個點是否落在範圍內
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
  • 只要事件一傳遞給一個控制元件,這個控制元件就會呼叫他自己的則hitTest:withEvent:方法方法,尋找並返回最合適的檢視(能夠響應事件的那個最合適的檢視);

  • 例項:
    檢視結構如下:GrayViewview的子檢視,RedView、YellowViewGrayView的子檢視,BlueView、GreenViewRedView的子檢視,PurpleView,CyanViewYellowView的子檢視;並且新增順序是從上到下,從左到右。
    檢視結構
    在每個自定義的子檢視中重新- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)even方法;

// 此方法返回的View是本次點選事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"事件傳遞到%@",NSStringFromClass([self class]));
    // 1.判斷當前控制元件能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    // 2. 判斷點在不在當前控制元件
    if ([self pointInside:point withEvent:event] == NO) return nil;
    // 3.從後往前遍歷自己的子控制元件,因為後新增進來的檢視一般在最上面,所以從後往前取出子檢視,使得遍歷效率提高
    NSInteger count = self.subviews.count;
    for (NSInteger i = count - 1; i >= 0; i--) {
        UIView * childView = self.subviews[i];
        // 把當前控制元件上的座標系轉換成子控制元件上的座標系
        CGPoint childP = [self convertPoint:point toView:childView];
        //子控制元件在重複呼叫自己的hitTest方法
        UIView *fitView = [childView hitTest:childP withEvent:event];
        // 如果子檢視是最合適的就返回
        if (fitView) {
            return fitView;
        }
    }
    // 迴圈結束,說明只有自己是最合適的view
    return self;
}




//開始點選事呼叫,及響應事件處理
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%@是最佳的事件響應者",NSStringFromClass([self class]));
}

當點選GrayView時列印結果:
點選了GrayView
當點選BlueView時列印結果:
點選了BlueView

  • return nil的含義:
    hitTest:withEvent:中return nil的意思是呼叫當前hitTest:withEvent:方法的檢視不是合適的檢視,子控制元件也不是合適的檢視。如果同級的兄弟控制元件也沒有合適的檢視,那麼最合適的檢視就是父控制元件。

  • 截事件的處理:
    正因hitTest:withEvent:方法方法可以返回最合適的檢視,所以可以通過重寫hitTest:withEvent:方法方法,返回指定的檢視作為最合適的圖去響應事件。

  • 想讓誰成為最合適的檢視就重寫誰自己的父控制元件的hitTest:withEvent:方法返回指定的子控制元件,或者重寫自己的hitTest:withEvent:方法return self。但是,建議在父控制元件的則hitTest:withEvent:方法中返回子控制元件作為最合適的觀點!

  • 列如不管點選哪個檢視都讓YellowView成為處理事件最合適的view,只需要將其俯檢視(GrayView)的hitTest:withEvent:方法重寫,返回YellowView

// 重寫GrayView的hitTest:withEvent:方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"事件傳遞到%@",NSStringFromClass([self class]));
    // 1.判斷當前控制元件能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    for (UIView * childView in self.subviews) {
        if ([childView isMemberOfClass:NSClassFromString(@"YellowView")]) {
            return childView;
        }
    }
    return self;
}

或者重寫YellowViewhitTest:withEvent:返回self

//重寫YellowView的該方法
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    return self;
}

注意: 因為YellowView後新增到GrayView,所以會優先將事件傳遞給它,這樣可以實現點選任意檢視YellowView就是最合適的檢視,但是如果想讓RedView成為最合適的檢視,只重寫RedViewhitTest: withEvent:方法並返回self,當點選YellowView或者YellowView的子檢視PurpleViewCyanView時返回的最合適檢視並不是RedView,所以要攔截某個檢視為最合適的檢視最好重寫其俯檢視的hitTest: withEvent:方法。

  • 綜上事件的傳遞順序是這樣的:
      觸控事件 - > UIApplication事件佇列 - > [UIWindow hitTest:withEvent:] - >返回更合適的檢視 - > [子控制元件hitTest:withEvent:] - >返回最合適的檢視

事件的響應

當事件產生並傳遞找到最合適的控制元件,就會調該用控制元件的觸控方法來作具體的事件處理(也就是響應該事件),如果該控制元件沒有響應觸控事件(有沒有重寫touchesBegantouchesMovedtouchesEndedtouchesCancelled這些響應觸控事件的方法),這些觸控方法的預設做法是將事件順著響應者鏈條向上傳遞(也就是觸控方法預設不處理事件,只傳遞事件)。

響應者鏈

  • 響應者鏈條示意圖
    響應者鏈
    響應者鏈是由多個響應者物件連線起來的鏈條。
  • 響應者鏈的事件響應傳遞過程:
  1. 如果當前檢視是控制器的檢視,那麼控制器就是下一個響應者,如果當前檢視不是控制器的檢視,那麼父檢視就是當前檢視的下一個響應者;
  2. 如果當前響應者不處理事件,就將事件傳遞給下一個響應者,以此類推;
  3. 在檢視層次結構的底頂級檢視,如果也不能處理收到的事件或訊息,則其將事件或訊息傳遞給視窗物件進行處理;
  4. 如果視窗物件也不處理,則其將事件或訊息傳遞給UIApplication的物件;
  5. 如果UIApplication的也不能處理該事件或訊息,則將其丟棄。
  • 事件響應的底層原理:
    只要點選控制元件,就會呼叫touchBegin等那4個觸控事件響應方法,如果沒有重寫這個方法,自己處理不了觸控事件,系統預設就會用supertouchesBegan方法,直到有響應者重寫過改法後,不再傳遞該觸控事件了。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
// 預設會把事件傳遞給上一個響應者,上一個響應者是父控制元件,交給父控制元件處理
[super touchesBegan:touches withEvent:event]; 
}

nextResponder

UIResponder具有nextResponder屬性,也就是其SuperView或是UIViewConterller等,這個屬性有時候還是很有用的,比如UIViewConterllerView的新增一個UITableView,點選自定cell跳轉別的控制器,這時候就可以通過cell.superView.superView.nextResponder就可以獲取控制器,然後進行跳轉。

總結

  • 事件的傳遞:即尋找最合適的檢視的過程,當一個事件發生後,事件會從父控制元件傳給子控制元件,也就是說由UIApplication - > UIWindow - > UIView - >初始檢視;
  • 事件的響應:即處理事件,當找到最合適的檢視後,先看該檢視能不能處理該事件(即有沒有有重寫幾個觸控方法),重寫了就不再往下傳遞,沒重寫就順著事件響應者鏈從子控制元件傳給父控制元件去查詢沒有響應者去處理事件,如果都沒有處理該事件,則該事件不被任何響應者響應,就拋棄該事件。
  • 事件產生傳遞處理的整體過程:
    當用戶點選一個UIView時,系統會產生一個事件,並將其放入UIApplication的事件佇列中。然後該事件會順著這條鏈傳遞到使用者點選的那個UIView:UIApplication->UIWindow->RootView->...->Subview。然後開始處理這個事件,若Subview不處理,事件將會傳遞給檢視控制器,若沒有控制器則傳給其superView,最後傳給UIWindow,UIApplication。若UIApplication還是沒處理則將事件傳給nil。