
一、響應(yīng)者鏈(Responder Chain)
先來說說響應(yīng)者對象(Responder Object),顧名思義,指的是有響應(yīng)和處理事件能力的對象。響應(yīng)者鏈就是由一系列的響應(yīng)者對象構(gòu)成的一個(gè)層次結(jié)構(gòu)。
UIResponder是所有響應(yīng)對象的基類,在UIResponder類中定義了處理上述各種事件的接口。我們熟悉的UIApplication、 UIViewController、UIWindow和所有繼承自UIView的UIKit類都直接或間接的繼承自UIResponder,所以它們的實(shí)例都是可以構(gòu)成響應(yīng)者鏈的響應(yīng)者對象。圖一展示了響應(yīng)者鏈的基本構(gòu)成:
1、響應(yīng)者鏈通常是由視圖(UIView)構(gòu)成的;
2、一個(gè)視圖的下一個(gè)響應(yīng)者是它視圖控制器(UIViewController)(如果有的話),然后再轉(zhuǎn)給它的父視圖(Super View);
3、視圖控制器(如果有的話)的下一個(gè)響應(yīng)者為其管理的視圖的父視圖;
4、單例的窗口(UIWindow)的內(nèi)容視圖將指向窗口本身作為它的下一個(gè)響應(yīng)者
5、單例的應(yīng)用(UIApplication)是一個(gè)響應(yīng)者鏈的終點(diǎn),它的下一個(gè)響應(yīng)者指向nil,以結(jié)束整個(gè)循環(huán)。
二、事件分發(fā)(Event Delivery)
第一響應(yīng)者(First responder)指的是當(dāng)前接受觸摸的響應(yīng)者對象(通常是一個(gè)UIView對象),即表示當(dāng)前該對象正在與用戶交互,它是響應(yīng)者鏈的開端。整個(gè)響應(yīng)者鏈和事件分發(fā)的使命都是找出第一響應(yīng)者。
UIWindow對象以消息的形式將事件發(fā)送給第一響應(yīng)者,使其有機(jī)會(huì)首先處理事件。如果第一響應(yīng)者沒有進(jìn)行處理,系統(tǒng)就將事件(通過消息)傳遞給響應(yīng)者鏈中的下一個(gè)響應(yīng)者,看看它是否可以進(jìn)行處理。
iOS系統(tǒng)檢測到手指觸摸(Touch)操作時(shí)會(huì)將其打包成一個(gè)UIEvent對象,并放入當(dāng)前活動(dòng)Application的事件隊(duì)列,單例的UIApplication會(huì)從事件隊(duì)列中取出觸摸事件并傳遞給單例的UIWindow來處理,UIWindow對象首先會(huì)使用hitTest:withEvent:方法尋找此次Touch操作初始點(diǎn)所在的視圖(View),即需要將觸摸事件傳遞給其處理的視圖,這個(gè)過程稱之為hit-test view。
UIWindow實(shí)例對象會(huì)首先在它的內(nèi)容視圖上調(diào)用hitTest:withEvent:,此方法會(huì)在其視圖層級(jí)結(jié)構(gòu)中的每個(gè)視圖上調(diào)用pointInside:withEvent:(該方法用來判斷點(diǎn)擊事件發(fā)生的位置是否處于當(dāng)前視圖范圍內(nèi),以確定用戶是不是點(diǎn)擊了當(dāng)前視圖),如果pointInside:withEvent:返回YES,則繼續(xù)逐級(jí)調(diào)用,直到找到touch操作發(fā)生的位置,這個(gè)視圖也就是要找的hit-test view。
hitTest:withEvent:方法的處理流程如下
1.首先調(diào)用當(dāng)前視圖的pointInside:withEvent:方法判斷觸摸點(diǎn)是否在當(dāng)前視圖內(nèi);
若返回NO,則hitTest:withEvent:返回nil;
若返回YES,則向當(dāng)前視圖的所有子視圖(subviews)發(fā)送hitTest:withEvent:消息,所有子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖,即從subviews數(shù)組的末尾向前遍歷,直到有子視圖返回非空對象或者全部子視圖遍歷完畢;
若第一次有子視圖返回非空對象,則hitTest:withEvent:方法返回此對象,處理結(jié)束;
如所有子視圖都返回非,則hitTest:withEvent:方法返回自身(self)。

1、A是UIWindow的根視圖,因此,UIWindwo對象會(huì)首相對A進(jìn)行hit-test;
2、顯然用戶點(diǎn)擊的范圍是在A的范圍內(nèi),因此,pointInside:withEvent:返回了YES,這時(shí)會(huì)繼續(xù)檢查A的子視圖;
3、這時(shí)候會(huì)有兩個(gè)分支,B和C:
點(diǎn)擊的范圍不再B內(nèi),因此B分支的pointInside:withEvent:返回NO,對應(yīng)的hitTest:withEvent:返回nil;
點(diǎn)擊的范圍在C內(nèi),即C的pointInside:withEvent:返回YES;
4、這時(shí)候有D和E兩個(gè)分支:
點(diǎn)擊的范圍不再D內(nèi),因此D的pointInside:withEvent:返回NO,對應(yīng)的hitTest:withEvent:返回nil;
點(diǎn)擊的范圍在E內(nèi),即E的pointInside:withEvent:返回YES,由于E沒有子視圖(也可以理解成對E的子視圖進(jìn)行hit-test時(shí)返回了nil),因此,E的hitTest:withEvent:會(huì)將E返回,再往回回溯,就是C的hitTest:withEvent:返回E--->>A的hitTest:withEvent:返回E。
至此,本次點(diǎn)擊事件的第一響應(yīng)者就通過響應(yīng)者鏈的事件分發(fā)邏輯成功的找到了。
不難看出,這個(gè)處理流程有點(diǎn)類似二分搜索的思想,這樣能以最快的速度,最精確地定位出能響應(yīng)觸摸事件的UIView。
三、說明
1、如果最終hit-test沒有找到第一響應(yīng)者,或者第一響應(yīng)者沒有處理該事件,則該事件會(huì)沿著響應(yīng)者鏈向上回溯,如果UIWindow實(shí)例和UIApplication實(shí)例都不能處理該事件,則該事件會(huì)被丟棄;
2、hitTest:withEvent:方法將會(huì)忽略
1.忽略隱藏(hidden=YES)的視圖
2.禁止用戶操作(userInteractionEnabled=YES)的視圖
3.alpha級(jí)別小于0.01(alpha<0.01)的視圖
4.如果一個(gè)子視圖的區(qū)域超過父視圖的bound區(qū)域(父視圖的clipsToBounds 屬性為NO,這樣超過父視圖bound區(qū)域的子視圖內(nèi)容也會(huì)顯示)
那么正常情況下對子視圖在父視圖之外區(qū)域的觸摸操作不會(huì)被識(shí)別,因?yàn)楦敢晥D的pointInside:withEvent:方法會(huì)返回NO,這樣就不會(huì)繼續(xù)向下遍歷子視圖了。當(dāng)然,也可以重寫pointInside:withEvent:方法來處理這種情況。

事件傳遞的完整過程
先將事件對象由上往下傳遞(由父控件傳遞給子控件),找到最合適的控件來處理這個(gè)事件。 調(diào)用最合適控件的touches….方法 如果調(diào)用了[super touches….];就會(huì)將事件順著響應(yīng)者鏈條往上傳遞,傳遞給上一個(gè)響應(yīng)者 接著就會(huì)調(diào)用上一個(gè)響應(yīng)者的touches….方法
如何判斷上一個(gè)響應(yīng)者
如果當(dāng)前這個(gè)view是控制器的view,那么控制器就是上一個(gè)響應(yīng)者 如果當(dāng)前這個(gè)view不是控制器的view,那么父控件就是上一個(gè)響應(yīng)者
響應(yīng)者鏈條的事件傳遞過程
如果view是控制器的view,就傳遞給控制器;如不是,則將其傳遞給它的父視圖 在視圖層次結(jié)構(gòu)的最頂級(jí)視圖,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對象進(jìn)行處理 如果window對象也不處理,則其將事件或消息傳遞給UIApplication對象 如果UIApplication也不能處理該事件或消息,則將其丟棄
注意:為什么用隊(duì)列管理事件,而不用棧?
隊(duì)列先進(jìn)先出,能保證先產(chǎn)生的事件先處理。棧先進(jìn)后出。