轉(zhuǎn)載: https://blog.csdn.net/qq871531334/article/details/82288933
版權(quán)聲明: https://blog.csdn.net/qq871531334/article/details/82288933
本文主要講解iOS觸摸事件的一系列機(jī)制围肥,涉及的問題大致包括:
觸摸事件由觸屏生成后如何傳遞到當(dāng)前應(yīng)用?
應(yīng)用接收觸摸事件后如何尋找最佳響應(yīng)者嚎杨?實(shí)現(xiàn)原理楣嘁?
觸摸事件如何沿著響應(yīng)鏈流動或听?
響應(yīng)鏈发绢、手勢識別器好港、UIControl之間對于觸摸事件的響應(yīng)有著什么樣的瓜葛媒怯?
事件的生命周期
當(dāng)指尖觸碰屏幕的那一刻鸽嫂,一個觸摸事件就在系統(tǒng)中生成了。經(jīng)過IPC進(jìn)程間通信征讲,事件最終被傳遞到了合適的應(yīng)用据某。在應(yīng)用內(nèi)歷經(jīng)峰回路轉(zhuǎn)的奇幻之旅后,最終被釋放诗箍。大致經(jīng)過如下圖:
圖片來源(http://qingmo.me/2017/03/04/FlowOfUITouch/)
系統(tǒng)響應(yīng)階段
1.手指觸碰屏幕癣籽,屏幕感應(yīng)到觸碰后,將事件交由IOKit處理滤祖。
2.IOKit將觸摸事件封裝成一個IOHIDEvent對象筷狼,并通過mach port傳遞給SpringBoad進(jìn)程。
3.SpringBoard進(jìn)程因接收到觸摸事件氨距,觸發(fā)了主線程runloop的source1事件源的回調(diào)。
此時SpringBoard會根據(jù)當(dāng)前桌面的狀態(tài)棘劣,判斷應(yīng)該由誰處理此次觸摸事件俏让。因?yàn)槭录l(fā)生時,你可能正在桌面上翻頁茬暇,也可能正在刷微博首昔。若是前者(即前臺無APP運(yùn)行),則觸發(fā)SpringBoard本身主線程runloop的source0事件源的回調(diào)糙俗,將事件交由桌面系統(tǒng)去消耗勒奇;若是后者(即有app正在前臺運(yùn)行),則將觸摸事件通過IPC傳遞給前臺APP進(jìn)程巧骚,接下來的事情便是APP內(nèi)部對于觸摸事件的響應(yīng)了赊颠。
APP響應(yīng)階段
APP進(jìn)程的mach port接受到SpringBoard進(jìn)程傳遞來的觸摸事件,主線程的runloop被喚醒劈彪,觸發(fā)了source1回調(diào)竣蹦。
source1回調(diào)又觸發(fā)了一個source0回調(diào),將接收到的IOHIDEvent對象封裝成UIEvent對象沧奴,此時APP將正式開始對于觸摸事件的響應(yīng)痘括。
source0回調(diào)內(nèi)部將觸摸事件添加到UIApplication對象的事件隊(duì)列中。事件出隊(duì)后滔吠,UIApplication開始一個尋找最佳響應(yīng)者的過程纲菌,這個過程又稱hit-testing,細(xì)節(jié)將在[尋找事件的最佳響應(yīng)者]一節(jié)闡述疮绷。另外翰舌,此處開始便是與我們平時開發(fā)相關(guān)的工作了。
尋找到最佳響應(yīng)者后冬骚,接下來的事情便是事件在響應(yīng)鏈中的傳遞及響應(yīng)了灶芝,關(guān)于響應(yīng)鏈相關(guān)的內(nèi)容詳見[事件的響應(yīng)及在響應(yīng)鏈中的傳遞]一節(jié)郑原。事實(shí)上,事件除了被響應(yīng)者消耗夜涕,還能被手勢識別器或是target-action模式捕捉并消耗掉犯犁。其中涉及對觸摸事件的響應(yīng)優(yōu)先級,詳見[事件的三徒弟UIResponder女器、UIGestureRecognizer酸役、UIControl]一節(jié)。
觸摸事件歷經(jīng)坎坷后要么被某個響應(yīng)對象捕獲后釋放驾胆,要么致死也沒能找到能夠響應(yīng)的對象涣澡,最終釋放。至此丧诺,這個觸摸事件的使命就算終結(jié)了入桂。runloop若沒有其他事件需要處理,也將重歸于眠驳阎,等待新的事件到來后喚醒抗愁。
現(xiàn)在,你可以回答第一個問題了呵晚。觸摸事件從觸屏產(chǎn)生后蜘腌,由IOKit將觸摸事件傳遞給SpringBoard進(jìn)程,再由SpringBoard分發(fā)給當(dāng)前前臺APP處理饵隙。
觸摸撮珠、事件、響應(yīng)者
說了那么多金矛,到底什么是觸摸芯急、什么是事件、什么是響應(yīng)者驶俊?先簡單科普一下志于。
UITouch
源起觸摸
一個手指一次觸摸屏幕,就對應(yīng)生成一個UITouch對象废睦。多個手指同時觸摸伺绽,生成多個UITouch對象。
多個手指先后觸摸嗜湃,系統(tǒng)會根據(jù)觸摸的位置判斷是否更新同一個UITouch對象奈应。若兩個手指一前一后觸摸同一個位置(即雙擊),那么第一次觸摸時生成一個UITouch對象购披,第二次觸摸更新這個UITouch對象(UITouch對象的 tap count 屬性值從1變成2)杖挣;若兩個手指一前一后觸摸的位置不同,將會生成兩個UITouch對象刚陡,兩者之間沒有聯(lián)系惩妇。
每個UITouch對象記錄了觸摸的一些信息株汉,包括觸摸時間、位置歌殃、階段乔妈、所處的視圖、窗口等信息氓皱。
手指離開屏幕一段時間后路召,確定該UITouch對象不會再被更新將被釋放。
UIEvent
觸摸的目的是生成觸摸事件供響應(yīng)者響應(yīng)波材,一個觸摸事件對應(yīng)一個UIEvent對象股淡,其中的 type 屬性標(biāo)識了事件的類型(之前說過事件不只是觸摸事件)。
UIEvent對象中包含了觸發(fā)該事件的觸摸對象的集合廷区,因?yàn)橐粋€觸摸事件可能是由多個手指同時觸摸產(chǎn)生的唯灵。觸摸對象集合通過 allTouches 屬性獲取。
UIResponder
每個響應(yīng)者都是一個UIResponder對象隙轻,即所有派生自UIResponder的對象埠帕,本身都具備響應(yīng)事件的能力。因此以下類的實(shí)例都是響應(yīng)者:
UIView
UIViewController
UIApplication
AppDelegate
響應(yīng)者之所以能響應(yīng)事件大脉,因?yàn)槠涮峁┝?個處理觸摸事件的方法:
這幾個方法在響應(yīng)者對象接收到事件的時候調(diào)用搞监,用于做出對事件的響應(yīng)水孩。關(guān)于響應(yīng)者何時接收到事件以及事件如何沿著響應(yīng)鏈傳遞將在下面章節(jié)說明镰矿。
尋找事件的最佳響應(yīng)者(Hit-Testing)
第一節(jié)講過APP接收到觸摸事件后,會被放入當(dāng)前應(yīng)用的一個事件隊(duì)列中(PS為什么是隊(duì)列而不是棧俘种?很好理解因?yàn)橛|摸事件必然是先發(fā)生先執(zhí)行秤标,切合隊(duì)列FIFO的原則)。
每個事件的理想宿命是被能夠響應(yīng)它的對象響應(yīng)后釋放宙刘,然而響應(yīng)者諸多苍姜,事件一次只有一個,誰都想把事件搶到自己碗里來悬包,為避免紛爭衙猪,就得有一個先后順序,也就是得有一個響應(yīng)者的優(yōu)先級布近。因此這就存在一個尋找事件最佳響應(yīng)者(又稱第一響應(yīng)者 first responder)的過程垫释,目的是找到一個具備最高優(yōu)先級響應(yīng)權(quán)的響應(yīng)對象(the most appropriate responder object),這個過程叫做Hit-Testing撑瞧,那個命中的最佳響應(yīng)者稱為hit-tested view棵譬。
本節(jié)要探討的問題是:
應(yīng)用接收到事件后,如何尋找最佳響應(yīng)者预伺?底層如何實(shí)現(xiàn)订咸?
尋找最佳響應(yīng)者過程中事件的攔截曼尊。
事件自下而上的傳遞
應(yīng)用接收到事件后先將其置入事件隊(duì)列中以等待處理。出隊(duì)后脏嚷,application首先將事件傳遞給當(dāng)前應(yīng)用最后顯示的窗口(UIWindow)詢問其能否響應(yīng)事件骆撇。若窗口能響應(yīng)事件,則傳遞給子視圖詢問是否能響應(yīng)然眼,子視圖若能響應(yīng)則繼續(xù)詢問子視圖艾船。子視圖詢問的順序是優(yōu)先詢問后添加的子視圖,即子視圖數(shù)組中靠后的視圖高每。事件傳遞順序如下:
事實(shí)上把UIWindow也看成是視圖即可屿岂,這樣整個傳遞過程就是一個遞歸詢問子視圖能否響應(yīng)事件過程,且后添加的子視圖優(yōu)先級高(對于window而言就是后顯示的window優(yōu)先級高)鲸匿。
具體流程如下:
UIApplication首先將事件傳遞給窗口對象(UIWindow)爷怀,若存在多個窗口,則優(yōu)先詢問后顯示的窗口带欢。
若窗口不能響應(yīng)事件运授,則將事件傳遞其他窗口;若窗口能響應(yīng)事件乔煞,則從后往前詢問窗口的子視圖吁朦。
重復(fù)步驟2。即視圖若不能響應(yīng)渡贾,則將事件傳遞給上一個同級子視圖逗宜;若能響應(yīng),則從后往前詢問當(dāng)前視圖的子視圖空骚。
視圖若沒有能響應(yīng)的子視圖了纺讲,則自身就是最合適的響應(yīng)者。
示例:
hit-testing 場景
視圖層級如下(同一層級的視圖越在下面囤屹,表示越后添加):
現(xiàn)在假設(shè)在E視圖所處的屏幕位置觸發(fā)一個觸摸熬甚,應(yīng)用接收到這個觸摸事件事件后,先將事件傳遞給UIWindow肋坚,然后自下而上開始在子視圖中尋找最佳響應(yīng)者乡括。事件傳遞的順序如下所示:
UIWindow將事件傳遞給其子視圖A
A判斷自身能響應(yīng)該事件,繼續(xù)將事件傳遞給C(因?yàn)橐晥DC比視圖B后添加智厌,因此優(yōu)先傳給C)诲泌。
C判斷自身能響應(yīng)事件,繼續(xù)將事件傳遞給F(同理F比E后添加)峦剔。
F判斷自身不能響應(yīng)事件档礁,C又將事件傳遞給E。
E判斷自身能響應(yīng)事件吝沫,同時E已經(jīng)沒有子視圖呻澜,因此最終E就是最佳響應(yīng)者递礼。
Hit-Testing的本質(zhì)
上面講了事件在響應(yīng)者之間傳遞的規(guī)則,視圖通過判斷自身能否響應(yīng)事件來決定是否繼續(xù)向子視圖傳遞羹幸。那么問題來了:視圖如何判斷能否響應(yīng)事件脊髓?以及視圖如何將事件傳遞給子視圖?
首先要知道的是栅受,以下幾種狀態(tài)的視圖無法響應(yīng)事件:
不允許交互:userInteractionEnabled = NO
隱藏:hidden = YES 如果父視圖隱藏将硝,那么子視圖也會隱藏,隱藏的視圖無法接收事件
透明度:alpha < 0.01 如果設(shè)置一個視圖的透明度<0.01屏镊,會直接影響子視圖的透明度依疼。alpha:0.0~0.01為透明。
hitTest:withEvent:
每個UIView對象都有一個 hitTest:withEvent: 方法而芥,這個方法是Hit-Testing過程中最核心的存在律罢,其作用是詢問事件在當(dāng)前視圖中的響應(yīng)者,同時又是作為事件傳遞的橋梁棍丐。
hitTest:withEvent: 方法返回一個UIView對象误辑,作為當(dāng)前視圖層次中的響應(yīng)者。默認(rèn)實(shí)現(xiàn)是:
若當(dāng)前視圖無法響應(yīng)事件歌逢,則返回nil
若當(dāng)前視圖可以響應(yīng)事件巾钉,但無子視圖可以響應(yīng)事件,則返回自身作為當(dāng)前視圖層次中的事件響應(yīng)者
若當(dāng)前視圖可以響應(yīng)事件秘案,同時有子視圖可以響應(yīng)砰苍,則返回子視圖層次中的事件響應(yīng)者
一開始UIApplication將事件通過調(diào)用UIWindow對象的 hitTest:withEvent: 傳遞給UIWindow對象,UIWindow的 hitTest:withEvent: 在執(zhí)行時若判斷本身能響應(yīng)事件踏烙,則調(diào)用子視圖的 hitTest:withEvent: 將事件傳遞給子視圖并詢問子視圖上的最佳響應(yīng)者师骗。最終UIWindow返回一個視圖層次中的響應(yīng)者視圖給UIApplication历等,這個視圖就是hit-testing的最佳響應(yīng)者讨惩。
系統(tǒng)對于視圖能否響應(yīng)事件的判斷邏輯除了之前提到的3種限制狀態(tài),默認(rèn)能響應(yīng)的條件就是觸摸點(diǎn)在當(dāng)前視圖的坐標(biāo)系范圍內(nèi)寒屯。因此荐捻,hitTest:withEvent: 的默認(rèn)實(shí)現(xiàn)就可以推測了,大致如下:
值得注意的是 pointInside:withEvent: 這個方法寡夹,用于判斷觸摸點(diǎn)是否在自身坐標(biāo)范圍內(nèi)处面。默認(rèn)實(shí)現(xiàn)是若在坐標(biāo)范圍內(nèi)則返回YES,否則返回NO菩掏。
現(xiàn)在我們在上述示例的視圖層次中的每個視圖類中添加下面3個方法來驗(yàn)證一下之前的分析(注意 hitTest:withEvent: 和 pointInside:withEvent: 方法都要調(diào)用父類的實(shí)現(xiàn)魂角,否則不會按照默認(rèn)的邏輯來執(zhí)行Hit-Testing):
單點(diǎn)觸摸視圖E,相關(guān)日志打印如下:
可以看到最終是視圖E先對事件進(jìn)行了響應(yīng)智绸,同時事件傳遞過程也和之前的分析一致野揪。事實(shí)上單擊后從 [AView hitTest:withEvent:] 到 [EView pointInside:withEvent:] 的過程會執(zhí)行兩遍访忿,兩次傳的是同一個touch,區(qū)別在于touch的狀態(tài)不同斯稳,第一次是begin階段海铆,第二次是end階段。也就是說挣惰,應(yīng)用對于事件的傳遞起源于觸摸狀態(tài)的變化卧斟。
Hit-Testing過程中的事件攔截(自定義事件流向)
實(shí)際開發(fā)中可能會遇到一些特殊的交互需求,需要定制視圖對于事件的響應(yīng)憎茂。例如下面Tabbar的這種情況珍语,中間的原型按鈕是底部Tabbar上的控件,而Tabbar是添加在控制器根視圖中的竖幔。默認(rèn)情況下我們點(diǎn)擊圖中紅色方框中按鈕的區(qū)域廊酣,會發(fā)現(xiàn)按鈕并不會得到響應(yīng)。
hit-testing過程中事件攔截場景
分析一下原因其實(shí)很容易就能明白問題所在赏枚。忽略不相關(guān)的控件亡驰,視圖層次如下:
點(diǎn)擊紅色方框區(qū)域后,生成的觸摸事件首先傳到UIWindow饿幅,然后傳到控制器的根視圖即RootView凡辱。RootView經(jīng)判斷可以響應(yīng)觸摸事件,而后將事件傳給了子控件TabBar栗恩。問題就出在這里透乾,因?yàn)橛|摸點(diǎn)不在TabBar的坐標(biāo)范圍內(nèi),因此TabBar無法響應(yīng)該觸摸事件磕秤,hitTest:withEvent: 直接返回了nil乳乌。而后RootView就會詢問TableView是否能夠響應(yīng),事實(shí)上是可以的市咆,因此事件最終被TableView消耗汉操。整個過程,事件根本沒有傳遞到圓形按鈕蒙兰。
有問題就會有解決策略磷瘤。經(jīng)過分析,發(fā)現(xiàn)原因是hit-Testing的過程中搜变,事件在傳遞到TabBar的時候沒能繼續(xù)往CircleButton傳采缚,因?yàn)辄c(diǎn)擊區(qū)域坐標(biāo)不在Tabbar的坐標(biāo)范圍內(nèi),因此Tabbar被識別成了無法響應(yīng)事件挠他。既然如此扳抽,我們可以修改事件hit-Testing的過程,當(dāng)點(diǎn)擊紅色方框區(qū)域時讓事件流向原型按鈕。
事件傳遞到TabBar時贸呢,TabBar的 hitTest:withEvent: 被調(diào)用赂苗,但是 pointInside:withEvent: 會返回NO,如此一來 hitTest:withEvent: 返回了nil贮尉。既然如此拌滋,可以重寫TabBard的 pointInside:withEvent: ,判斷當(dāng)前觸摸坐標(biāo)是否在子視圖CircleButton的坐標(biāo)范圍內(nèi)猜谚,若在败砂,則返回YES,反之返回NO魏铅。這樣一來點(diǎn)擊紅色區(qū)域昌犹,事件最終會傳遞到CircleButton,CircleButton能夠響應(yīng)事件览芳,最終事件就由CircleButton響應(yīng)了斜姥。同時點(diǎn)擊紅色方框以外的非TabBar區(qū)域的情況下,因?yàn)門abBar無法響應(yīng)事件沧竟,會按照預(yù)期由TableView響應(yīng)铸敏。代碼如下:
這樣一來,點(diǎn)擊紅色方框區(qū)域的按鈕就有效了悟泵。
事件的響應(yīng)及在響應(yīng)鏈中的傳遞
經(jīng)歷Hit-Testing后杈笔,UIApplication已經(jīng)知道事件的最佳響應(yīng)者是誰了,接下來要做的事情就是:
將事件傳遞給最佳響應(yīng)者響應(yīng)
事件沿著響應(yīng)鏈傳遞
事件響應(yīng)的前奏
因?yàn)樽罴秧憫?yīng)者具有最高的事件響應(yīng)優(yōu)先級糕非,因此UIApplication會先將事件傳遞給它供其響應(yīng)蒙具。首先,UIApplication將事件通過 sendEvent: 傳遞給事件所屬的window朽肥,window同樣通過 sendEvent: 再將事件傳遞給hit-tested view禁筏,即最佳響應(yīng)者。過程如下:
以尋找事件的最佳響應(yīng)者一節(jié)中點(diǎn)擊視圖E為例衡招,在EView的 touchesBegan:withEvent: 上斷點(diǎn)查看調(diào)用棧就能看清這一過程:
touchesBegan調(diào)用棧
那么問題又來了篱昔。這個過程中,假如應(yīng)用中存在多個window對象蚁吝,UIApplication是怎么知道要把事件傳給哪個window的旱爆?window又是怎么知道哪個視圖才是最佳響應(yīng)者的呢舀射?
其實(shí)簡單思考一下窘茁,這兩個過程都是傳遞事件的過程,涉及的方法都是 sendEvent: 脆烟,而該方法的參數(shù)(UIEvent對象)是唯一貫穿整個經(jīng)過的線索山林,那么就可以大膽猜測必然是該觸摸事件對象上綁定了這些信息。事實(shí)上之前在介紹UITouch的時候就說過touch對象保存了觸摸所屬的window及view,而event對象又綁定了touch對象驼抹,如此一來桑孩,是不是就說得通了。要是不信的話框冀,那就自定義一個Window類流椒,重寫 sendEvent: 方法,捕捉該方法調(diào)用時參數(shù)event的狀態(tài)明也,答案就顯而易見了宣虾。
sendEvent
至于這兩個屬性是什么時候綁定到touch對象上的,必然是在hit-testing的過程中唄温数,仔細(xì)想想hit-testing干的不就是這個事兒嗎~
事件的響應(yīng)
前面介紹UIResponder的時候說過阅爽,每個響應(yīng)者必定都是UIResponder對象杏糙,通過4個響應(yīng)觸摸事件的方法來響應(yīng)事件。每個UIResponder對象默認(rèn)都已經(jīng)實(shí)現(xiàn)了這4個方法,但是默認(rèn)不對事件做任何處理辫狼,單純只是將事件沿著響應(yīng)鏈傳遞。若要截獲事件進(jìn)行自定義的響應(yīng)操作形娇,就要重寫相關(guān)的方法仔粥。例如,通過重寫 touchesMoved: withEvent: 方法實(shí)現(xiàn)簡單的視圖拖動冕屯。
每個響應(yīng)觸摸事件的方法都會接收兩個參數(shù)淑蔚,分別對應(yīng)觸摸對象集合和事件對象。通過監(jiān)聽觸摸對象中保存的觸摸點(diǎn)位置的變動愕撰,可以時時修改視圖的位置刹衫。視圖(UIView)作為響應(yīng)者對象,本身已經(jīng)實(shí)現(xiàn)了 touchesMoved: withEvent: 方法搞挣,因此要創(chuàng)建一個自定義視圖(繼承自UIView)带迟,重寫該方法。
每個響應(yīng)者都有權(quán)決定是否執(zhí)行對事件的響應(yīng)囱桨,只要重寫相關(guān)的觸摸事件方法即可仓犬。
事件的傳遞(響應(yīng)鏈)
前面一直在提最佳響應(yīng)者,之所以稱之為“最佳”舍肠,是因?yàn)槠渚邆漤憫?yīng)事件的最高優(yōu)先權(quán)(響應(yīng)鏈頂端的男人)搀继。最佳響應(yīng)者首先接收到事件,然后便擁有了對事件的絕對控制權(quán):即它可以選擇獨(dú)吞這個事件翠语,也可以將這個事件往下傳遞給其他響應(yīng)者叽躯,這個由響應(yīng)者構(gòu)成的視圖鏈就稱之為響應(yīng)鏈。
響應(yīng)者對于事件的操作方式:
響應(yīng)者對于事件的攔截以及傳遞都是通過 touchesBegan:withEvent: 方法控制的肌括,該方法的默認(rèn)實(shí)現(xiàn)是將事件沿著默認(rèn)的響應(yīng)鏈往下傳遞点骑。
響應(yīng)者對于接收到的事件有3種操作:
不攔截,默認(rèn)操作。事件會自動沿著默認(rèn)的響應(yīng)鏈往下傳遞
攔截黑滴,不再往下分發(fā)事件憨募。重寫 touchesBegan:withEvent: 進(jìn)行事件處理,不調(diào)用父類的 touchesBegan:withEvent:
攔截袁辈,繼續(xù)往下分發(fā)事件菜谣。重寫 touchesBegan:withEvent: 進(jìn)行事件處理,同時調(diào)用父類的 touchesBegan:withEvent: 將事件往下傳遞
響應(yīng)鏈中的事件傳遞規(guī)則:
每一個響應(yīng)者對象(UIResponder對象)都有一個 nextResponder 方法晚缩,用于獲取響應(yīng)鏈中當(dāng)前對象的下一個響應(yīng)者葛菇。因此,一旦事件的最佳響應(yīng)者確定了橡羞,這個事件所處的響應(yīng)鏈就確定了眯停。
對于響應(yīng)者對象,默認(rèn)的 nextResponder 實(shí)現(xiàn)如下:
UIView卿泽。若視圖是控制器的根視圖莺债,則其nextResponder為控制器對象;否則签夭,其nextResponder為父視圖齐邦。
UIViewController。若控制器的視圖是window的根視圖第租,則其nextResponder為窗口對象措拇;若控制器是從別的控制器present出來的,則其nextResponder為presenting view controller慎宾。
UIWindow丐吓。nextResponder為UIApplication對象。
UIApplication趟据。若當(dāng)前應(yīng)用的app delegate是一個UIResponder對象券犁,且不是UIView、UIViewController或app本身汹碱,則UIApplication的nextResponder為app delegate粘衬。
上圖是官網(wǎng)對于響應(yīng)鏈的示例展示,若觸摸發(fā)生在UITextField上咳促,則事件的傳遞順序是:
圖中虛線箭頭是指若該UIView是作為UIViewController根視圖存在的稚新,則其nextResponder為UIViewController對象;若是直接add在UIWindow上的跪腹,則其nextResponder為UIWindow對象褂删。
可以用以下方式打印一個響應(yīng)鏈中的每一個響應(yīng)對象,在最佳響應(yīng)者的 touchBegin:withEvent: 方法中調(diào)用即可(別忘了調(diào)用父類的方法)
以上一節(jié)原型按鈕的案例為例尺迂,重寫CircleButton的 touchBegin:withEvent:
點(diǎn)擊原型按鈕的任意區(qū)域笤妙,打印出的完整響應(yīng)鏈如下:
另外如果有需要冒掌,完全可以重寫響應(yīng)者的 nextResponder 方法來自定義響應(yīng)鏈噪裕。
事件的三徒弟UIResponder蹲盘、UIGestureRecognizer、UIControl
iOS中膳音,除了UIResponder能夠響應(yīng)事件召衔,手勢識別器、UIControl同樣具備對事件的處理能力祭陷。當(dāng)這幾者同時存在于某一場景下的時候苍凛,事件又會有怎樣的歸宿呢?
拋磚引玉
場景界面如圖:
手勢沖突場景
代碼不能再簡單:
然后我像往常一樣懷揣著吃奶的自信點(diǎn)擊了cell兵志。what醇蝴??點(diǎn)不動想罕?悠栓?點(diǎn)歪了嗎?按价?再點(diǎn)惭适,還是沒反應(yīng)!楼镐!我試著短按了一小會兒cell癞志,依舊沒反應(yīng)!框产!我不死心凄杯,長按了一會兒,didSelectRowAtIndexPath終于調(diào)了秉宿,還算給點(diǎn)面子 - -盾舌。然后我又點(diǎn)了下面的button,沒有任何問題蘸鲸。but what 妖谴??
為了搞清楚狀況酌摇,我自定義了相關(guān)的控件類膝舅,均重寫了4個響應(yīng)觸摸事件的方法以打印日志(每個重寫的觸摸事件方法都調(diào)用了父類的方法以保證事件默認(rèn)傳遞邏輯)。
觀察各種情況下的日志現(xiàn)象:
現(xiàn)象一 快速點(diǎn)擊cell
現(xiàn)象二 短按cell
現(xiàn)象三 長按cell
現(xiàn)象四 點(diǎn)擊button
如果上面的現(xiàn)象依舊能讓你舒心地抿上一口咖啡窑多,那么恭喜你仍稀,本節(jié)的內(nèi)容已經(jīng)不適合你了。如果覺得一臉懵逼埂息,那就繼續(xù)往下看吧~
二師兄—手勢識別器
關(guān)于手勢識別器即 UIGestureRecognizer 本身的使用不是本文要所討論的內(nèi)容技潘,按下不表遥巴。此處要探討的是:手勢識別器與UIResponder的聯(lián)系。
事實(shí)上享幽,手勢分為離散型手勢(discrete gestures)和持續(xù)型手勢(continuous gesture)铲掐。系統(tǒng)提供的離散型手勢包括點(diǎn)按手勢(UITapGestureRecognizer)和輕掃手勢(UISwipeGestureRecognizer),其余均為持續(xù)型手勢值桩。
兩者主要區(qū)別在于狀態(tài)變化過程:
離散型:
識別成功:Possible —> Recognized
識別失敯诿埂:Possible —> Failed
持續(xù)型:
完整識別:Possible —> Began —> [Changed] —> Ended
不完整識別:Possible —> Began —> [Changed] —> Cancel
離散型手勢
先拋開上面的場景,看一個簡單的demo奔坟。
控制器的視圖上add了一個View記為YellowView携栋,并綁定了一個單擊手勢識別器。
單擊YellowView咳秉,日志打印如下:
從日志上看出YellowView最后Cancel了對觸摸事件的響應(yīng)婉支,而正常應(yīng)當(dāng)是觸摸結(jié)束后,YellowView的 touchesEnded:withEvent: 的方法被調(diào)用才對澜建。另外向挖,期間還執(zhí)行了手勢識別器綁定的action 。我從官方文檔找到了這樣的解釋:
大致理解是霎奢,Window在將事件傳遞給hit-tested view之前户誓,會先將事件傳遞給相關(guān)的手勢識別器并由手勢識別器優(yōu)先識別。若手勢識別器成功識別了事件幕侠,就會取消hit-tested view對事件的響應(yīng)帝美;若手勢識別器沒能識別事件,hit-tested view才完全接手事件的響應(yīng)權(quán)晤硕。
一句話概括:手勢識別器比UIResponder具有更高的事件響應(yīng)優(yōu)先級!舞箍!
按照這個解釋舰褪,Window在將事件傳遞給hit-tested view即YellowView之前,先傳遞給了控制器根視圖上的手勢識別器疏橄。手勢識別器成功識別了該事件占拍,通知Application取消YellowView對事件的響應(yīng)。
然而看日志捎迫,卻是YellowView的 touchesBegan:withEvent: 先調(diào)用了晃酒,既然手勢識別器先響應(yīng),不應(yīng)該上面的action先執(zhí)行嗎窄绒,這又怎么解釋贝次?事實(shí)上這個認(rèn)知是錯誤的。手勢識別器的action的調(diào)用時機(jī)(即此處的 actionTap)并不是手勢識別器接收到事件的時機(jī)彰导,而是手勢識別器成功識別事件后的時機(jī)蛔翅,即手勢識別器的狀態(tài)變?yōu)閁IGestureRecognizerStateRecognized敲茄。因此從該日志中并不能看出事件是優(yōu)先傳遞給手勢識別器的,那該怎么證明Window先將事件傳遞給了手勢識別器山析?
要解決這個問題堰燎,只要知道手勢識別器是如何接收事件的,然后在接收事件的方法中打印日志對比調(diào)用時間先后即可盖腿。說起來你可能不信爽待,手勢識別器對于事件的響應(yīng)也是通過這4個熟悉的方法來實(shí)現(xiàn)的损同。
需要注意的是翩腐,雖然手勢識別器通過這幾個方法來響應(yīng)事件,但它并不是UIResponder的子類膏燃,相關(guān)的方法聲明在 UIGestureRecognizerSubclass.h 中茂卦。
這樣一來,我們便可以自定義一個單擊手勢識別器的類组哩,重寫這幾個方法來監(jiān)聽手勢識別器接收事件的時機(jī)等龙。創(chuàng)建一個UITapGestureRecognizer的子類,重寫響應(yīng)事件的方法伶贰,每個方法中調(diào)用父類的實(shí)現(xiàn)蛛砰,并替換demo中的手勢識別器。另外需要在.m文件中引入 import黍衙,因?yàn)橄嚓P(guān)方法聲明在該頭文件中泥畅。
現(xiàn)在,再次點(diǎn)擊YellowView琅翻,日志如下:
很明顯位仁,確實(shí)是手勢識別器先接收到了事件。之后手勢識別器成功識別了手勢方椎,執(zhí)行了action聂抢,再由Application取消了YellowView對事件的響應(yīng)。
Window怎么知道要把事件傳遞給哪些手勢識別器棠众?
之前探討過Application怎么知道要把event傳遞給哪個Window琳疏,以及Window怎么知道要把event傳遞給哪個hit-tested view的問題,答案是這些信息都保存在event所綁定的touch對象上闸拿。手勢識別器也是一樣的空盼,event綁定的touch對象上維護(hù)了一個手勢識別器數(shù)組,里面的手勢識別器毫無疑問是在hit-testing的過程中收集的胸墙。打個斷點(diǎn)看一下touch上綁定的手勢識別器數(shù)組:
Window先將事件傳遞給這些手勢識別器我注,再傳給hit-tested view。一旦有手勢識別器成功識別了手勢迟隅,Application就會取消hit-tested view對事件的響應(yīng)但骨。
持續(xù)型手勢
將上面Demo中視圖綁定的單擊手勢識別器用滑動手勢識別器(UIPanGestureRecognizer)替換励七。
在YellowView上執(zhí)行一次滑動:
日志打印如下:
在一開始滑動的過程中,手勢識別器處在識別手勢階段奔缠,滑動產(chǎn)生的連續(xù)事件既會傳遞給手勢識別器又會傳遞給YellowView掠抬,因此YellowView的 touchesMoved:withEvent: 在開始一段時間內(nèi)會持續(xù)調(diào)用;當(dāng)手勢識別器成功識別了該滑動手勢時校哎,手勢識別器的action開始調(diào)用两波,同時通知Application取消YellowView對事件的響應(yīng)。之后僅由滑動手勢識別器接收事件并響應(yīng)闷哆,YellowView不再接收事件腰奋。
另外,在滑動的過程中抱怔,若手勢識別器未能識別手勢劣坊,則事件在觸摸滑動過程中會一直傳遞給hit-tested view,直到觸摸結(jié)束屈留。讀者可自行驗(yàn)證局冰。
手勢識別器的3個屬性
先總結(jié)一下手勢識別器與UIResponder對于事件響應(yīng)的聯(lián)系:
當(dāng)觸摸發(fā)生或者觸摸的狀態(tài)發(fā)生變化時,Window都會傳遞事件尋求響應(yīng)灌危。
Window先將綁定了觸摸對象的事件傳遞給觸摸對象上綁定的手勢識別器康二,再發(fā)送給觸摸對象對應(yīng)的hit-tested view。
手勢識別器識別手勢期間勇蝙,若觸摸對象的觸摸狀態(tài)發(fā)生變化沫勿,事件都是先發(fā)送給手勢識別器再發(fā)送給hit-test view。
手勢識別器若成功識別了手勢浅蚪,則通知Application取消hit-tested view對于事件的響應(yīng)藕帜,并停止向hit-tested view發(fā)送事件;
若手勢識別器未能識別手勢惜傲,而此時觸摸并未結(jié)束洽故,則停止向手勢識別器發(fā)送事件,僅向hit-test view發(fā)送事件盗誊。
若手勢識別器未能識別手勢时甚,且此時觸摸已經(jīng)結(jié)束,則向hit-tested view發(fā)送end狀態(tài)的touch事件以停止對事件的響應(yīng)哈踱。
cancelsTouchesInView
默認(rèn)為YES荒适。表示當(dāng)手勢識別器成功識別了手勢之后,會通知Application取消響應(yīng)鏈對事件的響應(yīng)开镣,并不再傳遞事件給hit-test view刀诬。若設(shè)置成NO,表示手勢識別成功后不取消響應(yīng)鏈對事件的響應(yīng)邪财,事件依舊會傳遞給hit-test view陕壹。
demo中設(shè)置: pan.cancelsTouchesInView = NO
滑動時日志如下:
即便滑動手勢識別器識別了手勢质欲,Application也會依舊發(fā)送事件給YellowView。
delaysTouchesBegan
默認(rèn)為NO糠馆。默認(rèn)情況下手勢識別器在識別手勢期間嘶伟,當(dāng)觸摸狀態(tài)發(fā)生改變時,Application都會將事件傳遞給手勢識別器和hit-tested view又碌;若設(shè)置成YES九昧,則表示手勢識別器在識別手勢期間,截斷事件毕匀,即不會將事件發(fā)送給hit-tested view铸鹰。
設(shè)置 pan.delaysTouchesBegan = YES
日志如下:
因?yàn)榛瑒邮謩葑R別器在識別期間,事件不會傳遞給YellowView期揪,因此期間YellowView的 touchesBegan:withEvent: 和 touchesMoved:withEvent: 都不會被調(diào)用掉奄;而后滑動手勢識別器成功識別了手勢规个,也就獨(dú)吞了事件凤薛,不會再傳遞給YellowView。因此只打印了手勢識別器成功識別手勢后的action調(diào)用诞仓。
delaysTouchesEnded
默認(rèn)為YES缤苫。當(dāng)手勢識別失敗時,若此時觸摸已經(jīng)結(jié)束墅拭,會延遲一小段時間(0.15s)再調(diào)用響應(yīng)者的 touchesEnded:withEvent:活玲;若設(shè)置成NO,則在手勢識別失敗時會立即通知Application發(fā)送狀態(tài)為end的touch事件給hit-tested view以調(diào)用 touchesEnded:withEvent: 結(jié)束事件響應(yīng)谍婉。
大師兄—UIControl
UIControl是系統(tǒng)提供的能夠以target-action模式處理觸摸事件的控件舒憾,iOS中UIButton、UISegmentedControl穗熬、UISwitch等控件都是UIControl的子類镀迂。當(dāng)UIControl跟蹤到觸摸事件時,會向其上添加的target發(fā)送事件以執(zhí)行action唤蔗。值得注意的是探遵,UIConotrol是UIView的子類,因此本身也具備UIResponder應(yīng)有的身份妓柜。
關(guān)于UIControl箱季,此處介紹兩點(diǎn):
target-action執(zhí)行時機(jī)及過程
觸摸事件優(yōu)先級
target-action
target:處理交互事件的對象
action:處理交互事件的方式
UIControl作為能夠響應(yīng)事件的控件,必然也需要待事件交互符合條件時才去響應(yīng)棍掐,因此也會跟蹤事件發(fā)生的過程藏雏。不同于UIControl以及UIGestureRecognizer通過 touches 系列方法跟蹤,UIControl有其獨(dú)特的跟蹤方式:
乍一看作煌,這4個方法和UIResponder的那4個方法幾乎吻合掘殴,只不過UIControl只能接收單點(diǎn)觸控蝠嘉,因此接收的參數(shù)是單個UITouch對象。這幾個方法的職能也和UIResponder一致杯巨,用來跟蹤觸摸的開始蚤告、滑動、結(jié)束服爷、取消杜恰。不過,UIControl本身也是UIResponder仍源,因此同樣有 touches 系列的4個方法心褐。事實(shí)上,UIControl的 Tracking 系列方法是在 touch 系列方法內(nèi)部調(diào)用的笼踩。比如 beginTrackingWithTouch 是在 touchesBegan 方法內(nèi)部調(diào)用的逗爹, 因此它雖然也是UIResponder,但 touches 系列方法的默認(rèn)實(shí)現(xiàn)和UIResponder本類還是有區(qū)別的嚎于。
當(dāng)UIControl跟蹤事件的過程中掘而,識別出事件交互符合響應(yīng)條件,就會觸發(fā)target-action進(jìn)行響應(yīng)于购。UIControl控件通過 addTarget:action:forControlEvents: 添加事件處理的target和action袍睡,當(dāng)事件發(fā)生時,UIControl通知target執(zhí)行對應(yīng)的action肋僧。說是“通知”其實(shí)很籠統(tǒng)斑胜,事實(shí)上這里有個action傳遞的過程。當(dāng)UIControl監(jiān)聽到需要處理的交互事件時嫌吠,會調(diào)用 sendAction:to:forEvent: 將target止潘、action以及event對象發(fā)送給全局應(yīng)用,Application對象再通過 sendAction:to:from:forEvent: 向target發(fā)送action辫诅。
因此凭戴,可以通過重寫UIControl的 sendAction:to:forEvent: 或 sendAction:to:from:forEvent: 自定義事件執(zhí)行的target及action。
另外泥栖,若不指定target簇宽,即 addTarget:action:forControlEvents: 時target傳空,那么當(dāng)事件發(fā)生時吧享,Application會在響應(yīng)鏈上從上往下尋找能響應(yīng)action的對象魏割。官方說明如下:
觸摸事件優(yōu)先級
當(dāng)原本關(guān)系已經(jīng)錯綜復(fù)雜的UIGestureRecognizer和UIResponder之間又冒出一個UIControl,又會摩擦出什么樣的火花呢钢颂?
簡單理解:UIControl會阻止父視圖上的手勢識別器行為钞它,也就是UIControl處理事件的優(yōu)先級比UIGestureRecognizer高,但前提是相比于父視圖上的手勢識別器。
UIControl測試場景
預(yù)置場景:在BlueView上添加一個button遭垛,同時給button添加一個target-action事件尼桶。
示例一:在BlueView上添加點(diǎn)擊手勢識別器
示例二:在button上添加手勢識別器
操作方式:單擊button
測試結(jié)果:示例一中,button的target-action響應(yīng)了單擊事件锯仪;示例二中泵督,BlueView上的手勢識別器響應(yīng)了事件。過程日志打印如下:
按鈕點(diǎn)擊
原因分析:點(diǎn)擊button后庶喜,事件先傳遞給手勢識別器小腊,再傳遞給作為hit-tested view存在的button(UIControl本身也是UIResponder,這一過程和普通事件響應(yīng)者無異)久窟。示例一中秩冈,由于button阻止了父視圖BlueView中的手勢識別器的識別,導(dǎo)致手勢識別器識別失敵饪浮(狀態(tài)為failed 枚舉值為5)入问,button完全接手了事件的響應(yīng)權(quán),事件最終由button響應(yīng)稀颁;示例二中芬失,button未阻止其本身綁定的手勢識別器的識別,因此手勢識別器先識別手勢并識別成功(狀態(tài)為ended 枚舉值為3)峻村,而后通知Application取消響應(yīng)鏈對事件的響應(yīng)麸折,因?yàn)?touchesCancelled 被調(diào)用,同時 cancelTrackingWithEvent 跟著調(diào)用粘昨,因此button的target-action得不到執(zhí)行。
其他:經(jīng)測試窜锯,若示例一中的手勢識別器設(shè)置 cancelsTouchesInView 為NO张肾,手勢識別器和button都能響應(yīng)事件。也就是說這種情況下锚扎,button不會阻止父視圖中手勢識別器的識別吞瞪。
結(jié)論:UIControl比其父視圖上的手勢識別器具有更高的事件響應(yīng)優(yōu)先級。
TODO:
上述過程中驾孔,手勢識別器在執(zhí)行touchesEnded時是根據(jù)什么將狀態(tài)置為ended還是failed的芍秆?即根據(jù)什么判斷應(yīng)當(dāng)識別成功還是識別失敗翠勉?
糾正
以上所述UIControl的響應(yīng)優(yōu)先級比手勢識別器高的說法不準(zhǔn)確妖啥,準(zhǔn)確地說只適用于系統(tǒng)提供的有默認(rèn)action操作的UIControl,例如UIbutton对碌、UISwitch等的單擊荆虱,而對于自定義的UIControl,經(jīng)驗(yàn)證,響應(yīng)優(yōu)先級比手勢識別器低怀读。讀者可自行驗(yàn)證诉位,感謝 @閆仕偉 同學(xué)的糾正。
撥云見日
現(xiàn)在菜枷,把膠卷回放到本章節(jié)開頭的場景苍糠。給你一杯咖啡的時間看看能不能解釋得通那幾個現(xiàn)象了,不說了泡咖啡去了...
我肥來了啤誊!
先看現(xiàn)象二椿息,短按 cell無法響應(yīng),日志如下:
這個日志和上面離散型手勢Demo中打印的日志完全一致坷衍。短按后寝优,BackView上的手勢識別器先接收到事件,之后事件傳遞給hit-tested view枫耳,作為響應(yīng)者鏈中一員的GLTableView的 touchesBegan:withEvent: 被調(diào)用乏矾;而后手勢識別器成功識別了點(diǎn)擊事件,action執(zhí)行迁杨,同時通知Application取消響應(yīng)鏈中的事件響應(yīng)钻心,GLTableView的 touchesCancelled:withEvent: 被調(diào)用。
因?yàn)槭录蝗∠饲π虼薈ell無法響應(yīng)點(diǎn)擊捷沸。
再看現(xiàn)象三,長按cell能夠響應(yīng)狐史,日志如下:
長按的過程中痒给,一開始事件同樣被傳遞給手勢識別器和hit-tested view,作為響應(yīng)鏈中一員的GLTableView的 touchesBegan:withEvent: 被調(diào)用骏全;此后在長按的過程中苍柏,手勢識別器一直在識別手勢,直到一定時間后手勢識別失敗姜贡,才將事件的響應(yīng)權(quán)完全交給響應(yīng)鏈试吁。當(dāng)觸摸結(jié)束的時候,GLTableView的 touchesEnded:withEvent: 被調(diào)用楼咳,同時Cell響應(yīng)了點(diǎn)擊熄捍。
OK,現(xiàn)在回到現(xiàn)象一母怜。按照之前的分析余耽,快速點(diǎn)擊cell,講道理不管是表現(xiàn)還是日志都應(yīng)該和現(xiàn)象二一致才對糙申。然而日志僅僅打印了手勢識別器的action執(zhí)行結(jié)果宾添。分析一下原因:GLTableView的 touchesBegan 沒有調(diào)用船惨,說明事件沒有傳遞給hit-tested view。那只有一種可能缕陕,就是事件被某個手勢識別器攔截了粱锐。目前已知的手勢識別器攔截事件的方法,就是設(shè)置 delaysTouchesBegan 為YES扛邑,在手勢識別器未識別完成的情況下不會將事件傳遞給hit-tested view怜浅。然后事實(shí)上并沒有進(jìn)行這樣的設(shè)置,那么問題可能出在別的手勢識別器上蔬崩。
Window的 sendEvent: 打個斷點(diǎn)查看event上的touch對象維護(hù)的手勢識別器數(shù)組:
ScrollView延遲發(fā)送事件
捕獲可疑對象:UIScrollViewDelayedTouchesBeganGestureRecognizer 恶座,光看名字就覺得這貨脫不了干系。從類名上猜測沥阳,這個手勢識別器大概會延遲事件向響應(yīng)鏈的傳遞跨琳。github上找到了該私有類的頭文件:
有一個_touchDelay變量,大概是用來控制延遲事件發(fā)送的桐罕。另外脉让,方法列表里有個 sendTouchesShouldBeginForDelayedTouches: 方法,聽名字似乎是在一段時間延遲后向響應(yīng)鏈傳遞事件用的功炮。為一探究竟溅潜,我創(chuàng)建了一個類hook了這個方法:
斷點(diǎn)看一下點(diǎn)擊cell后 hook_sendTouchesShouldBeginForDelayedTouches: 調(diào)用時的信息:
延遲的本質(zhì)
可以看到這個手勢識別器的 _touchDelay 變量中,保存了一個計(jì)時器薪伏,以及一個長得很像延遲時間間隔的變量m_delay」隼剑現(xiàn)在,可以推測該手勢識別器截斷了事件并延遲0.15s才發(fā)送給hit-tested view嫁怀。為驗(yàn)證猜測设捐,我分別在Window的 sendEvent: ,hook_sendTouchesShouldBeginForDelayedTouches: 以及TableView的 touchesBegan: 中打印時間戳眶掌,若猜測成立挡育,則應(yīng)當(dāng)前兩者的調(diào)用時間相差0.15s左右,后兩者的調(diào)用時間很接近朴爬。短按Cell后打印結(jié)果如下(不能快速點(diǎn)擊,否則還沒過延遲時間觸摸就結(jié)束了橡淆,無法驗(yàn)證猜測):
因?yàn)橛袃蓚€ UIScrollViewDelayedTouchesBeganGestureRecognizer召噩,所以 hook_sendTouchesShouldBeginForDelayedTouches 調(diào)了兩次,兩次的時間很接近逸爵【叩危可以看到,結(jié)果完全符合猜測师倔。
這樣就都解釋得通了」乖希現(xiàn)象一由于點(diǎn)擊后,UIScrollViewDelayedTouchesBeganGestureRecognizer 攔截了事件并延遲了0.15s發(fā)送。又因?yàn)辄c(diǎn)擊時間比0.15s短疲恢,在發(fā)送事件前觸摸就結(jié)束了凶朗,因此事件沒有傳遞到hit-tested view,導(dǎo)致TableView的 touchBegin 沒有調(diào)用显拳。而現(xiàn)象二棚愤,由于短按的時間超過了0.15s,手勢識別器攔截了事件并經(jīng)過0.15s后杂数,觸摸還未結(jié)束宛畦,于是將事件傳遞給了hit-tested view,使得TableView接收到了事件揍移。因此現(xiàn)象二的日志雖然和離散型手勢Demo中的日志一致次和,但實(shí)際上前者的hit-tested view是在觸摸后延遲了約0.15s左右才接收到觸摸事件的。
至于現(xiàn)象四 那伐,你現(xiàn)在應(yīng)該已經(jīng)覺得理所當(dāng)然了才對踏施。
總結(jié)
觸摸發(fā)生時,系統(tǒng)內(nèi)核生成觸摸事件喧锦,先由IOKit處理封裝成IOHIDEvent對象读规,通過IPC傳遞給系統(tǒng)進(jìn)程SpringBoard,而后再傳遞給前臺APP處理燃少。
事件傳遞到APP內(nèi)部時被封裝成開發(fā)者可見的UIEvent對象束亏,先經(jīng)過hit-testing尋找第一響應(yīng)者,而后由Window對象將事件傳遞給hit-tested view阵具,并開始在響應(yīng)鏈上的傳遞碍遍。
UIRespnder、UIGestureRecognizer阳液、UIControl怕敬,籠統(tǒng)地講,事件響應(yīng)優(yōu)先級依次遞增帘皿。
參考資料
史上最詳細(xì)的iOS之事件的傳遞和響應(yīng)機(jī)制-原理篇
Understanding Event Handling, Responders, and the Responder Chain
作者:NicooYang
鏈接:http://www.reibang.com/p/da46db65c3e7
來源:簡書
著作權(quán)歸作者所有东跪。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處鹰溜。