iOS底層原理之事件的傳遞與響應
iOS中的事件
iOS的事件分為3大型別:觸控事件、加速計事件、遠端控制事件;而我們最常用到的是觸控事件。
UIResponder(響應者物件)
在iOS中不是任何物件都能處理事件,只有繼承了UIResponder
的物件才能接受並處理事件,我們稱之為“響應者物件”。UIApplication
、UIViewController
、UIView
都繼承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
:方法方法,尋找並返回最合適的檢視(能夠響應事件的那個最合適的檢視); -
例項:
檢視結構如下:GrayView
是view
的子檢視,RedView、YellowView
是GrayView
的子檢視,BlueView、GreenView
是RedView
的子檢視,PurpleView,CyanView
是YellowView的
子檢視;並且新增順序是從上到下,從左到右。
在每個自定義的子檢視中重新- (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
時列印結果:
當點選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;
}
或者重寫YellowView
的hitTest:withEvent:
返回self
;
//重寫YellowView的該方法
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self;
}
注意: 因為YellowView
後新增到GrayView
,所以會優先將事件傳遞給它,這樣可以實現點選任意檢視YellowView
就是最合適的檢視,但是如果想讓RedView
成為最合適的檢視,只重寫RedView
的hitTest: withEvent:
方法並返回self
,當點選YellowView
或者YellowView
的子檢視PurpleView
和CyanView
時返回的最合適檢視並不是RedView
,所以要攔截某個檢視為最合適的檢視最好重寫其俯檢視的hitTest: withEvent:
方法。
- 綜上事件的傳遞順序是這樣的:
觸控事件 - > UIApplication事件佇列 - > [UIWindow hitTest:withEvent:] - >返回更合適的檢視 - > [子控制元件hitTest:withEvent:] - >返回最合適的檢視
事件的響應
當事件產生並傳遞找到最合適的控制元件,就會調該用控制元件的觸控方法來作具體的事件處理(也就是響應該事件),如果該控制元件沒有響應觸控事件(有沒有重寫touchesBegan
、touchesMoved
、touchesEnded
、touchesCancelled
這些響應觸控事件的方法),這些觸控方法的預設做法是將事件順著響應者鏈條向上傳遞(也就是觸控方法預設不處理事件,只傳遞事件)。
響應者鏈
- 響應者鏈條示意圖
響應者鏈是由多個響應者物件連線起來的鏈條。 - 響應者鏈的事件響應傳遞過程:
- 如果當前檢視是控制器的檢視,那麼控制器就是下一個響應者,如果當前檢視不是控制器的檢視,那麼父檢視就是當前檢視的下一個響應者;
- 如果當前響應者不處理事件,就將事件傳遞給下一個響應者,以此類推;
- 在檢視層次結構的底頂級檢視,如果也不能處理收到的事件或訊息,則其將事件或訊息傳遞給視窗物件進行處理;
- 如果視窗物件也不處理,則其將事件或訊息傳遞給
UIApplication
的物件; - 如果
UIApplication
的也不能處理該事件或訊息,則將其丟棄。
- 事件響應的底層原理:
只要點選控制元件,就會呼叫touchBegin
等那4個觸控事件響應方法,如果沒有重寫這個方法,自己處理不了觸控事件,系統預設就會用super
的touchesBegan
方法,直到有響應者重寫過改法後,不再傳遞該觸控事件了。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 預設會把事件傳遞給上一個響應者,上一個響應者是父控制元件,交給父控制元件處理
[super touchesBegan:touches withEvent:event];
}
nextResponder
UIResponder
具有nextResponder
屬性,也就是其SuperView
或是UIViewConterller
等,這個屬性有時候還是很有用的,比如UIViewConterller
的View
的新增一個UITableView
,點選自定cell
跳轉別的控制器,這時候就可以通過cell.superView.superView.nextResponder
就可以獲取控制器,然後進行跳轉。
總結
- 事件的傳遞:即尋找最合適的檢視的過程,當一個事件發生後,事件會
從父控制元件傳給子控制元件
,也就是說由UIApplication - > UIWindow - > UIView - >初始檢視; - 事件的響應:即處理事件,當找到最合適的檢視後,先看該檢視能不能處理該事件(即有沒有有重寫幾個觸控方法),重寫了就不再往下傳遞,沒重寫就順著事件響應者鏈
從子控制元件傳給父控制元件
去查詢沒有響應者去處理事件,如果都沒有處理該事件,則該事件不被任何響應者響應,就拋棄該事件。 - 事件產生傳遞處理的整體過程:
當用戶點選一個UIView
時,系統會產生一個事件,並將其放入UIApplication
的事件佇列中。然後該事件會順著這條鏈傳遞到使用者點選的那個UIView:UIApplication->UIWindow->RootView->...->Subview
。然後開始處理這個事件,若Subview
不處理,事件將會傳遞給檢視控制器,若沒有控制器則傳給其superView
,最後傳給UIWindow
,UIApplicatio
n。若UIApplication
還是沒處理則將事件傳給nil。