深入淺出iOS事件機(jī)制

原文地址:http://zhoon.github.io/ios/2015/04/12/ios-event.html

本文章將講解有關(guān)iOS事件的傳遞機(jī)制,如有錯(cuò)誤或者不同的見(jiàn)解,歡迎留言指出。

iOS的事件有好幾種:Touch Events(觸摸事件)、Motion Events(運(yùn)動(dòng)事件,比如重力感應(yīng)和搖一搖等)、Remote Events(遠(yuǎn)程事件,比如用耳機(jī)上得按鍵來(lái)控制手機(jī)),其中最常用的應(yīng)該就是Touch Events了,基本存在于每個(gè)app的每個(gè)地方,今天我們主要就講講它,至于其他兩個(gè)事件有興趣的可以自行查閱資料。

在網(wǎng)頁(yè)上當(dāng)我們講到事件,我們會(huì)講到事件響應(yīng)鏈,我們會(huì)講到事件的響應(yīng)者和事件的傳遞方式(冒泡),那么在app上,其實(shí)也離不開(kāi)這幾個(gè)問(wèn)題,今天我們也重這幾個(gè)方面來(lái)介紹iOS的事件機(jī)制: 1、響應(yīng)鏈?zhǔn)鞘裁磿r(shí)候怎樣構(gòu)建的? 2、事件第一個(gè)響應(yīng)者是怎么確定的? 3、事件第一個(gè)響應(yīng)者確定后,系統(tǒng)是怎樣傳遞事件的?

響應(yīng)鏈的構(gòu)建

無(wú)論是哪種事件,其傳遞和響應(yīng)都與響應(yīng)鏈息息相關(guān),那么響應(yīng)鏈到底是一個(gè)什么樣的東西呢? 在UIKit中有一個(gè)類:UIResponder,我們可以看看頭文件的幾個(gè)屬性和方法:

UIResponder是所有可以響應(yīng)事件的類的基類(從名字應(yīng)該就可以看出來(lái)了),其中包括最常見(jiàn)的UIView和UIViewController甚至是UIApplication,所以我們的UIView和UIViewController都是作為響應(yīng)事件的載體。

那么響應(yīng)鏈跟這個(gè)UIResponder有什么關(guān)系呢?事實(shí)事件響應(yīng)鏈的形成和事件的響應(yīng)和傳遞,UIResponder都幫我們做了很多事。我們的app中,所有的視圖都是按照一定的結(jié)構(gòu)組織起來(lái)的,即樹(shù)狀層次結(jié)構(gòu),每個(gè)view都有自己的superView,包括controller的topmost view(controller的self.view)。當(dāng)一個(gè)view被add到superView上的時(shí)候,他的nextResponder屬性就會(huì)被指向它的superView,當(dāng)controller被初始化的時(shí)候,self.view(topmost view)的nextResponder會(huì)被指向所在的controller,而controller的nextResponder會(huì)被指向self.view的superView,這樣,整個(gè)app就通過(guò)nextResponder串成了一條鏈,也就是我們所說(shuō)的響應(yīng)鏈。所以響應(yīng)鏈就是一條虛擬的鏈,并沒(méi)有一個(gè)對(duì)象來(lái)專門存儲(chǔ)這樣的一條鏈,而是通過(guò)UIResponder的屬性串連起來(lái)的。如下圖:

Hit-Testing View

文章開(kāi)頭說(shuō)到有iOS三種event類型,事件傳遞中UIWindow會(huì)根據(jù)不同的event,用不同的方式尋找initial object,initial object決定于當(dāng)前的事件類型。比如Touch Event,UIWindow會(huì)首先試著把事件傳遞給事件發(fā)生的那個(gè)view,就是下文要說(shuō)的hit-testview。對(duì)于Motion和Remote Event,UIWindow會(huì)把例如震動(dòng)或者遠(yuǎn)程控制的事件傳遞給當(dāng)前的firstResponder,有關(guān)firstResponder的相關(guān)信息請(qǐng)看這里。下面主要講Touch Event的hit-testview。

有了事件響應(yīng)鏈,接下來(lái)的事情就是尋找響應(yīng)事件的具體響應(yīng)者了,我們稱著為:Hit-Testing View,尋找這個(gè)View的過(guò)程我們稱著為Hit-Test。

那么什么是Hit-Test呢,我們可以把它理解為一個(gè)探測(cè)器,通過(guò)這個(gè)探測(cè)器我們可以找到并判斷手指是否點(diǎn)擊在某個(gè)視圖上面,換句話說(shuō)就是通過(guò)Hit-Test可以找到手指點(diǎn)擊到的處于屏幕最前面的那個(gè)UIView。

在解釋Hit-Test是怎么工作之前,先來(lái)看看它是什么時(shí)候被調(diào)用的。前面說(shuō)Hit-Test是一個(gè)探測(cè)器,那么在代碼里面其實(shí)就是一個(gè)函數(shù),UIView有如下兩個(gè)方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

每當(dāng)手指接觸屏幕,UIApplication接收到手指的事件之后,就會(huì)去調(diào)用UIWindow的hitTest:withEvent:,看看當(dāng)前點(diǎn)擊的點(diǎn)是不是在window內(nèi),如果是則繼續(xù)依次調(diào)用subView的hitTest:withEvent:方法,直到找到最后需要的view。調(diào)用結(jié)束并且hit-test view確定之后,這個(gè)view和view上面依附的手勢(shì),都會(huì)和一個(gè)UITouch的對(duì)象關(guān)聯(lián)起來(lái),這個(gè)UITouch會(huì)作為事件傳遞的參數(shù)之一,我們可以看到UITouch頭文件里面有一個(gè)view和gestureRecognizers的屬性,就是hitTest view和它的手勢(shì)。

現(xiàn)在知道Hit-Test是什么時(shí)候調(diào)用了,那么接下來(lái)看看它是怎么工作的。Hit-Test是采用遞歸的方法從view層級(jí)的根節(jié)點(diǎn)開(kāi)始遍歷,看看下面這張圖:

UIWindow有一個(gè)MianVIew,MainView里面有三個(gè)subView:view A、view B、view C,他們各自有兩個(gè)subView,他們層級(jí)關(guān)系是:view A在最下面,view B中間,view C最上(也就是addSubview的順序,越晚add進(jìn)去越在上面),其中view A和view B有一部分重疊。如果手指在view B.1和view A.2重疊的上面點(diǎn)擊,按照上面說(shuō)的遞歸方式,順序如下圖所示:

遞歸是向界面的根節(jié)點(diǎn)UIWindow發(fā)送hitTest:withEvent:消息開(kāi)始的,從這個(gè)消息返回的是一個(gè)UIView,也就是手指當(dāng)前位置最前面的那個(gè) hittest view。 當(dāng)向UIWindow發(fā)送hitTest:withEvent:消息時(shí),hitTest:withEvent:里面所做的事,就是判斷當(dāng)前的點(diǎn)擊位置是否在window里面,如果在則遍歷window的subview然后依次對(duì)subview發(fā)送hitTest:withEvent:消息(注意這里給subview發(fā)送消息是根據(jù)當(dāng)前subview的index順序,index越大就越先被訪問(wèn))。如果當(dāng)前的point沒(méi)有在view上面,那么這個(gè)view的subview也就不會(huì)被遍歷了。當(dāng)事件遍歷到了view B.1,發(fā)現(xiàn)point在view B.1里面,并且view B.1沒(méi)有subview,那么他就是我們要找的hittest view了,找到之后就會(huì)一路返回直到根節(jié)點(diǎn),而view B之后的view A也不會(huì)被遍歷了。

一圖勝千言:

注意hitTest里面是有判斷當(dāng)前的view是否支持點(diǎn)擊事件,比如userInteractionEnabled、hidden、alpha等屬性,都會(huì)影響一個(gè)view是否可以相應(yīng)事件,如果不響應(yīng)則直接返回nil。 我們留意到還有一個(gè)pointInside:withEvent:方法,這個(gè)方法跟hittest:withEvent:一樣都是UIView的一個(gè)方法,通過(guò)他開(kāi)判斷point是否在view的frame范圍內(nèi)。如果這些條件都滿足了,那么遍歷就可以繼續(xù)往下走了,代碼表現(xiàn)大概如下:

Hit-Test的應(yīng)用

一、擴(kuò)大view的點(diǎn)擊區(qū)域

一個(gè)按鈕尺寸是10pt*10pt,如果要擴(kuò)大按鈕的點(diǎn)擊區(qū)域(按鈕四周之外的10pt也可以響應(yīng)按鈕的事件),可以怎么做呢?或許重寫(xiě)hittest:withEvent:是個(gè)好辦法,hitest就是返回可以響應(yīng)事件的view,如果我們?cè)赽utton的子類里面重寫(xiě)它,在方法里面判斷如果point在button的frame之外的10pt內(nèi),就返回button自己。

二、將事件傳遞給兄弟view

如上面第一個(gè)圖,如果需要是需要view A響應(yīng)事件而不是B(即使點(diǎn)在重疊的部分),什么都不做的話,當(dāng)點(diǎn)擊在重疊的時(shí)候,A是不能響應(yīng)事件的,除非B的userInteractionEnabled為NO并且者B沒(méi)有任何事件的響應(yīng)函數(shù)。這個(gè)時(shí)候通過(guò)重寫(xiě)B(tài)的hittest可以解決這個(gè)問(wèn)題,在B的hittest里面直接返回nil就行了。

三、將事件傳遞給subview

如下圖,藍(lán)色的scrollView設(shè)置pagingEnabled使得image停止?jié)L動(dòng)后都會(huì)固定在居中的位置,如果在scrollView的左邊或者右邊活動(dòng),發(fā)現(xiàn)scrollView是無(wú)法滾動(dòng)的,原因就是hittest里面沒(méi)有滿足pointInSide這個(gè)條件,scrollView的bound只有藍(lán)色的區(qū)域。這個(gè)時(shí)候重寫(xiě)UIView的hittest:withEvent:,然后返回scrollView即可解決問(wèn)題。

事件的傳遞

有了響應(yīng)鏈,并且找到了第一個(gè)響應(yīng)事件的對(duì)象,接下來(lái)就是把事件發(fā)送個(gè)這個(gè)響應(yīng)者了。 UIApplication中有個(gè)sendEvent:的方法,在UIWindow中同樣也可以發(fā)現(xiàn)一個(gè)同樣的方法。UIApplication是通過(guò)這個(gè)方法把事件發(fā)送給UIWindow,然后UIWindow通過(guò)同樣的接口,把事件發(fā)送給hit-testview。這個(gè)我們可以從Time Profiler里面得到證實(shí):

當(dāng)我點(diǎn)擊了WRBuyBookButton之后,UIWindow會(huì)通過(guò)一個(gè)私有方法,在里面會(huì)去調(diào)用按鈕的touchesBegan和touchesEnded方法,touchesBegan里面有設(shè)置按鈕的高亮等之類的動(dòng)作,這樣就實(shí)現(xiàn)了事件的傳遞。而事件的響應(yīng),也就是按鈕上綁定的action,是在touchEnded里面通過(guò)調(diào)用UIApplication的sendAction:to:from:forEvent:方法來(lái)實(shí)現(xiàn)的,至于這個(gè)方法里面是怎么去響應(yīng)action,就只能猜測(cè)了(可能是通過(guò)oc底層消息機(jī)制的相關(guān)接口 objc_msgSend 來(lái)發(fā)送消息實(shí)現(xiàn)的,可以參考message.h文件)。如果第一響應(yīng)者沒(méi)有響應(yīng)這個(gè)事件,那么就會(huì)根據(jù)響應(yīng)鏈,把事件冒泡傳遞給nextResponder來(lái)響應(yīng)。

注意這里是怎么把事件傳遞給nextResponder的呢?拿touch事件來(lái)說(shuō),UIResponder里面touch四個(gè)階段的方法里面,實(shí)際上是什么事都沒(méi)有做的,UIView繼承了它進(jìn)行重寫(xiě),重寫(xiě)的內(nèi)容也是沒(méi)有什么東西,就是把事件傳遞給nextResponder,比如:[self.nextResponder touchesBegan:touches withEvent:event]。所以當(dāng)一個(gè)view或者controller里面沒(méi)有重寫(xiě)touch事件,那么這個(gè)事件就會(huì)一直傳遞下去,直到UIApplication,這也就是事件往上冒泡的原理。如果view重寫(xiě)了touch方法,我們一般會(huì)看到的效果是,這個(gè)view響應(yīng)了事件之后,事件就被截?cái)嗔?就像JavaScript里面調(diào)用e.stopPropagation()),它的nextResponder不會(huì)收到這個(gè)事件,即使重寫(xiě)了nextResponder的touch方法。這個(gè)時(shí)候如果想事件繼續(xù)傳遞下去,可以調(diào)用[super touchesBegan:touches withEvent:event],不建議直接調(diào)[self.nextResponder touchesBegan:touches withEvent:event]。

關(guān)于UIScrollView的事件

先說(shuō)一個(gè)現(xiàn)象,我們平時(shí)加到UIScrollView(或者UITableView和UICollection)上面的UIButton,即使有設(shè)置highLighted的樣式,點(diǎn)擊的時(shí)候卻發(fā)現(xiàn)這個(gè)樣式老是不出來(lái),但是按鈕的事件明明可以響應(yīng)的,很詭異。

后來(lái)才知道,UIScrollView因?yàn)橐獫L動(dòng),所以對(duì)事件做了特殊的處理: 當(dāng)UIScrollView接收到事件之后,會(huì)暫時(shí)劫持當(dāng)前的事件300毫秒,如果300毫秒之后手指還沒(méi)有滾動(dòng),則認(rèn)為你放棄滾動(dòng),放棄對(duì)事件的劫持并往下傳遞,但是從Time Profiler看到此時(shí)按鈕并不是調(diào)用自身的touch方法,而是調(diào)用自身綁定的手勢(shì)的touch事件,由于按鈕的highLighted樣式是寫(xiě)在按鈕的touch方法上的,所以這個(gè)這個(gè)時(shí)候就看不到高亮了。但是長(zhǎng)按按鈕缺可以讓按鈕有高亮的狀態(tài),這個(gè)就不太清楚為什么了,因?yàn)閺腡ime Profiler里面看按鈕的touchesBegan好像還是沒(méi)有被調(diào)。 如果300毫秒之內(nèi)手指滾動(dòng)了,則響應(yīng)滾動(dòng)的事件,事件就不會(huì)繼續(xù)傳給subView了,也就是不會(huì)繼續(xù)調(diào)用按鈕上手勢(shì)的touch方法了。

可以通過(guò)UIScrollView的一個(gè)屬性來(lái)解決這個(gè)問(wèn)題:delaysContentTouches,意思是是否需要延遲處理事件的傳遞,默認(rèn)是NO。把delaysContentTouches設(shè)置為YES之后,一切看起來(lái)挺好的,按鈕終于有高亮樣式了哈哈哈,但是發(fā)現(xiàn)另一個(gè)問(wèn)題:如果手指點(diǎn)擊在按鈕上面并滾動(dòng)UIScrollView,發(fā)現(xiàn)怎么也滾動(dòng)不了。原因是當(dāng)手指點(diǎn)擊UIScrollView并在滾動(dòng)之前,如果subView接收并且可以響應(yīng)事件(delaysContentTouches設(shè)置為YES),則事件響應(yīng)鏈會(huì)在subView響應(yīng)事件之后就截?cái)?,即UIScrollView本身不會(huì)響應(yīng)到此事件,不會(huì)發(fā)生滾動(dòng)??梢栽O(shè)置canCancelContentTouches為YES來(lái)讓UIScrollView可以滾動(dòng),與之類似的還有一個(gè)touchesShouldCancelInContentView:接口,可以根據(jù)參數(shù)view來(lái)更方便的判斷是否需要cancel,如果有需要可以在UIScrollView的子類里面重寫(xiě)這個(gè)接口。

這一塊里面的具體實(shí)現(xiàn)原理我們都不知道,水太深了,只能通過(guò)Time Profiler來(lái)看到一些大概的實(shí)現(xiàn),我們也沒(méi)必要去深究,大方向理解就好了。真的有興趣的同學(xué)也可以去研究研究,期待你的分享。

參考資料:

1、http://smnh.me/hit-testing-in-ios/

2、http://southpeak.github.io/blog/2015/03/07/uiresponder/

3、https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIScrollView_Class/index.html#//apple_ref/doc/uid/TP40006922

4、https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/event_delivery_responder_chain/event_delivery_responder_chain.html

5、https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW2

6、https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文章將講解有關(guān)iOS事件的傳遞機(jī)制,如有錯(cuò)誤或者不同的見(jiàn)解,歡迎留言指出。 iOS的事件有好幾種:Touch E...
    Crazy2015閱讀 325評(píng)論 0 1
  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的?困惑于Cell怎么突然不能點(diǎn)擊了?糾結(jié)于如何實(shí)現(xiàn)這個(gè)奇葩響應(yīng)需求?亦或是...
    Lotheve閱讀 59,626評(píng)論 51 604
  • 在iOS開(kāi)發(fā)中經(jīng)常會(huì)涉及到觸摸事件。本想自己總結(jié)一下,但是遇到了這篇文章,感覺(jué)總結(jié)的已經(jīng)很到位,特此轉(zhuǎn)載。作者:L...
    WQ_UESTC閱讀 6,251評(píng)論 4 26
  • iOS的事件有好幾種:Touch Events(觸摸事件)、Motion Events(運(yùn)動(dòng)事件,比如重力感應(yīng)和搖...
    悟2023閱讀 732評(píng)論 0 1
  • 4事件分發(fā)機(jī)制 iOS中的事件大概分為三種,分別是Milti-Touch Events, Motion Event...
    Kevin_Junbaozi閱讀 917評(píng)論 0 2

友情鏈接更多精彩內(nèi)容