日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

iOS触摸屏幕后发生了什么

發布時間:2024/1/1 编程问答 50 豆豆
生活随笔 收集整理的這篇文章主要介紹了 iOS触摸屏幕后发生了什么 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

好奇觸摸事件是如何從屏幕轉移到APP內的?

困惑于Cell怎么突然不能點擊了?

糾結于如何實現這個奇葩響應需求?

亦或是已經被響應鏈、手勢、target-action這一系列響應觸摸事件的方式折騰到不會打Hello World?

現在 是時候帶你上分了~ (強行YY完畢)

本文主要講解iOS觸摸事件的一系列機制,涉及的問題大致包括:

  • 觸摸事件由觸屏生成后如何傳遞到當前應用?

  • 應用接收觸摸事件后如何尋找最佳響應者?實現原理?

  • 觸摸事件如何沿著響應鏈流動?

  • 響應鏈、手勢識別器、UIControl之間對于觸摸事件的響應有著什么樣的瓜葛?

tips: iOS中的事件除了觸摸事件,還包括加速計事件、遠程控制事件。由于兩者不在本文討論范疇,因此文中所說事件均特指觸摸事件。

事件的生命周期

當指尖觸碰屏幕的那一刻,一個觸摸事件就在系統中生成了。經過IPC進程間通信,事件最終被傳遞到了合適的應用。在應用內歷經峰回路轉的奇幻之旅后,最終被釋放。大致經過如下圖:

圖片來源(http://qingmo.me/2017/03/04/FlowOfUITouch/)

系統響應階段

1.手指觸碰屏幕,屏幕感應到觸碰后,將事件交由IOKit處理。

2.IOKit將觸摸事件封裝成一個IOHIDEvent對象,并通過mach port傳遞給SpringBoad進程。

mach port 進程端口,各進程之間通過它進行通信。

SpringBoad.app 是一個系統進程,可以理解為桌面系統,可以統一管理和分發系統接收到的觸摸事件。

3.SpringBoard進程因接收到觸摸事件,觸發了主線程runloop的source1事件源的回調。

此時SpringBoard會根據當前桌面的狀態,判斷應該由誰處理此次觸摸事件。因為事件發生時,你可能正在桌面上翻頁,也可能正在刷微博。若是前者(即前臺無APP運行),則觸發SpringBoard本身主線程runloop的source0事件源的回調,將事件交由桌面系統去消耗;若是后者(即有app正在前臺運行),則將觸摸事件通過IPC傳遞給前臺APP進程,接下來的事情便是APP內部對于觸摸事件的響應了。

APP響應階段

  • APP進程的mach port接受到SpringBoard進程傳遞來的觸摸事件,主線程的runloop被喚醒,觸發了source1回調。

  • source1回調又觸發了一個source0回調,將接收到的IOHIDEvent對象封裝成UIEvent對象,此時APP將正式開始對于觸摸事件的響應。

  • source0回調內部將觸摸事件添加到UIApplication對象的事件隊列中。事件出隊后,UIApplication開始一個尋找最佳響應者的過程,這個過程又稱hit-testing,細節將在[尋找事件的最佳響應者]一節闡述。另外,此處開始便是與我們平時開發相關的工作了。

  • 尋找到最佳響應者后,接下來的事情便是事件在響應鏈中的傳遞及響應了,關于響應鏈相關的內容詳見[事件的響應及在響應鏈中的傳遞]一節。事實上,事件除了被響應者消耗,還能被手勢識別器或是target-action模式捕捉并消耗掉。其中涉及對觸摸事件的響應優先級,詳見[事件的三徒弟UIResponder、UIGestureRecognizer、UIControl]一節。

  • 觸摸事件歷經坎坷后要么被某個響應對象捕獲后釋放,要么致死也沒能找到能夠響應的對象,最終釋放。至此,這個觸摸事件的使命就算終結了。runloop若沒有其他事件需要處理,也將重歸于眠,等待新的事件到來后喚醒。

  • 現在,你可以回答第一個問題了。觸摸事件從觸屏產生后,由IOKit將觸摸事件傳遞給SpringBoard進程,再由SpringBoard分發給當前前臺APP處理。

    觸摸、事件、響應者

    說了那么多,到底什么是觸摸、什么是事件、什么是響應者?先簡單科普一下。

    UITouch

    源起觸摸

    • 一個手指一次觸摸屏幕,就對應生成一個UITouch對象。多個手指同時觸摸,生成多個UITouch對象。

    • 多個手指先后觸摸,系統會根據觸摸的位置判斷是否更新同一個UITouch對象。若兩個手指一前一后觸摸同一個位置(即雙擊),那么第一次觸摸時生成一個UITouch對象,第二次觸摸更新這個UITouch對象(UITouch對象的 tap count 屬性值從1變成2);若兩個手指一前一后觸摸的位置不同,將會生成兩個UITouch對象,兩者之間沒有聯系。

    • 每個UITouch對象記錄了觸摸的一些信息,包括觸摸時間、位置、階段、所處的視圖、窗口等信息。

    //觸摸的各個階段狀態? //例如當手指移動時,會更新phase屬性到UITouchPhaseMoved;手指離屏后,更新到UITouchPhaseEnded typedef?NS_ENUM(NSInteger,?UITouchPhase)?{UITouchPhaseBegan,?????????????//?whenever?a?finger?touches?the?surface.UITouchPhaseMoved,?????????????//?whenever?a?finger?moves?on?the?surface.UITouchPhaseStationary,????????//?whenever?a?finger?is?touching?the?surface?but?hasn't?moved?since?the?previous?event.UITouchPhaseEnded,?????????????//?whenever?a?finger?leaves?the?surface.UITouchPhaseCancelled,?????????//?whenever?a?touch?doesn't?end?but?we?need?to?stop?tracking?(e.g.?putting?device?to?face) };
    • 手指離開屏幕一段時間后,確定該UITouch對象不會再被更新將被釋放。

    UIEvent

    事件的真身

    • 觸摸的目的是生成觸摸事件供響應者響應,一個觸摸事件對應一個UIEvent對象,其中的 type 屬性標識了事件的類型(之前說過事件不只是觸摸事件)。

    • UIEvent對象中包含了觸發該事件的觸摸對象的集合,因為一個觸摸事件可能是由多個手指同時觸摸產生的。觸摸對象集合通過 allTouches 屬性獲取。

    UIResponder

    一切為了滿足它的野心

    每個響應者都是一個UIResponder對象,即所有派生自UIResponder的對象,本身都具備響應事件的能力。因此以下類的實例都是響應者:

    • UIView

    • UIViewController

    • UIApplication

    • AppDelegate

    響應者之所以能響應事件,因為其提供了4個處理觸摸事件的方法:

    //手指觸碰屏幕,觸摸開始 -?(void)touchesBegan:(NSSet<UITouch?*>?*)touches?withEvent:(nullable?UIEvent?*)event; //手指在屏幕上移動 -?(void)touchesMoved:(NSSet<UITouch?*>?*)touches?withEvent:(nullable?UIEvent?*)event; //手指離開屏幕,觸摸結束 -?(void)touchesEnded:(NSSet<UITouch?*>?*)touches?withEvent:(nullable?UIEvent?*)event; //觸摸結束前,某個系統事件中斷了觸摸,例如電話呼入 -?(void)touchesCancelled:(NSSet<UITouch?*>?*)touches?withEvent:(nullable?UIEvent?*)event;

    這幾個方法在響應者對象接收到事件的時候調用,用于做出對事件的響應。關于響應者何時接收到事件以及事件如何沿著響應鏈傳遞將在下面章節說明。

    尋找事件的最佳響應者(Hit-Testing)

    第一節講過APP接收到觸摸事件后,會被放入當前應用的一個事件隊列中(PS為什么是隊列而不是棧?很好理解因為觸摸事件必然是先發生先執行,切合隊列FIFO的原則)。

    每個事件的理想宿命是被能夠響應它的對象響應后釋放,然而響應者諸多,事件一次只有一個,誰都想把事件搶到自己碗里來,為避免紛爭,就得有一個先后順序,也就是得有一個響應者的優先級。因此這就存在一個尋找事件最佳響應者(又稱第一響應者 first responder)的過程,目的是找到一個具備最高優先級響應權的響應對象(the most appropriate responder object),這個過程叫做Hit-Testing,那個命中的最佳響應者稱為hit-tested view。

    本節要探討的問題是:

  • 應用接收到事件后,如何尋找最佳響應者?底層如何實現?

  • 尋找最佳響應者過程中事件的攔截。

  • 事件自下而上的傳遞

    應用接收到事件后先將其置入事件隊列中以等待處理。出隊后,application首先將事件傳遞給當前應用最后顯示的窗口(UIWindow)詢問其能否響應事件。若窗口能響應事件,則傳遞給子視圖詢問是否能響應,子視圖若能響應則繼續詢問子視圖。子視圖詢問的順序是優先詢問后添加的子視圖,即子視圖數組中靠后的視圖。事件傳遞順序如下:

    UIApplication?——>?UIWindow?——>?子視圖?——>?...?——>?子視圖

    事實上把UIWindow也看成是視圖即可,這樣整個傳遞過程就是一個遞歸詢問子視圖能否響應事件過程,且后添加的子視圖優先級高(對于window而言就是后顯示的window優先級高)。

    具體流程如下:

  • UIApplication首先將事件傳遞給窗口對象(UIWindow),若存在多個窗口,則優先詢問后顯示的窗口。

  • 若窗口不能響應事件,則將事件傳遞其他窗口;若窗口能響應事件,則從后往前詢問窗口的子視圖。

  • 重復步驟2。即視圖若不能響應,則將事件傳遞給上一個同級子視圖;若能響應,則從后往前詢問當前視圖的子視圖。

  • 視圖若沒有能響應的子視圖了,則自身就是最合適的響應者。

  • 示例:

    hit-testing 場景

    視圖層級如下(同一層級的視圖越在下面,表示越后添加):

    A ├──?B │???└──?D └──?C├──?E└──?F

    現在假設在E視圖所處的屏幕位置觸發一個觸摸,應用接收到這個觸摸事件事件后,先將事件傳遞給UIWindow,然后自下而上開始在子視圖中尋找最佳響應者。事件傳遞的順序如下所示:

  • UIWindow將事件傳遞給其子視圖A

  • A判斷自身能響應該事件,繼續將事件傳遞給C(因為視圖C比視圖B后添加,因此優先傳給C)。

  • C判斷自身能響應事件,繼續將事件傳遞給F(同理F比E后添加)。

  • F判斷自身不能響應事件,C又將事件傳遞給E。

  • E判斷自身能響應事件,同時E已經沒有子視圖,因此最終E就是最佳響應者。

  • Hit-Testing的本質

    上面講了事件在響應者之間傳遞的規則,視圖通過判斷自身能否響應事件來決定是否繼續向子視圖傳遞。那么問題來了:視圖如何判斷能否響應事件?以及視圖如何將事件傳遞給子視圖?

    首先要知道的是,以下幾種狀態的視圖無法響應事件:

    • 不允許交互:userInteractionEnabled = NO

    • 隱藏:hidden = YES 如果父視圖隱藏,那么子視圖也會隱藏,隱藏的視圖無法接收事件

    • 透明度:alpha < 0.01 如果設置一個視圖的透明度<0.01,會直接影響子視圖的透明度。alpha:0.0~0.01為透明。

    hitTest:withEvent:

    每個UIView對象都有一個 hitTest:withEvent: 方法,這個方法是Hit-Testing過程中最核心的存在,其作用是詢問事件在當前視圖中的響應者,同時又是作為事件傳遞的橋梁。

    hitTest:withEvent: 方法返回一個UIView對象,作為當前視圖層次中的響應者。默認實現是:

    • 若當前視圖無法響應事件,則返回nil

    • 若當前視圖可以響應事件,但無子視圖可以響應事件,則返回自身作為當前視圖層次中的事件響應者

    • 若當前視圖可以響應事件,同時有子視圖可以響應,則返回子視圖層次中的事件響應者

    一開始UIApplication將事件通過調用UIWindow對象的 hitTest:withEvent: 傳遞給UIWindow對象,UIWindow的 hitTest:withEvent: 在執行時若判斷本身能響應事件,則調用子視圖的 hitTest:withEvent: 將事件傳遞給子視圖并詢問子視圖上的最佳響應者。最終UIWindow返回一個視圖層次中的響應者視圖給UIApplication,這個視圖就是hit-testing的最佳響應者。

    系統對于視圖能否響應事件的判斷邏輯除了之前提到的3種限制狀態,默認能響應的條件就是觸摸點在當前視圖的坐標系范圍內。因此,hitTest:withEvent: 的默認實現就可以推測了,大致如下:

    -?(UIView?*)hitTest:(CGPoint)point?withEvent:(UIEvent?*)event{//3種狀態無法響應事件if?(self.userInteractionEnabled?==?NO?||?self.hidden?==?YES?||??self.alpha?<=?0.01)?return?nil;?//觸摸點若不在當前視圖上則無法響應事件if?([self?pointInside:point?withEvent:event]?==?NO)?return?nil;?//從后往前遍歷子視圖數組?int?count?=?(int)self.subviews.count;?for?(int?i?=?count?-?1;?i?>=?0;?i--)?{?//?獲取子視圖UIView?*childView?=?self.subviews[i];?//?坐標系的轉換,把觸摸點在當前視圖上坐標轉換為在子視圖上的坐標CGPoint?childP?=?[self?convertPoint:point?toView:childView];?//詢問子視圖層級中的最佳響應視圖UIView?*fitView?=?[childView?hitTest:childP?withEvent:event];?if?(fitView)?{//如果子視圖中有更合適的就返回return?fitView;?}}?//沒有在子視圖中找到更合適的響應視圖,那么自身就是最合適的return?self; }

    值得注意的是 pointInside:withEvent: 這個方法,用于判斷觸摸點是否在自身坐標范圍內。默認實現是若在坐標范圍內則返回YES,否則返回NO。

    現在我們在上述示例的視圖層次中的每個視圖類中添加下面3個方法來驗證一下之前的分析(注意 hitTest:withEvent: 和 pointInside:withEvent: 方法都要調用父類的實現,否則不會按照默認的邏輯來執行Hit-Testing):

    -?(UIView?*)hitTest:(CGPoint)point?withEvent:(UIEvent?*)event{NSLog(@"%s",__func__);return?[super?hitTest:point?withEvent:event]; } -?(BOOL)pointInside:(CGPoint)point?withEvent:(UIEvent?*)event{NSLog(@"%s",__func__);return?[super?pointInside:point?withEvent:event]; } -?(void)touchesBegan:(NSSet<UITouch?*>?*)touches?withEvent:(UIEvent?*)event{NSLog(@"%s",__func__); }

    單點觸摸視圖E,相關日志打印如下:

    -[AView?hitTest:withEvent:] -[AView?pointInside:withEvent:] -[CView?hitTest:withEvent:] -[CView?pointInside:withEvent:] -[FView?hitTest:withEvent:] -[FView?pointInside:withEvent:] -[EView?hitTest:withEvent:] -[EView?pointInside:withEvent:] -[EView?touchesBegan:withEvent:]

    可以看到最終是視圖E先對事件進行了響應,同時事件傳遞過程也和之前的分析一致。事實上單擊后從 [AView hitTest:withEvent:] 到 [EView pointInside:withEvent:] 的過程會執行兩遍,兩次傳的是同一個touch,區別在于touch的狀態不同,第一次是begin階段,第二次是end階段。也就是說,應用對于事件的傳遞起源于觸摸狀態的變化

    Hit-Testing過程中的事件攔截(自定義事件流向)

    實際開發中可能會遇到一些特殊的交互需求,需要定制視圖對于事件的響應。例如下面Tabbar的這種情況,中間的原型按鈕是底部Tabbar上的控件,而Tabbar是添加在控制器根視圖中的。默認情況下我們點擊圖中紅色方框中按鈕的區域,會發現按鈕并不會得到響應。

    hit-testing過程中事件攔截場景

    分析一下原因其實很容易就能明白問題所在。忽略不相關的控件,視圖層次如下:

    RootView └──?TableView └──?TabBar└──?CircleButton

    點擊紅色方框區域后,生成的觸摸事件首先傳到UIWindow,然后傳到控制器的根視圖即RootView。RootView經判斷可以響應觸摸事件,而后將事件傳給了子控件TabBar。問題就出在這里,因為觸摸點不在TabBar的坐標范圍內,因此TabBar無法響應該觸摸事件,hitTest:withEvent: 直接返回了nil。而后RootView就會詢問TableView是否能夠響應,事實上是可以的,因此事件最終被TableView消耗。整個過程,事件根本沒有傳遞到圓形按鈕。

    有問題就會有解決策略。經過分析,發現原因是hit-Testing的過程中,事件在傳遞到TabBar的時候沒能繼續往CircleButton傳,因為點擊區域坐標不在Tabbar的坐標范圍內,因此Tabbar被識別成了無法響應事件。既然如此,我們可以修改事件hit-Testing的過程,當點擊紅色方框區域時讓事件流向原型按鈕。

    事件傳遞到TabBar時,TabBar的 hitTest:withEvent: 被調用,但是 pointInside:withEvent: 會返回NO,如此一來 hitTest:withEvent: 返回了nil。既然如此,可以重寫TabBard的 pointInside:withEvent: ,判斷當前觸摸坐標是否在子視圖CircleButton的坐標范圍內,若在,則返回YES,反之返回NO。這樣一來點擊紅色區域,事件最終會傳遞到CircleButton,CircleButton能夠響應事件,最終事件就由CircleButton響應了。同時點擊紅色方框以外的非TabBar區域的情況下,因為TabBar無法響應事件,會按照預期由TableView響應。代碼如下:

    //TabBar -?(BOOL)pointInside:(CGPoint)point?withEvent:(UIEvent?*)event {//將觸摸點坐標轉換到在CircleButton上的坐標CGPoint?pointTemp?=?[self?convertPoint:point?toView:_CircleButton];//若觸摸點在CricleButton上則返回YESif?([_CircleButton?pointInside:pointTemp?withEvent:event])?{return?YES;}//否則返回默認的操作return?[super?pointInside:point?withEvent:event]; }

    這樣一來,點擊紅色方框區域的按鈕就有效了。

    現在第二個問題也可以回答了。另外項目中如遇到不按常理出牌的事件響應需求,相信你也應該可以應對了。

    事件的響應及在響應鏈中的傳遞

    經歷Hit-Testing后,UIApplication已經知道事件的最佳響應者是誰了,接下來要做的事情就是:

  • 將事件傳遞給最佳響應者響應

  • 事件沿著響應鏈傳遞

  • 事件響應的前奏

    因為最佳響應者具有最高的事件響應優先級,因此UIApplication會先將事件傳遞給它供其響應。首先,UIApplication將事件通過 sendEvent: 傳遞給事件所屬的window,window同樣通過 sendEvent: 再將事件傳遞給hit-tested view,即最佳響應者。過程如下:

    UIApplication?——>?UIWindow?——>?hit-tested?view

    以尋找事件的最佳響應者一節中點擊視圖E為例,在EView的 touchesBegan:withEvent: 上斷點查看調用棧就能看清這一過程:

    touchesBegan調用棧

    那么問題又來了。這個過程中,假如應用中存在多個window對象,UIApplication是怎么知道要把事件傳給哪個window的?window又是怎么知道哪個視圖才是最佳響應者的呢?

    其實簡單思考一下,這兩個過程都是傳遞事件的過程,涉及的方法都是 sendEvent: ,而該方法的參數(UIEvent對象)是唯一貫穿整個經過的線索,那么就可以大膽猜測必然是該觸摸事件對象上綁定了這些信息。事實上之前在介紹UITouch的時候就說過touch對象保存了觸摸所屬的window及view,而event對象又綁定了touch對象,如此一來,是不是就說得通了。要是不信的話,那就自定義一個Window類,重寫 sendEvent: 方法,捕捉該方法調用時參數event的狀態,答案就顯而易見了。

    sendEvent

    至于這兩個屬性是什么時候綁定到touch對象上的,必然是在hit-testing的過程中唄,仔細想想hit-testing干的不就是這個事兒嗎~

    事件的響應

    前面介紹UIResponder的時候說過,每個響應者必定都是UIResponder對象,通過4個響應觸摸事件的方法來響應事件。每個UIResponder對象默認都已經實現了這4個方法,但是默認不對事件做任何處理,單純只是將事件沿著響應鏈傳遞。若要截獲事件進行自定義的響應操作,就要重寫相關的方法。例如,通過重寫 touchesMoved: withEvent: 方法實現簡單的視圖拖動。

    -?(void)touchesMoved:(NSSet<UITouch?*>?*)touches?withEvent:(nullable?UIEvent?*)event;

    每個響應觸摸事件的方法都會接收兩個參數,分別對應觸摸對象集合和事件對象。通過監聽觸摸對象中保存的觸摸點位置的變動,可以時時修改視圖的位置。視圖(UIView)作為響應者對象,本身已經實現了 touchesMoved: withEvent: 方法,因此要創建一個自定義視圖(繼承自UIView),重寫該方法。

    //MovedView //重寫touchesMoved方法(觸摸滑動過程中持續調用) -?(void)touchesMoved:(NSSet<UITouch?*>?*)touches?withEvent:(UIEvent?*)event {//獲取觸摸對象UITouch?*touch?=?[touches?anyObject];//獲取前一個觸摸點位置CGPoint?prePoint?=?[touch?previousLocationInView:self];//獲取當前觸摸點位置CGPoint?curPoint?=?[touch?locationInView:self];//計算偏移量CGFloat?offsetX?=?curPoint.x?-?prePoint.x;CGFloat?offsetY?=?curPoint.y?-?prePoint.y;//相對之前的位置偏移視圖self.transform?=?CGAffineTransformTranslate(self.transform,?offsetX,?offsetY); }

    每個響應者都有權決定是否執行對事件的響應,只要重寫相關的觸摸事件方法即可。

    事件的傳遞(響應鏈)

    前面一直在提最佳響應者,之所以稱之為“最佳”,是因為其具備響應事件的最高優先權(響應鏈頂端的男人)。最佳響應者首先接收到事件,然后便擁有了對事件的絕對控制權:即它可以選擇獨吞這個事件,也可以將這個事件往下傳遞給其他響應者,這個由響應者構成的視圖鏈就稱之為響應鏈。

    需要注意的是,上一節中也說到了事件的傳遞,與此處所說的事件的傳遞有本質區別。上一節所說的事件傳遞的目的是為了尋找事件的最佳響應者,是自下而上的傳遞;而這里的事件傳遞目的是響應者做出對事件的響應,這個過程是自上而下的。前者為“尋找”,后者為“響應”。

    響應者對于事件的操作方式:

    響應者對于事件的攔截以及傳遞都是通過 touchesBegan:withEvent: 方法控制的,該方法的默認實現是將事件沿著默認的響應鏈往下傳遞。

    響應者對于接收到的事件有3種操作:

    • 不攔截,默認操作。事件會自動沿著默認的響應鏈往下傳遞

    • 攔截,不再往下分發事件。重寫 touchesBegan:withEvent: 進行事件處理,不調用父類的 touchesBegan:withEvent:

    • 攔截,繼續往下分發事件。重寫 touchesBegan:withEvent: 進行事件處理,同時調用父類的 touchesBegan:withEvent: 將事件往下傳遞

    響應鏈中的事件傳遞規則:

    每一個響應者對象(UIResponder對象)都有一個 nextResponder 方法,用于獲取響應鏈中當前對象的下一個響應者。因此,一旦事件的最佳響應者確定了,這個事件所處的響應鏈就確定了。

    對于響應者對象,默認的 nextResponder 實現如下:

    • UIView。若視圖是控制器的根視圖,則其nextResponder為控制器對象;否則,其nextResponder為父視圖。

    • UIViewController。若控制器的視圖是window的根視圖,則其nextResponder為窗口對象;若控制器是從別的控制器present出來的,則其nextResponder為presenting view controller。

    • UIWindow。nextResponder為UIApplication對象。

    • UIApplication。若當前應用的app delegate是一個UIResponder對象,且不是UIView、UIViewController或app本身,則UIApplication的nextResponder為app delegate。

    上圖是官網對于響應鏈的示例展示,若觸摸發生在UITextField上,則事件的傳遞順序是:

    UITextField?——>?UIView?——>?UIView?——>?UIViewController?——>?UIWindow?——>?UIApplication?——>?UIApplicationDelegation

    圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的,則其nextResponder為UIViewController對象;若是直接add在UIWindow上的,則其nextResponder為UIWindow對象。

    可以用以下方式打印一個響應鏈中的每一個響應對象,在最佳響應者的 touchBegin:withEvent: 方法中調用即可(別忘了調用父類的方法)

    -?(void)printResponderChain {UIResponder?*responder?=?self;printf("%s",[NSStringFromClass([responder?class])?UTF8String]);while?(responder.nextResponder)?{responder?=?responder.nextResponder;printf("?-->?%s",[NSStringFromClass([responder?class])?UTF8String]);} }

    以上一節原型按鈕的案例為例,重寫CircleButton的 touchBegin:withEvent:

    -?(void)touchesBegan:(NSSet<UITouch?*>?*)touches?withEvent:(UIEvent?*)event {[self?printResponderChain];[super?touchesBegan:touches?withEvent:event]; }

    點擊原型按鈕的任意區域,打印出的完整響應鏈如下:

    CircleButton --> CustomeTabBar --> UIView --> UIViewController --> UIViewControllerWrapperView --> UINavigationTransitionView --> UILayoutContainerView --> UINavigationController --> UIWindow --> UIApplication --> AppDelegate

    另外如果有需要,完全可以重寫響應者的 nextResponder 方法來自定義響應鏈。

    現在,第三個問題也解決了。

    事件的三徒弟UIResponder、UIGestureRecognizer、UIControl

    iOS中,除了UIResponder能夠響應事件,手勢識別器、UIControl同樣具備對事件的處理能力。當這幾者同時存在于某一場景下的時候,事件又會有怎樣的歸宿呢?

    拋磚引玉

    場景界面如圖:

    手勢沖突場景

    代碼不能再簡單:

    -?(void)viewDidLoad?{[super?viewDidLoad];//底部是一個綁定了單擊手勢的backViewUITapGestureRecognizer?*tap?=?[[UITapGestureRecognizer?alloc]?initWithTarget:self?action:@selector(actionTapView)];[_backView?addGestureRecognizer:tap];//上面是一個常規的tableView_tableMain.tableFooterView?=?[UIView?new];//還有一個和tableView同級的button[_button?addTarget:self?action:@selector(buttonTap)?forControlEvents:UIControlEventTouchUpInside]; }-?(void)actionTapView{NSLog(@"backview?taped"); }-?(void)buttonTap?{NSLog(@"button?clicked!"); }-?(void)tableView:(UITableView?*)tableView?didSelectRowAtIndexPath:(NSIndexPath?*)indexPath{NSLog(@"cell?selected!"); }

    然后我像往常一樣懷揣著吃奶的自信點擊了cell。what??點不動??點歪了嗎??再點,還是沒反應!!我試著短按了一小會兒cell,依舊沒反應!!我不死心,長按了一會兒,didSelectRowAtIndexPath終于調了,還算給點面子 - -。然后我又點了下面的button,沒有任何問題。but what ??

    為了搞清楚狀況,我自定義了相關的控件類,均重寫了4個響應觸摸事件的方法以打印日志(每個重寫的觸摸事件方法都調用了父類的方法以保證事件默認傳遞邏輯)。

    觀察各種情況下的日志現象:

    現象一 快速點擊cell

    backview?taped

    現象二 短按cell

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

    現象三 長按cell

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

    現象四 點擊button

    -[GLButton?touchesBegan:withEvent:] -[GLButton?touchesEnded:withEvent:] button?clicked!

    如果上面的現象依舊能讓你舒心地抿上一口咖啡,那么恭喜你,本節的內容已經不適合你了。如果覺得一臉懵逼,那就繼續往下看吧~

    二師兄—手勢識別器

    關于手勢識別器即 UIGestureRecognizer 本身的使用不是本文要所討論的內容,按下不表。此處要探討的是:手勢識別器與UIResponder的聯系。

    事實上,手勢分為離散型手勢(discrete gestures)和持續型手勢(continuous gesture)。系統提供的離散型手勢包括點按手勢(UITapGestureRecognizer)和輕掃手勢(UISwipeGestureRecognizer),其余均為持續型手勢。

    兩者主要區別在于狀態變化過程:

    離散型:

    • 識別成功:Possible —> Recognized

    • 識別失敗:Possible —> Failed

    持續型:

    • 完整識別:Possible —> Began —> [Changed] —> Ended

    • 不完整識別:Possible —> Began —> [Changed] —> Cancel

    離散型手勢

    先拋開上面的場景,看一個簡單的demo。

    控制器的視圖上add了一個View記為YellowView,并綁定了一個單擊手勢識別器。

    //?LXFViewController -?(void)viewDidLoad?{[super?viewDidLoad];UITapGestureRecognizer?*tap?=?[[UITapGestureRecognizer?alloc]?initWithTarget:self?action:@selector(actionTap)];[self.view?addGestureRecognizer:tap]; } -?(void)actionTap{NSLog(@"View?Taped"); }

    單擊YellowView,日志打印如下:

    -[YellowView?touchesBegan:withEvent:] View?Taped -[YellowView?touchesCancelled:withEvent:]

    從日志上看出YellowView最后Cancel了對觸摸事件的響應,而正常應當是觸摸結束后,YellowView的 touchesEnded:withEvent: 的方法被調用才對。另外,期間還執行了手勢識別器綁定的action 。我從官方文檔找到了這樣的解釋:

    A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties.

    大致理解是,Window在將事件傳遞給hit-tested view之前,會先將事件傳遞給相關的手勢識別器并由手勢識別器優先識別。若手勢識別器成功識別了事件,就會取消hit-tested view對事件的響應;若手勢識別器沒能識別事件,hit-tested view才完全接手事件的響應權。

    一句話概括:手勢識別器比UIResponder具有更高的事件響應優先級!!

    按照這個解釋,Window在將事件傳遞給hit-tested view即YellowView之前,先傳遞給了控制器根視圖上的手勢識別器。手勢識別器成功識別了該事件,通知Application取消YellowView對事件的響應。

    然而看日志,卻是YellowView的 touchesBegan:withEvent: 先調用了,既然手勢識別器先響應,不應該上面的action先執行嗎,這又怎么解釋?事實上這個認知是錯誤的。手勢識別器的action的調用時機(即此處的 actionTap)并不是手勢識別器接收到事件的時機,而是手勢識別器成功識別事件后的時機,即手勢識別器的狀態變為UIGestureRecognizerStateRecognized。因此從該日志中并不能看出事件是優先傳遞給手勢識別器的,那該怎么證明Window先將事件傳遞給了手勢識別器?

    要解決這個問題,只要知道手勢識別器是如何接收事件的,然后在接收事件的方法中打印日志對比調用時間先后即可。說起來你可能不信,手勢識別器對于事件的響應也是通過這4個熟悉的方法來實現的。

    -?(void)touchesBegan:(NSSet*)touches?withEvent:(UIEvent?*)event; -?(void)touchesMoved:(NSSet*)touches?withEvent:(UIEvent?*)event; -?(void)touchesEnded:(NSSet*)touches?withEvent:(UIEvent?*)event; -?(void)touchesCancelled:(NSSet*)touches?withEvent:(UIEvent?*)event;

    需要注意的是,雖然手勢識別器通過這幾個方法來響應事件,但它并不是UIResponder的子類,相關的方法聲明在 UIGestureRecognizerSubclass.h 中。

    這樣一來,我們便可以自定義一個單擊手勢識別器的類,重寫這幾個方法來監聽手勢識別器接收事件的時機。創建一個UITapGestureRecognizer的子類,重寫響應事件的方法,每個方法中調用父類的實現,并替換demo中的手勢識別器。另外需要在.m文件中引入 import,因為相關方法聲明在該頭文件中。

    //?LXFTapGestureRecognizer?(繼承自UITapGestureRecognizer) -?(void)touchesBegan:(NSSet*)touches?withEvent:(UIEvent?*)event{NSLog(@"%s",__func__);[super?touchesBegan:touches?withEvent:event]; } -?(void)touchesMoved:(NSSet*)touches?withEvent:(UIEvent?*)event{NSLog(@"%s",__func__);[super?touchesMoved:touches?withEvent:event]; } -?(void)touchesEnded:(NSSet*)touches?withEvent:(UIEvent?*)event{NSLog(@"%s",__func__);[super?touchesEnded:touches?withEvent:event]; } -?(void)touchesCancelled:(NSSet*)touches?withEvent:(UIEvent?*)event{NSLog(@"%s",__func__);[super?touchesCancelled:touches?withEvent:event]; }

    現在,再次點擊YellowView,日志如下:

    -[LXFTapGestureRecognizer?touchesBegan:withEvent:] -[YellowView?touchesBegan:withEvent:] -[LXFTapGestureRecognizer?touchesEnded:withEvent:] View?Taped -[YellowView?touchesCancelled:withEvent:]

    很明顯,確實是手勢識別器先接收到了事件。之后手勢識別器成功識別了手勢,執行了action,再由Application取消了YellowView對事件的響應。

    Window怎么知道要把事件傳遞給哪些手勢識別器?

    之前探討過Application怎么知道要把event傳遞給哪個Window,以及Window怎么知道要把event傳遞給哪個hit-tested view的問題,答案是這些信息都保存在event所綁定的touch對象上。手勢識別器也是一樣的,event綁定的touch對象上維護了一個手勢識別器數組,里面的手勢識別器毫無疑問是在hit-testing的過程中收集的。打個斷點看一下touch上綁定的手勢識別器數組:

    Window先將事件傳遞給這些手勢識別器,再傳給hit-tested view。一旦有手勢識別器成功識別了手勢,Application就會取消hit-tested view對事件的響應。

    持續型手勢

    將上面Demo中視圖綁定的單擊手勢識別器用滑動手勢識別器(UIPanGestureRecognizer)替換。

    -?(void)viewDidLoad?{[super?viewDidLoad];UIPanGestureRecognizer?*pan?=?[[UIPanGestureRecognizer?alloc]?initWithTarget:self?action:@selector(actionPan)];[self.view?addGestureRecognizer:pan]; } -?(void)actionPan{NSLog(@"View?panned"); }

    在YellowView上執行一次滑動:

    日志打印如下:

    -[YellowView?touchesBegan:withEvent:] -[YellowView?touchesMoved:withEvent:] -[YellowView?touchesMoved:withEvent:] -[YellowView?touchesMoved:withEvent:] View?panned -[YellowView?touchesCancelled:withEvent:] View?panned View?panned View?panned ...

    在一開始滑動的過程中,手勢識別器處在識別手勢階段,滑動產生的連續事件既會傳遞給手勢識別器又會傳遞給YellowView,因此YellowView的 touchesMoved:withEvent: 在開始一段時間內會持續調用;當手勢識別器成功識別了該滑動手勢時,手勢識別器的action開始調用,同時通知Application取消YellowView對事件的響應。之后僅由滑動手勢識別器接收事件并響應,YellowView不再接收事件。

    另外,在滑動的過程中,若手勢識別器未能識別手勢,則事件在觸摸滑動過程中會一直傳遞給hit-tested view,直到觸摸結束。讀者可自行驗證。

    手勢識別器的3個屬性

    @property(nonatomic)?BOOL?cancelsTouchesInView; @property(nonatomic)?BOOL?delaysTouchesBegan; @property(nonatomic)?BOOL?delaysTouchesEnded;

    先總結一下手勢識別器與UIResponder對于事件響應的聯系:

    當觸摸發生或者觸摸的狀態發生變化時,Window都會傳遞事件尋求響應。

    • Window先將綁定了觸摸對象的事件傳遞給觸摸對象上綁定的手勢識別器,再發送給觸摸對象對應的hit-tested view。

    • 手勢識別器識別手勢期間,若觸摸對象的觸摸狀態發生變化,事件都是先發送給手勢識別器再發送給hit-test view。

    • 手勢識別器若成功識別了手勢,則通知Application取消hit-tested view對于事件的響應,并停止向hit-tested view發送事件;

    • 若手勢識別器未能識別手勢,而此時觸摸并未結束,則停止向手勢識別器發送事件,僅向hit-test view發送事件。

    • 若手勢識別器未能識別手勢,且此時觸摸已經結束,則向hit-tested view發送end狀態的touch事件以停止對事件的響應。

    cancelsTouchesInView

    默認為YES。表示當手勢識別器成功識別了手勢之后,會通知Application取消響應鏈對事件的響應,并不再傳遞事件給hit-test view。若設置成NO,表示手勢識別成功后不取消響應鏈對事件的響應,事件依舊會傳遞給hit-test view。

    demo中設置: pan.cancelsTouchesInView = NO

    滑動時日志如下:

    -[YellowView?touchesBegan:withEvent:] -[YellowView?touchesMoved:withEvent:] -[YellowView?touchesMoved:withEvent:] -[YellowView?touchesMoved:withEvent:] View?panned -[YellowView?touchesMoved:withEvent:] View?panned View?panned -[YellowView?touchesMoved:withEvent:] View?panned -[YellowView?touchesMoved:withEvent:] ...

    即便滑動手勢識別器識別了手勢,Application也會依舊發送事件給YellowView。

    delaysTouchesBegan

    默認為NO。默認情況下手勢識別器在識別手勢期間,當觸摸狀態發生改變時,Application都會將事件傳遞給手勢識別器和hit-tested view;若設置成YES,則表示手勢識別器在識別手勢期間,截斷事件,即不會將事件發送給hit-tested view。

    設置 pan.delaysTouchesBegan = YES

    日志如下:

    View?panned View?panned View?panned View?panned ...

    因為滑動手勢識別器在識別期間,事件不會傳遞給YellowView,因此期間YellowView的 touchesBegan:withEvent: 和 touchesMoved:withEvent: 都不會被調用;而后滑動手勢識別器成功識別了手勢,也就獨吞了事件,不會再傳遞給YellowView。因此只打印了手勢識別器成功識別手勢后的action調用。

    delaysTouchesEnded

    默認為YES。當手勢識別失敗時,若此時觸摸已經結束,會延遲一小段時間(0.15s)再調用響應者的 touchesEnded:withEvent:;若設置成NO,則在手勢識別失敗時會立即通知Application發送狀態為end的touch事件給hit-tested view以調用 touchesEnded:withEvent: 結束事件響應。

    總結:手勢識別器比響應鏈具有更高的事件響應優先級。

    大師兄—UIControl

    UIControl是系統提供的能夠以target-action模式處理觸摸事件的控件,iOS中UIButton、UISegmentedControl、UISwitch等控件都是UIControl的子類。當UIControl跟蹤到觸摸事件時,會向其上添加的target發送事件以執行action。值得注意的是,UIConotrol是UIView的子類,因此本身也具備UIResponder應有的身份。

    關于UIControl,此處介紹兩點:

  • target-action執行時機及過程

  • 觸摸事件優先級

  • target-action

    • target:處理交互事件的對象

    • action:處理交互事件的方式

    UIControl作為能夠響應事件的控件,必然也需要待事件交互符合條件時才去響應,因此也會跟蹤事件發生的過程。不同于UIControl以及UIGestureRecognizer通過 touches 系列方法跟蹤,UIControl有其獨特的跟蹤方式:

    -?(BOOL)beginTrackingWithTouch:(UITouch?*)touch?withEvent:(nullable?UIEvent?*)event; -?(BOOL)continueTrackingWithTouch:(UITouch?*)touch?withEvent:(nullable?UIEvent?*)event; -?(void)endTrackingWithTouch:(nullable?UITouch?*)touch?withEvent:(nullable?UIEvent?*)event; -?(void)cancelTrackingWithEvent:(nullable?UIEvent?*)event;

    乍一看,這4個方法和UIResponder的那4個方法幾乎吻合,只不過UIControl只能接收單點觸控,因此接收的參數是單個UITouch對象。這幾個方法的職能也和UIResponder一致,用來跟蹤觸摸的開始、滑動、結束、取消。不過,UIControl本身也是UIResponder,因此同樣有 touches 系列的4個方法。事實上,UIControl的 Tracking 系列方法是在 touch 系列方法內部調用的。比如 beginTrackingWithTouch 是在 touchesBegan 方法內部調用的, 因此它雖然也是UIResponder,但 touches 系列方法的默認實現和UIResponder本類還是有區別的。

    當UIControl跟蹤事件的過程中,識別出事件交互符合響應條件,就會觸發target-action進行響應。UIControl控件通過 addTarget:action:forControlEvents: 添加事件處理的target和action,當事件發生時,UIControl通知target執行對應的action。說是“通知”其實很籠統,事實上這里有個action傳遞的過程。當UIControl監聽到需要處理的交互事件時,會調用 sendAction:to:forEvent: 將target、action以及event對象發送給全局應用,Application對象再通過 sendAction:to:from:forEvent: 向target發送action。

    因此,可以通過重寫UIControl的 sendAction:to:forEvent: 或 sendAction:to:from:forEvent: 自定義事件執行的target及action。

    另外,若不指定target,即 addTarget:action:forControlEvents: 時target傳空,那么當事件發生時,Application會在響應鏈上從上往下尋找能響應action的對象。官方說明如下:

    If you specify nil for the target object, the control searches the responder chain for an object that defines the specified action method.

    觸摸事件優先級

    當原本關系已經錯綜復雜的UIGestureRecognizer和UIResponder之間又冒出一個UIControl,又會摩擦出什么樣的火花呢?

    In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer.This applies only to gesture recognition that overlaps the default action for a control, which includes:

    A single finger single tap on a UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.

    A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.

    A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.

    簡單理解:UIControl會阻止父視圖上的手勢識別器行為,也就是UIControl處理事件的優先級比UIGestureRecognizer高,但前提是相比于父視圖上的手勢識別器。

    UIControl測試場景

    預置場景:在BlueView上添加一個button,同時給button添加一個target-action事件。

    • 示例一:在BlueView上添加點擊手勢識別器

    • 示例二:在button上添加手勢識別器

    操作方式:單擊button

    測試結果:示例一中,button的target-action響應了單擊事件;示例二中,BlueView上的手勢識別器響應了事件。過程日志打印如下:

    //示例一 -[CLTapGestureRecognizer?touchesBegan:withEvent:] -[CLButton?touchesBegan:withEvent:] -[CLButton?beginTrackingWithTouch:withEvent:] -[CLTapGestureRecognizer?touchesEnded:withEvent:]?after?called?state?=?5 -[CLButton?touchesEnded:withEvent:] -[CLButton?endTrackingWithTouch:withEvent:]

    按鈕點擊

    //示例二 -[CLTapGestureRecognizer?touchesBegan:withEvent:] -[CLButton?touchesBegan:withEvent:] -[CLButton?beginTrackingWithTouch:withEvent:] -[CLTapGestureRecognizer?touchesEnded:withEvent:]?after?called?state?=?3 手勢觸發 -[CLButton?touchesCancelled:withEvent:] -[CLButton?cancelTrackingWithEvent:]

    原因分析:點擊button后,事件先傳遞給手勢識別器,再傳遞給作為hit-tested view存在的button(UIControl本身也是UIResponder,這一過程和普通事件響應者無異)。示例一中,由于button阻止了父視圖BlueView中的手勢識別器的識別,導致手勢識別器識別失敗(狀態為failed 枚舉值為5),button完全接手了事件的響應權,事件最終由button響應;示例二中,button未阻止其本身綁定的手勢識別器的識別,因此手勢識別器先識別手勢并識別成功(狀態為ended 枚舉值為3),而后通知Application取消響應鏈對事件的響應,因為 touchesCancelled 被調用,同時 cancelTrackingWithEvent 跟著調用,因此button的target-action得不到執行。

    其他:經測試,若示例一中的手勢識別器設置 cancelsTouchesInView 為NO,手勢識別器和button都能響應事件。也就是說這種情況下,button不會阻止父視圖中手勢識別器的識別。

    結論:UIControl比其父視圖上的手勢識別器具有更高的事件響應優先級。

    TODO:

    上述過程中,手勢識別器在執行touchesEnded時是根據什么將狀態置為ended還是failed的?即根據什么判斷應當識別成功還是識別失敗?

    糾正

    以上所述UIControl的響應優先級比手勢識別器高的說法不準確,準確地說只適用于系統提供的有默認action操作的UIControl,例如UIbutton、UISwitch等的單擊,而對于自定義的UIControl,經驗證,響應優先級比手勢識別器低。讀者可自行驗證,感謝 @閆仕偉 同學的糾正。

    撥云見日

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

    我肥來了!

    先看現象二,短按 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對象維護的手勢識別器數組:

    ScrollView延遲發送事件

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

    @interface?UIScrollViewDelayedTouchesBeganGestureRecognizer?:?UIGestureRecognizer?{UIView*?_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: 調用時的信息:

    延遲的本質

    可以看到這個手勢識別器的 _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左右才接收到觸摸事件的。

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

    總結

    • 觸摸發生時,系統內核生成觸摸事件,先由IOKit處理封裝成IOHIDEvent對象,通過IPC傳遞給系統進程SpringBoard,而后再傳遞給前臺APP處理。

    • 事件傳遞到APP內部時被封裝成開發者可見的UIEvent對象,先經過hit-testing尋找第一響應者,而后由Window對象將事件傳遞給hit-tested view,并開始在響應鏈上的傳遞。

    • UIRespnder、UIGestureRecognizer、UIControl,籠統地講,事件響應優先級依次遞增。

    參考資料

  • 史上最詳細的iOS之事件的傳遞和響應機制-原理篇

  • Understanding Event Handling, Responders, and the Responder Chain

  • iOS觸摸事件的流動

  • UIKit: UIControl

  • 總結

    以上是生活随笔為你收集整理的iOS触摸屏幕后发生了什么的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    999国产在线 | 成人午夜电影在线播放 | 国产亚洲高清视频 | 中文字幕最新精品 | 亚洲综合一区二区精品导航 | 人人爽人人爽av | 久久久久久久久久电影 | 日韩视频 一区 | 日韩亚洲在线 | 在线观看视频黄色 | 在线精品观看 | 久久看毛片| 欧美特一级 | 在线国产一区二区 | 久久国产精品久久久 | 国产精品免费大片视频 | 亚洲精品久久久久久久不卡四虎 | 丁香影院在线 | 午夜电影 电影 | 久久久亚洲精华液 | 日本中文字幕久久 | 久久国产精品二国产精品中国洋人 | 国产成人精品综合久久久 | 欧美一级大片在线观看 | 亚洲欧洲美洲av | 亚洲日日夜夜 | 99久久精品免费看国产四区 | 国产伦精品一区二区三区四区视频 | 日日精品 | 日韩三级免费 | 九九久久久久久久久激情 | 中文字幕精品一区二区三区电影 | 日本中文字幕在线播放 | 色婷婷97 | 亚洲欧美精品一区二区 | 婷婷五月色综合 | 在线观看国产永久免费视频 | 视频一区二区在线观看 | 国产美女视频免费观看的网站 | 久久人人爽人人片av | 很污的网站 | 干亚洲少妇 | 天天干夜夜夜操天 | 国产剧情一区在线 | 69国产盗摄一区二区三区五区 | 在线免费观看羞羞视频 | 久久一久久 | 在线欧美小视频 | 麻豆91在线看 | 91精品国产综合久久福利 | 一本一本久久a久久精品牛牛影视 | 中文字幕一区三区 | 国产亚洲精品久久久久动 | 日日夜夜精品视频天天综合网 | 国产在线播放一区二区 | 五月婷婷视频在线观看 | 国产日产亚洲精华av | 日本美女xx | 日韩激情小视频 | 国产精品二区三区 | av视屏在线播放 | 国产精品免费视频网站 | 97国产在线观看 | 99精品在线免费在线观看 | 91av在线视频免费观看 | 色99在线| 天天操天天舔天天爽 | 国产一区不卡在线 | 91精品国自产在线偷拍蜜桃 | 91探花在线视频 | 中国一级片在线观看 | 日韩丝袜视频 | 亚洲欧美视频在线观看 | 97超碰资源总站 | 欧美日韩18 | 久草久草久草久草 | 九九99视频 | 亚洲精品播放 | 这里有精品在线视频 | 国产成人黄色在线 | 国内精品久久久久 | 五月天婷婷视频 | 久久99国产精品 | 国产综合久久 | 国产亚洲精品久久久久秋 | 91在线播放综合 | 国产精品久久 | 美女久久久久久久 | 精品在线一区二区 | 免费观看黄色12片一级视频 | 亚洲少妇自拍 | 久草视频免费在线观看 | 久久久久久不卡 | 国产欧美精品一区二区三区 | 久草视频中文在线 | 四虎影视成人永久免费观看亚洲欧美 | 高清av中文字幕 | 狠狠色狠狠色综合日日92 | 久久爱www.| 国产精品久久久精品 | 中文字幕在线观看三区 | 久久久久久久久久久久国产精品 | 亚洲美女视频在线观看 | 一区二区视频在线看 | 精品久久久久一区二区国产 | 免费观看一级特黄欧美大片 | 91夫妻视频 | 国产精品久久99精品毛片三a | 国产一区不卡在线 | 中文字幕在线观看国产 | 91网址在线看 | 韩日成人av | 精品久久久久免费极品大片 | 日本二区三区在线 | 99久久99久久 | 国产综合在线观看视频 | 500部大龄熟乱视频 欧美日本三级 | 婷婷丁香色综合狠狠色 | 韩国精品在线观看 | 特级毛片在线免费观看 | 成人av在线影视 | av国产网站 | 国产成人三级在线 | 国产伦精品一区二区三区高清 | 久久国语 | 精品影院一区二区久久久 | 国产91全国探花系列在线播放 | 中文字幕在线观看一区二区 | 国内精品亚洲 | 国产无吗一区二区三区在线欢 | 丁香婷婷综合激情五月色 | 日韩a在线播放 | 天堂av免费在线 | 亚洲精品美女在线观看 | 亚洲三级视频 | 美女久久久久久久久久 | 色视频在线免费 | 好看av在线| 午夜精品一二三区 | 亚州精品国产 | 精品一区二区三区久久久 | 九九九视频在线 | 国内精品久久久久国产 | 99久久久久国产精品免费 | 国产精品视频大全 | 精品在线视频一区 | 欧美日韩久久不卡 | 欧美亚洲国产精品久久高清浪潮 | 97超碰人人澡人人爱学生 | 国产在线观看国语版免费 | 91插插插免费视频 | 日韩欧美一区二区在线观看 | 99爱这里只有精品 | 99热.com| av888av.com| 久草视频在线资源 | 国产精品久久久久久久免费 | www.天天草 | 毛片网站在线看 | 久久久免费少妇 | 免费观看一级特黄欧美大片 | 天堂中文在线播放 | 亚洲精品66 | 日韩电影在线视频 | 国产日韩精品在线观看 | 国产品久精国精产拍 | 国产专区在线播放 | 国产一级黄 | 在线观看黄色的网站 | 久久激情五月激情 | 成人免费网视频 | 在线免费国产视频 | 久草在线久草在线2 | 五月天色中色 | 国产96视频| 日韩高清一二区 | 亚洲精品在线观看的 | 三日本三级少妇三级99 | 国产精品久久一区二区三区, | 欧美日韩在线视频免费 | 中国一级片在线 | 日韩欧美一区二区三区在线观看 | 免费看黄网站在线 | 免费av一级电影 | 亚洲精品在线观看中文字幕 | 亚州天堂 | 欧美一级黄色网 | 久久69精品久久久久久久电影好 | 免费看黄网站在线 | 国产亚洲精品久久久久久电影 | 91av视频在线观看 | 欧美精品在线观看一区 | 99久免费精品视频在线观看 | 九九热精品视频在线播放 | 亚洲五月激情 | 国产精品久久久久影院 | 天天躁日日躁狠狠躁av中文 | 99久热在线精品视频观看 | 午夜视频免费播放 | 国产精品一区欧美 | 中文字幕欧美激情 | 五月婷婷在线观看视频 | 日日夜操| 美女视频黄色免费 | 中文字幕色网站 | 91在线视频播放 | 日本中文字幕免费观看 | 国产精品国内免费一区二区三区 | 国产亚洲激情视频在线 | 亚洲一区二区精品在线 | 国产福利免费在线观看 | 天天插天天爱 | 在线精品国产 | 亚洲综合色播 | 夜夜躁狠狠躁日日躁视频黑人 | 日韩中文字幕免费在线观看 | 久久久麻豆精品一区二区 | 国产精品成人国产乱一区 | 精品毛片一区二区免费看 | 中文亚洲欧美日韩 | 精品久久影院 | 嫩嫩影院理论片 | 久久久久久久99精品免费观看 | 五月婷婷久草 | 超碰在线亚洲 | 99精彩视频在线观看免费 | 一区二区视频免费在线观看 | 九九免费在线视频 | 亚洲成人av影片 | 国产高清视频在线 | 亚洲最新视频在线 | 九色精品在线 | 免费观看国产精品视频 | 欧美激情精品久久久久 | 激情六月婷婷久久 | 黄色在线免费观看网址 | 日韩欧美在线观看一区二区 | 亚洲婷婷在线视频 | 九色精品 | 丝袜制服天堂 | 国产黄色片免费看 | 久久久久国产视频 | 99精品偷拍视频一区二区三区 | 1024在线看片| 97电院网手机版 | 伊香蕉大综综综合久久啪 | 日本精a在线观看 | www在线观看国产 | 福利视频第一页 | 黄色高清视频在线观看 | 日韩高清免费在线观看 | 午夜视频在线观看一区二区三区 | 日本久久久久久久久久久 | 91麻豆国产福利在线观看 | 欧美日韩激情视频8区 | 日本三级香港三级人妇99 | 久久综合亚洲鲁鲁五月久久 | 五月综合色婷婷 | 天天干,天天射,天天操,天天摸 | 精品久久久久久久久久久久 | 国产又粗又猛又色 | 成人性生交大片免费观看网站 | a级国产乱理论片在线观看 特级毛片在线观看 | 黄色不卡av | 亚洲在线 | 激情五月开心 | 日韩特黄一级欧美毛片特黄 | 中文久草 | 亚洲最大av网 | 在线不卡中文字幕播放 | 99精品久久久久久久久久综合 | 久久夜视频 | 欧美日韩中文在线 | 免费黄色在线网址 | 久久久国产精品麻豆 | 麻豆91小视频 | 久久久一本精品99久久精品66 | 国产一区二区高清 | 久久人人爽人人爽人人片av免费 | 国产精品福利无圣光在线一区 | 欧美日韩国产网站 | 在线观看免费高清视频大全追剧 | 夜夜操狠狠操 | 欧美日韩视频在线观看免费 | 色偷偷97| 黄色高清视频在线观看 | 欧美久草在线 | 91精品国产九九九久久久亚洲 | 在线91视频 | 亚洲免费精彩视频 | 国产伦理一区二区三区 | 麻豆成人精品 | 99视频在线看 | 国产精品一区二区三区在线看 | av成人在线网站 | 久久久免费高清视频 | 丝袜av一区 | 亚洲高清久久久 | 日韩,中文字幕 | 在线观看成人小视频 | 97成人精品视频在线观看 | 色婷婷国产精品一区在线观看 | 久久久久久毛片 | 成人国产精品久久久久久亚洲 | 亚洲人在线视频 | 人人超碰免费 | 4hu视频 | 久久精品一区二区三区视频 | 久草在线视频首页 | 玖玖视频 | 亚洲三级网站 | 精品国产欧美一区二区 | 国产黄色在线看 | www.久久99| 国产精品久久久久亚洲影视 | 天天操月月操 | 爱射综合| 国产1级毛片 | 狠狠狠操 | 精品国产乱码久久 | 91九色成人蝌蚪首页 | 国产理论一区二区三区 | 国产精品第10页 | 亚洲国产午夜视频 | 色综合久久88色综合天天6 | 久亚洲 | 欧美一性一交一乱 | 99re在线视频观看 | 一区二区不卡视频在线观看 | av夜夜操 | 国产色拍拍拍拍在线精品 | 91亚洲精品久久久蜜桃 | 日韩视频免费 | 欧美日韩一区二区免费在线观看 | 精品久久精品 | 一级特黄aaa大片在线观看 | 久久91久久久久麻豆精品 | 激情综合亚洲 | 91爱看片 | 992tv又爽又黄的免费视频 | 999久久| 97精品国产97久久久久久免费 | 国产精品亚洲综合久久 | 99re中文字幕 | 亚洲精品高清在线 | 亚洲乱亚洲乱亚洲 | 日韩二区三区在线观看 | 成人免费视频网站在线观看 | 欧美激情视频免费看 | 超碰在线最新 | 又爽又黄又无遮挡网站动态图 | 成人av免费在线观看 | 久久性生活片 | 国产精品午夜免费福利视频 | av在线永久免费观看 | 亚洲综合视频在线播放 | 99热九九这里只有精品10 | 婷婷六月网 | 天天透天天插 | 日韩中文字幕免费视频 | 九九免费精品视频在线观看 | 天天操天天爱天天干 | 日本久久久久久久久久 | 日韩在线观看不卡 | 九九九九精品九九九九 | 午夜三级大片 | 国产国语在线 | 国产精品美女久久久网av | 久久综合久久鬼 | 久久艹欧美 | 精品国产乱码久久久久久天美 | 中文字幕一区二区三区久久蜜桃 | 色婷婷在线播放 | 一区三区视频 | 韩国av电影在线观看 | 日韩一二区在线 | 国产精品久久久久影视 | 午夜视频在线观看网站 | 亚洲精品中文字幕在线观看 | 999久久久久久久久久久 | 久久久精品国产免费观看同学 | 久久久久国产精品一区二区 | 男女激情网址 | 久久视频 | 亚洲精品国产精品国自 | 91久久国产自产拍夜夜嗨 | 精品国产一区二区三区久久久蜜臀 | 国产精品自产拍在线观看 | 国内外激情视频 | 中文字幕一区在线 | 最新高清无码专区 | 99精品免费网 | 草樱av | 久久国产精品精品国产色婷婷 | 亚洲精品视频免费 | 国产精品日韩在线播放 | 九九热有精品 | 亚洲一区二区黄色 | 久久调教视频 | 亚洲在线视频网站 | 成人欧美一区二区三区在线观看 | 天天干,天天射,天天操,天天摸 | 午夜精品一区二区三区在线观看 | 国产精品一区免费观看 | 亚洲片在线| 欧美小视频在线观看 | 国产乱码精品一区二区蜜臀 | 久久99精品国产一区二区三区 | 久久 国产一区 | 久久精品国产免费看久久精品 | 超碰夜夜 | 精品国产欧美一区二区三区不卡 | 91传媒91久久久 | 国产精品久久久久一区二区 | 国产成人精品一区二区在线观看 | 欧美日韩视频免费看 | 在线中文字幕视频 | 天天爽夜夜爽人人爽一区二区 | 操操爽| 狠狠操狠狠插 | 国产不卡毛片 | 成人久久久久久久久久 | 99久久婷婷 | 九九九九免费视频 | 黄网站免费看 | 免费黄色a网站 | 在线观看免费av片 | 毛片无卡免费无播放器 | 激情综合网色播五月 | 久热超碰 | 日b视频国产 | 亚洲午夜精品福利 | 国产二区av| 欧美性天天 | 91免费观看视频网站 | 玖玖综合网 | 日韩欧美一区二区在线 | 欧美日韩精品二区第二页 | 成人午夜在线电影 | 久久黄页| 久久精品久久精品久久39 | 在线观看久久 | 91精品1区2区 | 美女免费视频网站 | 久久久久久影视 | 在线观看中文字幕网站 | 成人av资源站 | 福利视频 | 欧美成人精品欧美一级乱 | 免费h视频 | 国产午夜不卡 | 可以免费观看的av片 | 在线国产小视频 | 久久婷婷精品 | 一区二区激情视频 | 欧美激情视频一区二区三区免费 | 中文字幕一区二区三区乱码不卡 | 99热 精品在线 | 日韩免费电影在线观看 | 精品久久久久久久久久久院品网 | 六月丁香综合网 | 午夜精品久久久久久99热明星 | 亚洲成人中文在线 | 午夜视频在线观看欧美 | 国产精品视频一二三 | 91精选在线观看 | 人人插人人 | 久草在线国产 | 久久久一本精品99久久精品 | 国产日韩欧美精品在线观看 | 日韩欧美视频一区二区 | 国产精品手机播放 | 国产五月婷 | 欧美一区二区在线 | 国产一级特黄毛片在线毛片 | 国产精品精品国产婷婷这里av | 欧美日韩不卡一区二区三区 | 日韩r级在线 | 日韩欧美国产免费播放 | 国产成人精品久久久久 | 一级片免费在线 | 91电影福利 | 91在线免费公开视频 | 最近字幕在线观看第一季 | 精品国偷自产在线 | 成人sm另类专区 | ww视频在线观看 | 日韩黄色在线电影 | 麻豆精品国产传媒 | 亚洲欧洲成人精品av97 | 精品毛片一区二区免费看 | 久草在线观看 | 一级黄色免费网站 | 国产高清永久免费 | 久久久久国产一区二区三区 | 日韩视频中文字幕在线观看 | 91精品在线视频观看 | 五月婷婷视频在线 | 黄色av免费电影 | 日本中文一级片 | 精品国产一区二区三区久久久蜜臀 | 91网址在线观看 | 亚洲成av片人久久久 | 久久 地址 | 日本精品久久久久中文字幕 | 久久黄色网址 | 婷婷激情五月综合 | 激情丁香久久 | 色永久免费视频 | 97视频免费在线观看 | 日韩午夜电影 | 青草视频网 | 爱色av.com| 毛片播放网站 | 欧美一区二区三区免费观看 | 国产高清日韩欧美 | 日韩午夜电影院 | 综合国产视频 | 视频三区 | 黄色片免费电影 | 日韩精品影视 | 色a在线观看 | 日韩欧美高清一区二区三区 | 中文字幕不卡在线88 | 日韩欧美99| 激情婷婷色 | 国产激情久久久 | 欧美 日韩 性 | 日韩免费在线一区 | 成人av电影免费在线播放 | 亚洲第一区精品 | 成人午夜精品久久久久久久3d | 国产精品精品久久久久久 | 婷婷色网视频在线播放 | 午夜av影院 | 免费麻豆 | 五月婷av| www.av免费 | 亚洲精品白浆高清久久久久久 | 成人黄色电影在线播放 | 天天色天天艹 | 91视频免费网站 | 在线观看 国产 | 免费a网 | 欧美另类xxx | 成人黄色片免费看 | 麻豆va一区二区三区久久浪 | 天天婷婷 | 91精品对白一区国产伦 | 精品主播网红福利资源观看 | www免费看片com | 亚洲jizzjizz日本少妇 | 99色在线视频 | 成人黄色电影在线观看 | 二区三区在线观看 | 久草视频免费在线观看 | 日韩欧美一区二区三区在线 | 亚洲一区黄色 | 99re视频在线观看 | 91桃色在线观看视频 | 亚洲永久精品视频 | www.五月天婷婷 | 天天操夜夜操夜夜操 | 91精品国产综合久久福利不卡 | 天天艹日日干 | 免费一级特黄毛大片 | 久久99久国产精品黄毛片入口 | 国产又粗又猛又色 | 91精品国产欧美一区二区成人 | 综合久久一本 | 亚洲精品视频在线观看视频 | 99久高清在线观看视频99精品热在线观看视频 | 男女免费视频观看 | 青青草国产免费 | av在线免费在线观看 | 成年人三级网站 | 精品黄色在线观看 | 日本大尺码专区mv | 久久久夜色| 激情五月av| 免费av在线| av成人免费 | 国产精品刺激对白麻豆99 | 正在播放国产一区 | 成人在线免费小视频 | 亚洲免费av一区二区 | 二区三区av| 91视频麻豆 | 激情综合亚洲 | www.黄色网.com | 不卡的av| 天天做天天爱天天综合网 | 日韩一区二区免费在线观看 | 九九久久成人 | 国产精品成人a免费观看 | 99热九九这里只有精品10 | 高潮毛片无遮挡高清免费 | 悠悠av资源片 | 探花视频免费在线观看 | 成人小电影在线看 | av电影中文字幕在线观看 | 亚洲精品免费视频 | 日本不卡一区二区 | 亚洲va欧美va人人爽春色影视 | 精品国产乱码久久久久久久 | 国产国产人免费人成免费视频 | 香蕉在线影院 | 五月婷婷一级片 | 亚洲一区精品人人爽人人躁 | 精品久久久久久国产偷窥 | www.五月婷 | 久久视频在线观看 | 808电影| 蜜臀av性久久久久蜜臀av | 麻豆视屏 | 色综合久久久久综合 | 久久免费精品一区二区三区 | 高清免费av在线 | 中文字幕国产精品 | 国产一区 在线播放 | 91| 亚洲日本欧美 | 亚洲精品小区久久久久久 | 日本黄色免费大片 | 五月天综合婷婷 | 91精品综合| www.五月天 | 亚洲国产影院 | 天天舔天天射天天操 | 99爱在线| 国产中文字幕视频在线观看 | 午夜视频亚洲 | 国内外成人免费在线视频 | 欧美网址在线观看 | 五月婷激情| 久久国产麻豆 | 婷婷深爱五月 | 国产大片黄色 | 亚洲视频一区二区三区在线观看 | 午夜精品一区二区三区在线 | 98久9在线 | 免费 | 97电影手机版 | 色偷偷人人澡久久超碰69 | 精品在线播放视频 | 日韩精品一区二区久久 | 国产一级片视频 | 亚洲 欧美 91| 日韩在线视频网站 | 精品免费在线视频 | 日韩久久网站 | 精品91视频| 亚洲精品成人av在线 | 精品国产伦一区二区三区 | 白丝av免费观看 | 综合黄色网 | 国产91在线观 | 国产尤物在线视频 | 免费观看版| 欧美精品乱码久久久久久按摩 | 精品视频123区在线观看 | 久久免费国产精品1 | 久久精品人人做人人综合老师 | 国产精品中文字幕在线播放 | 日本老少交 | 福利av影院 | 一区二区精品在线视频 | 国产精品欧美久久久久天天影视 | 日韩精品短视频 | 精品国产一区二区三区日日嗨 | 97成人精品视频在线观看 | 91视频观看免费 | 国产玖玖在线 | 亚洲不卡123 | 国产成人精品av在线观 | 成人在线免费看视频 | av播放在线| 91久久精品一区二区三区 | 欧美精品久久久久久久久免 | 天堂va在线观看 | 日韩av网页 | 国产v在线观看 | 午夜av免费 | 亚洲欧美日韩精品一区二区 | 欧美一级久久久久 | 日本性高潮视频 | 日韩免费视频在线观看 | 成年人免费在线观看网站 | 日韩av在线资源 | 久久人人精 | 四虎国产精 | 日本高清中文字幕有码在线 | 久久久免费播放 | 久久免费看av | 久草爱视频 | 最新午夜电影 | av网在线观看 | 色99中文字幕 | 日韩成人免费观看 | 黄色免费在线视频 | 国产视频久久 | 天天噜天天色 | 人人干人人做 | 韩国三级在线一区 | 97色免费视频 | 国产黄色精品在线 | 中文字幕免费一区二区 | 97色婷婷成人综合在线观看 | 精品特级毛片 | 日韩av影片在线观看 | 国产女做a爱免费视频 | 奇米网网址 | 免费在线观看的av网站 | 国产婷婷| 波多野结衣电影一区二区 | 日韩黄色免费电影 | 色婷av| 亚洲人成人99网站 | 欧美日本啪啪无遮挡网站 | 五月婷婷中文 | 欧美激情在线网站 | 久久亚洲成人网 | 国产精品一区二区电影 | 国产精品无| 国产在线观看一 | 亚洲视频免费在线 | 欧美日韩高清一区二区 国产亚洲免费看 | 国产无限资源在线观看 | 美女久久视频 | 国产一二三区在线观看 | 五月天色网站 | 亚洲黄色免费观看 | 天天色天天射天天干 | 亚洲精品动漫在线 | av免费成人| 人人爱爱人人 | 97手机电影网| 欧美性视频网站 | 国产一区在线观看免费 | 超碰在线97国产 | 欧美久久久久 | www.五月天| 中文字幕中文字幕在线一区 | 色偷偷av男人天堂 | 九九久久国产 | 在线免费观看视频你懂的 | 色偷偷男人的天堂av | 日韩av资源在线观看 | 992tv又爽又黄的免费视频 | 麻豆视频在线免费观看 | av在线观 | 日韩手机在线观看 | 美女视频久久黄 | 国产色在线视频 | 日韩乱码在线 | 亚洲欧美视屏 | 韩日精品视频 | 日韩av快播电影网 | 伊人日日干 | 欧美男同网站 | 黄色免费大片 | 最新精品国产 | 久久热亚洲 | 久久久久日本精品一区二区三区 | 国产主播大尺度精品福利免费 | 成人免费网站视频 | 日韩精品久久一区二区三区 | 97精品国自产拍在线观看 | 一区二区三区在线看 | 伊人影院在线观看 | 久久99久久99精品免观看粉嫩 | av成人动漫在线观看 | 国产美女精品久久久 | 日韩网站免费观看 | 一级成人免费视频 | 精品国产精品一区二区夜夜嗨 | 色五婷婷 | 97超碰国产在线 | 日日操日日操 | 又黄又爽又刺激视频 | 99在线免费观看视频 | 天天爽人人爽夜夜爽 | 国产精华国产精品 | 日韩毛片久久久 | 亚洲精品视频第一页 | 国产69精品久久久久久久久久 | 四虎国产精品成人免费影视 | 草莓视频在线观看免费观看 | 亚洲精品大全 | 精品99免费 | 不卡av在线免费观看 | 亚洲国产视频直播 | 人人狠狠综合久久亚洲 | 精品中文字幕在线播放 | 日韩欧美国产精品 | 久久久精品国产一区二区三区 | 国产一区精品在线 | 日韩av不卡在线观看 | 久久久国产网站 | 亚洲免费不卡 | 激情av网 | 国产精品久免费的黄网站 | 久久精品国产一区 | 成人av片免费观看app下载 | 男女啪啪视屏 | 不卡的av在线播放 | 深夜免费福利视频 | av免费看电影 | 国产精品免费成人 | 成人久久电影 | 日韩在线免费不卡 | 91福利小视频 | 超碰国产在线播放 | 九九精品视频在线观看 | 国产在线国产 | 又黄又爽的免费高潮视频 | 国产一级二级av | 国产精品v欧美精品v日韩 | 中文字幕免费播放 | 97在线看片 | 在线观看国产中文字幕 | 久久免费观看少妇a级毛片 久久久久成人免费 | 免费看一级一片 | 91人人爱 | 亚洲国产精品电影 | 超碰在线97观看 | 在线中文字幕一区二区 | 亚洲污视频| 国产精品福利在线 | 黄色av成人在线观看 | 综合伊人av| 国产精品免费观看久久 | 亚洲jizzjizz日本少妇 | 久草国产在线 | 久久国产精品免费视频 | 天天天天色射综合 | 久青草视频在线观看 | 69精品视频在线观看 | 久久久久综合网 | 国产高清第一页 | 国产男女无遮挡猛进猛出在线观看 | 91高清完整版在线观看 | 国产女人免费看a级丨片 | 亚洲精品小视频在线观看 | 国产一区在线免费观看 | 日韩一二区在线观看 | 日日摸日日 | 久一久久| 久久久久久久免费 | 成年人免费观看在线视频 | 91探花系列在线播放 | 国产在线精品一区二区不卡了 | 免费福利片2019潦草影视午夜 | 久久国产精品视频观看 | 精品亚洲一区二区 | 青青草久草在线 | 久久99精品一区二区三区三区 | 亚洲综合色视频 | 伊人久久在线观看 | 亚洲国产网站 | 久久久久福利视频 | 99精品在线观看视频 | 亚洲精品在线国产 | 欧美少妇18p | 久草精品在线 | 中国一级片在线播放 | 一区二区三区高清在线 | 国产亚洲精品久久久网站好莱 | 精品五月天 | www.夜色321.com| 国产精品久久久久影视 | 国产精品18久久久久久不卡孕妇 | 天天艹天天 | 91社区国产高清 | 黄色avwww | 国产黄色高清 | 亚洲理论片在线观看 | 91看片麻豆 | 国产中文在线视频 | 国产精品久久久久久久7电影 | 亚州人成在线播放 | 国产三级精品在线 | 91探花国产综合在线精品 | 永久免费毛片 | 欧美在线观看视频一区二区三区 | 在线中文字幕av观看 | 夜夜操天天操 | 色婷婷久久一区二区 | 婷婷色六月天 | 免费观看丰满少妇做爰 | 中文字幕 在线看 | 成人午夜剧场在线观看 | 国产一区二区久久精品 | 西西人体4444www高清视频 | 黄在线免费看 | 日韩av午夜| 日本三级在线观看中文字 | 日韩理论 | 亚洲天堂自拍视频 | 精品国产一区二区三区免费 | 91完整版 | 天天爽夜夜爽人人爽一区二区 | 亚洲精品66 | 亚洲一区精品人人爽人人躁 | a√天堂中文在线 | 丁香婷婷在线 | 色婷婷久久久综合中文字幕 | 九九九九精品九九九九 | 久久九九久久九九 | 国产色一区 | 天天射天天射天天射 | 欧洲精品视频一区二区 | 91麻豆国产| 亚洲综合导航 | 亚洲首页| 亚洲精品国产精品久久99热 | 91精选在线| 九色精品 | 在线免费黄网站 | 99热最新地址 | 欧美日韩国产一区二区三区在线观看 | 久久免费99 | 美女在线观看网站 | 国产拍揄自揄精品视频麻豆 | 在线观看一区 | 97超碰精品 | 精品久久中文 | 黄在线 | 日本三级久久 | 亚洲国产日韩av | 丝袜美腿av | 特级免费毛片 | 美女视频黄免费的 | av大全免费在线观看 | 少妇视频在线播放 | 99视频一区二区 | 成年人视频在线免费播放 | 天干啦夜天干天干在线线 | 91九色视频观看 | 99视频免费播放 | 97超在线| 国产视频网站在线观看 | 久久久精品在线观看 | 在线岛国av | 夜又临在线观看 | 四虎影视国产精品免费久久 | 国产精品国内免费一区二区三区 | 亚洲不卡在线 | 91人人网| 四虎免费在线观看视频 | 国产区精品在线观看 | 九九视频这里只有精品 | 亚洲国产精品久久久 | 成人在线视频一区 | 国产精品九九热 | 麻豆传媒在线免费看 | 久久久香蕉视频 | 99资源网| 久久a国产 | 日韩亚洲国产中文字幕 | 一区二区三区污 | 黄色在线成人 | 婷婷丁香国产 | 免费亚洲成人 | 色欧美日韩 | 国产99久久99热这里精品5 | 午夜视频在线观看一区 | 热久久视久久精品18亚洲精品 | 日韩在线第一区 | 国产一区二区久久精品 | 久草在线中文888 | 视频国产区 | 免费一级特黄毛大片 | 久久综合久久综合这里只有精品 | 91成年人在线观看 | 亚洲春色奇米影视 | 五月天婷婷丁香花 | 日日日操操| 热久久视久久精品18亚洲精品 | www.五月婷婷| 韩国精品一区二区三区六区色诱 | av在线影视| 日日夜色 | 99视频精品免费视频 | 国产成人精品免费在线观看 | 亚洲欧美成人网 | 亚洲精品美女 | 国产啊v在线 | 国产探花 | .国产精品成人自产拍在线观看6 | 九九免费观看视频 | 国产视频资源 | 九色在线 | 韩国av免费观看 | 久久激情综合 | 久久久影院官网 | 2023亚洲精品国偷拍自产在线 | 亚洲综合在线观看视频 | 久久这里只有精品23 |