(八)解釋栗子

weixin_33924312發表於2019-03-01

本文系轉載,原文地址為iOS觸控事件全家桶

現在,把膠捲回放到本章節開頭的場景。給你一杯咖啡的時間看看能不能解釋得通那幾個現象了,不說了泡咖啡去了...

我肥來了!

先看現象二,短按 cell無法響應,日誌如下:

-[GLTableView touchesBegan:withEvent:]
backview taped
-[GLTableView touchesCancelled:withEvent:]

這個日誌和上面離散型手勢Demo中列印的日誌完全一致。短按後,BackView上的手勢識別器先接收到事件,之後事件傳遞給hit-tested view,作為響應者鏈中一員的GLTableView的 touchesBegan:withEvent: 被呼叫;而後手勢識別器成功識別了點選事件,action執行,同時通知Application取消響應鏈中的事件響應,GLTableView的 touchesCancelled:withEvent: 被呼叫。

因為事件被取消了,因此Cell無法響應點選。

再看現象三,長按cell能夠響應,日誌如下:

-[GLTableView touchesBegan:withEvent:]
-[GLTableView touchesEnded:withEvent:]
cell selected!

長按的過程中,一開始事件同樣被傳遞給手勢識別器和hit-tested view,作為響應鏈中一員的GLTableView的 touchesBegan:withEvent: 被呼叫;此後在長按的過程中,手勢識別器一直在識別手勢,直到一定時間後手勢識別失敗,才將事件的響應權完全交給響應鏈。當觸控結束的時候,GLTableView的 touchesEnded:withEvent: 被呼叫,同時Cell響應了點選。

OK,現在回到現象一。按照之前的分析,快速點選cell,講道理不管是表現還是日誌都應該和現象二一致才對。然而日誌僅僅列印了手勢識別器的action執行結果。分析一下原因:GLTableView的 touchesBegan 沒有呼叫,說明事件沒有傳遞給hit-tested view。那只有一種可能,就是事件被某個手勢識別器攔截了。目前已知的手勢識別器攔截事件的方法,就是設定 delaysTouchesBegan 為YES,在手勢識別器未識別完成的情況下不會將事件傳遞給hit-tested view。然後事實上並沒有進行這樣的設定,那麼問題可能出在別的手勢識別器上。

Window的 sendEvent: 打個斷點檢視event上的touch物件維護的手勢識別器陣列:

1510019-786581b9d7281d92.png
image

捕獲可疑物件:UIScrollViewDelayedTouchesBeganGestureRecognizer ,光看名字就覺得這貨脫不了干係。從類名上猜測,這個手勢識別器大概會延遲事件向響應鏈的傳遞。github上找到了該私有類的標頭檔案

@interface UIScrollViewDelayedTouchesBeganGestureRecognizer : UIGestureRecognizer {
    UIView<UIScrollViewDelayedTouchesBeganGestureRecognizerClient> * _client;
    struct CGPoint { 
        float x; 
        float y; 
    }  _startSceneReferenceLocation;
    UIDelayedAction * _touchDelay;
}
- (void).cxx_destruct;
- (id)_clientView;
- (void)_resetGestureRecognizer;
- (void)clearTimer;
- (void)dealloc;
- (void)sendDelayedTouches;
- (void)sendTouchesShouldBeginForDelayedTouches:(id)arg1;
- (void)sendTouchesShouldBeginForTouches:(id)arg1 withEvent:(id)arg2;
- (void)touchesBegan:(id)arg1 withEvent:(id)arg2;
- (void)touchesCancelled:(id)arg1 withEvent:(id)arg2;
- (void)touchesEnded:(id)arg1 withEvent:(id)arg2;
- (void)touchesMoved:(id)arg1 withEvent:(id)arg2;
@end

有一個_touchDelay變數,大概是用來控制延遲事件傳送的。另外,方法列表裡有個 sendTouchesShouldBeginForDelayedTouches: 方法,聽名字似乎是在一段時間延遲後向響應鏈傳遞事件用的。為一探究竟,我建立了一個類hook了這個方法:

//TouchEventHook.m
+ (void)load{
    Class aClass = objc_getClass("UIScrollViewDelayedTouchesBeganGestureRecognizer");
    SEL sel = @selector(hook_sendTouchesShouldBeginForDelayedTouches:);
    Method method = class_getClassMethod([self class], sel);
    class_addMethod(aClass, sel, class_getMethodImplementation([self class], sel), method_getTypeEncoding(method));
    exchangeMethod(aClass, @selector(sendTouchesShouldBeginForDelayedTouches:), sel);
}

- (void)hook_sendTouchesShouldBeginForDelayedTouches:(id)arg1{
    [self hook_sendTouchesShouldBeginForDelayedTouches:arg1];
}

void exchangeMethod(Class aClass, SEL oldSEL, SEL newSEL) {
    Method oldMethod = class_getInstanceMethod(aClass, oldSEL);
    Method newMethod = class_getInstanceMethod(aClass, newSEL);
    method_exchangeImplementations(oldMethod, newMethod);
}

斷點看一下點選cell後 hook_sendTouchesShouldBeginForDelayedTouches: 呼叫時的資訊:

1510019-bb69397f67b4eb44.png
image

可以看到這個手勢識別器的 _touchDelay 變數中,儲存了一個計時器,以及一個長得很像延遲時間間隔的變數m_delay。現在,可以推測該手勢識別器截斷了事件並延遲0.15s才傳送給hit-tested view。為驗證猜測,我分別在Window的 sendEvent:hook_sendTouchesShouldBeginForDelayedTouches: 以及TableView的 touchesBegan: 中列印時間戳,若猜測成立,則應當前兩者的呼叫時間相差0.15s左右,後兩者的呼叫時間很接近。短按Cell後列印結果如下(不能快速點選,否則還沒過延遲時間觸控就結束了,無法驗證猜測):

-[GLWindow sendEvent:]呼叫時間戳 :
525252194779.07ms
-[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]呼叫時間戳 :
525252194930.91ms
-[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]呼叫時間戳 :
525252194931.24ms
-[GLTableView touchesBegan:withEvent:]呼叫時間戳 :
525252194931.76ms

因為有兩個 UIScrollViewDelayedTouchesBeganGestureRecognizer,所以 hook_sendTouchesShouldBeginForDelayedTouches 調了兩次,兩次的時間很接近。可以看到,結果完全符合猜測。

這樣就都解釋得通了。現象一由於點選後,UIScrollViewDelayedTouchesBeganGestureRecognizer 攔截了事件並延遲了0.15s傳送。又因為點選時間比0.15s短,在傳送事件前觸控就結束了,因此事件沒有傳遞到hit-tested view,導致TableView的 touchBegin 沒有呼叫。而現象二,由於短按的時間超過了0.15s,手勢識別器攔截了事件並經過0.15s後,觸控還未結束,於是將事件傳遞給了hit-tested view,使得TableView接收到了事件。因此現象二的日誌雖然和離散型手勢Demo中的日誌一致,但實際上前者的hit-tested view是在觸控後延遲了約0.15s左右才接收到觸控事件的。

至於現象四 ,你現在應該已經覺得理所當然了才對。

相關文章