一.hitTest:withEvent:調用過程
iOS系統檢測到手指觸摸(Touch)操作時會將其放入當前活動Application的事件隊列,UIApplication會從事件隊列中取出觸摸事件并傳遞給key window(當前接收用戶事件的窗口)處理,window對象首先會使用hitTest:withEvent:方法尋找此次Touch操作初始點所在的視圖(View),即需要將觸摸事件傳遞給其處理的視圖,稱之為hit-test view。
window對象會在首先在view hierarchy的頂級view上調用hitTest:withEvent:,此方法會在視圖層級結構中的每個視圖上調用pointInside:withEvent:,如果pointInside:withEvent:返回YES,則繼續逐級調用,直到找到touch操作發生的位置,這個視圖也就是hit-test view。
hitTest:withEvent:方法的處理流程如下:
首先調用當前視圖的pointInside:withEvent:方法判斷觸摸點是否在當前視圖內;
若返回NO,則hitTest:withEvent:返回nil;
若返回YES,則向當前視圖的所有子視圖(subviews)發送hitTest:withEvent:消息,所有子視圖的遍歷順序是從top到bottom,即從subviews數組的末尾向前遍歷,直到有子視圖返回非空對象或者全部子視圖遍歷完畢;
若第一次有子視圖返回非空對象,則hitTest:withEvent:方法返回此對象,處理結束;
如所有子視圖都返回非,則hitTest:withEvent:方法返回自身(self)。
hitTest:withEvent:方法忽略隱藏(hidden=YES)的視圖,禁止用戶操作(userInteractionEnabled=YES)的視圖,以及alpha級別小于0.01(alpha<0.01)的視圖。如果一個子視圖的區域超過父視圖的bound區域(父視圖的clipsToBounds屬 性為NO,這樣超過父視圖bound區域的子視圖內容也會顯示),那么正常情況下對子視圖在父視圖之外區域的觸摸操作不會被識別,因為父視圖的 pointInside:withEvent:方法會返回NO,這樣就不會繼續向下遍歷子視圖了。當然,也可以重寫 pointInside:withEvent:方法來處理這種情況。
對于每個觸摸操作都會有一個UITouch對 象,UITouch對象用來表示一個觸摸操作,即一個手指在屏幕上按下、移動、離開的整個過程。UITouch對象在觸摸操作的過程中在不斷變化,所以在 使用UITouch對象時,不能直接retain,而需要使用其他手段存儲UITouch的內部信息。UITouch對象有一個view屬性,表示此觸摸操作初始發生所在的視圖,即上面檢測到的hit-test view,此屬性在UITouch的生命周期不再改變,即使觸摸操作后續移動到其他視圖之上。
如果父視圖需要對對哪個子視圖可以響應觸摸事件做特殊控制,則可以重寫hitTest:withEvent:或pointInside:withEvent:方法。
這里有幾個例子:
hitTest Hacking the responder chain
在此例子中button,scrollview同為topView的子視圖,但scrollview覆蓋在button之上,這樣在在button上的觸 摸操作返回的hit-test view為scrollview,button無法響應,可以修改topView的hitTest:withEvent:方法如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = [super hitTest:point withEvent:event];
CGPoint buttonPoint = [underButton convertPoint:point fromView:self];
if ([underButton pointInside:buttonPoint withEvent:event]) {
return underButton;
}
return result;
}
這樣如果觸摸點在button的范圍內,返回hittestView為button,從button按鈕可以響應點擊事件。
Paging-enabled UIScrollView with Previews
關于這兩個例子,可以看之前文章的說明,見Paging-enabled UIScrollView
三.hitTest:withEvent:探秘,詭異的三次調用
在測試中發現對每一次觸摸操作實際會觸發三次hitTest:withEvent:方法調用,有測試環境視圖結構如下 UIWindow->UIScrollView->TapDetectingImageView
首先在TapDetectingImageView的三次hitTest:withEvent:打印兩個參數查看有何不同
三次的打印結果分別為:
point:{356.25, 232.031} event: timestamp: 25269.8 touches: {()}
point:{356.25, 232.031} event: timestamp: 25269.8 touches: {()}
point:{356.25, 232.031} event: timestamp: 25272 touches: {()}
三次調用的point參數完全相同,event從指針看為同一對象,但時間戳有變化,第一次和第二次的時間戳相同,第三次的與之前有區別。
深入檢查event參數
對于UIEvent對象我們還可以查看其內部的數據,UIEvent實際上是對GSEventRefs的包裝,在GSEventRefs中又包含GSEventRecord,其結構如下:
typedef struct __GSEvent {
CFRuntimeBase _base;
GSEventRecord record;
} GSEvent; typedef struct __GSEvent* GSEventRef;
typedef struct GSEventRecord {
GSEventType type; // 0x8 //2
GSEventSubType subtype;? ? // 0xC //3
CGPoint location;? ? // 0x10 //4
CGPoint windowLocation;? ? // 0x18 //6
int windowContextId;? ? // 0x20 //8
uint64_t timestamp;? ? // 0x24, from mach_absolute_time //9
GSWindowRef window;? ? // 0x2C //
GSEventFlags flags;? ? // 0x30 //12
unsigned senderPID;? ? // 0x34 //13
CFIndex infoSize; // 0x38 //14 } GSEventRecord;
在GSEventRecord中我們可以獲取到GSEvent事件類型type,windowLocation(在窗口坐標系中的位置)等參數。
訪問UIEvent中的GSEventRecord可以使用以下代碼
if ([event respondsToSelector:@selector(_gsEvent)]) {
#define GSEVENT_TYPE 2
#define GSEVENT_SUBTYPE 3
#define GSEVENT_X_OFFSET 6
#define GSEVENT_Y_OFFSET 7
#define GSEVENT_FLAGS 12
#define GSEVENTKEY_KEYCODE 15
#define GSEVENT_TYPE_KEYUP 11
int *eventMem;
eventMem = (int *)objc_unretainedPointer([event performSelector:@selector(_gsEvent)]);
if (eventMem) {
int eventType = eventMem[GSEVENT_TYPE];
int eventSubType = eventMem[GSEVENT_SUBTYPE];
float xOffset =? *((float*)(eventMem + GSEVENT_X_OFFSET));
float yOffset =? *((float*)(eventMem + GSEVENT_Y_OFFSET));
}
}
按照上文描述的方法我們獲取到UIEvent內部的windowLocation數據,同時將接收到的point參數在window坐標系中的位置也打印出,這樣三次調用的數據分別為
point:{356.25, 232.031} windowPoint:{152, 232} event: timestamp: 25269.8 touches: {()} gsEventType:3001 gsXoffset:213.000000 gsYoffset:316.000000
point:{356.25, 232.031} windowPoint:{152, 232} event: timestamp: 25269.8 touches: {()} gsEventType:3001 gsXoffset:213.000000 gsYoffset:316.000000
point:{356.25, 232.031} windowPoint:{152, 232} event: timestamp: 25272 touches: {()} gsEventType:3001 gsXoffset:152.000000 gsYoffset:232.000000
第一次和第二次 調用的時間戳相同,GSEvent中的windowLocation也相同,但windowLocation并不是當前請求的point位置,第三次請求 的時間戳與前兩次不同,GSEvent中的windowLocation與當前請求的point位置一致。
多次點擊可發現,第一次和第二次調用中的windowLocation數據實際上是上次點擊操作的位置,猜測前兩次hitTest是對上次點擊操作的終結?第三次hitTest才是針對當前點擊的。
調用棧分析
使用
[NSThreadcallStackSymbols];
可以獲取到當前線程的調用棧數據,在TapDetectingImageView的hitTest:withEvent:中打印調用棧信息,分別為:
第一次調用:
0? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x0002b52d -[TapDetectingImageView hitTest:withEvent:] + 93
1? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132
2? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b515a7 __NSArrayChunkIterate + 359
3? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b2903f __NSArrayEnumerate + 1023
4? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102
5? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640
6? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00497397 -[UIScrollView hitTest:withEvent:] + 79
7? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132
8? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b515a7 __NSArrayChunkIterate + 359
9? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b2903f __NSArrayEnumerate + 1023
10? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102
11? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640
12? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x0002e01b -[UIEventProbeWindow hitTest:withEvent:] + 395
13? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00477a02 __47+[UIWindow _hitTestToPoint:pathIndex:forEvent:]_block_invoke_0 + 150
14? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00477888 +[UIWindow _topVisibleWindowPassingTest:] + 196
15? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00477965 +[UIWindow _hitTestToPoint:pathIndex:forEvent:] + 177
16? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0043cd06 _UIApplicationHandleEvent + 1696
17? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff8df9 _PurpleEventCallback + 339
18? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff8ad0 PurpleEventCallback + 46
19? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01aa4bf5 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 53
20? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01aa4962 __CFRunLoopDoSource1 + 146
21? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01ad5bb6 __CFRunLoopRun + 2118
22? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01ad4f44 CFRunLoopRunSpecific + 276
23? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01ad4e1b CFRunLoopRunInMode + 123
24? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff77e3 GSEventRunModal + 88
25? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff7668 GSEventRun + 104
26? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0043c65c UIApplicationMain + 1211
27? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x000026b2 main + 178
28? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x000025b5 start + 53
29? ???? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00000001 0x0 + 1
第二次調用
0? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x0002b52d -[TapDetectingImageView hitTest:withEvent:] + 93
1? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132
2? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b515a7 __NSArrayChunkIterate + 359
3? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b2903f __NSArrayEnumerate + 1023
4? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102
5? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640
6? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00497397 -[UIScrollView hitTest:withEvent:] + 79
7? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132
8? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b515a7 __NSArrayChunkIterate + 359
9? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b2903f __NSArrayEnumerate + 1023
10? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102
11? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640
12? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x0002e01b -[UIEventProbeWindow hitTest:withEvent:] + 395
13? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00477a02 __47+[UIWindow _hitTestToPoint:pathIndex:forEvent:]_block_invoke_0 + 150
14? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00477888 +[UIWindow _topVisibleWindowPassingTest:] + 196
15? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00477965 +[UIWindow _hitTestToPoint:pathIndex:forEvent:] + 177
16? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0043cfd3 _UIApplicationHandleEvent + 2413
17? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff8df9 _PurpleEventCallback + 339
18? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff8ad0 PurpleEventCallback + 46
19? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01aa4bf5 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 53
20? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01aa4962 __CFRunLoopDoSource1 + 146
21? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01ad5bb6 __CFRunLoopRun + 2118
22? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01ad4f44 CFRunLoopRunSpecific + 276
23? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01ad4e1b CFRunLoopRunInMode + 123
24? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff77e3 GSEventRunModal + 88
25? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff7668 GSEventRun + 104
26? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0043c65c UIApplicationMain + 1211
27? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x000026b2 main + 178
28? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x000025b5 start + 53
29? ???? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00000001 0x0 + 1
第三次調用:
0? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x0002b52d -[TapDetectingImageView hitTest:withEvent:] + 93
1? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132
2? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b515a7 __NSArrayChunkIterate + 359
3? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b2903f __NSArrayEnumerate + 1023
4? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102
5? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640
6? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00497397 -[UIScrollView hitTest:withEvent:] + 79
7? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f4d5 __38-[UIView(Geometry) hitTest:withEvent:]_block_invoke_0 + 132
8? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b515a7 __NSArrayChunkIterate + 359
9? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b2903f __NSArrayEnumerate + 1023
10? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01b28a16 -[NSArray enumerateObjectsWithOptions:usingBlock:] + 102
11? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0047f3d1 -[UIView(Geometry) hitTest:withEvent:] + 640
12? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x0002e01b -[UIEventProbeWindow hitTest:withEvent:] + 395
13? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0043d986 _UIApplicationHandleEvent + 4896
14? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff8df9 _PurpleEventCallback + 339
15? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff8ad0 PurpleEventCallback + 46
16? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01aa4bf5 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 53
17? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01aa4962 __CFRunLoopDoSource1 + 146
18? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01ad5bb6 __CFRunLoopRun + 2118
19? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01ad4f44 CFRunLoopRunSpecific + 276
20? CoreFoundation? ? ? ? ? ? ? ? ? ? ? 0x01ad4e1b CFRunLoopRunInMode + 123
21? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff77e3 GSEventRunModal + 88
22? GraphicsServices? ? ? ? ? ? ? ? ? ? 0x01ff7668 GSEventRun + 104
23? UIKit? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x0043c65c UIApplicationMain + 1211
24? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x000026b2 main + 178
25? RenrenPhoto? ? ? ? ? ? ? ? ? ? ? ? 0x000025b5 start + 53
26? ???? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0x00000001 0x0 + 1
從調用棧上看,三次調用的路徑都不相同,關鍵區分點在_UIApplicationHandleEvent函數中,
第一次的調用位置為_UIApplicationHandleEvent + 1696
第二次的調用位置為_UIApplicationHandleEvent + 2413
第三次的調用位置為_UIApplicationHandleEvent + 4896
結論
沒有結論,具體機制仍然是撲朔迷離,搞不懂….,實際寫代碼時也不需要考慮這些。
參考:
Event Handling Guide for iOS - Event Delivery
Event Handling Guide for iOS - Hit-Testing
Event handling for iOS - how hitTest:withEvent: and pointInside:withEvent: are related?
hitTest Hacking the responder chain
Paging-enabled UIScrollView with Previews
Catching Keyboard Events in iOS
Kenny TM GoogleCode Repo - GSEvent.h (a bit old but still useful)
Kenny TM Github Repo - GSEvent.h
Intercepting status bar touches on the iPhone
Synthesizing a touch event on the iPhone
/*
**當按鈕超過了父視圖范圍,點擊是沒有反應的。因為消息的傳遞是從最下層的父視圖開始調用hittest方法。
[objc] view plain copy print?
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [super hitTest:point withEvent:event];
return view;
}
**當存在view時才會傳遞對應的event,現在點擊了父視圖以外的范圍,自然返回的是nil。所以當子視圖(比如按鈕btn)因為一些原因超出了父視圖范圍,就要重寫hittest方法,讓其返回對應的子視圖,來接收事件。即:點擊的點在自視圖范圍內(self.button)就返回self.button此時就會執行self.button的點擊事件
*/
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
//先執行父類的方法返回響應對象
UIView*view = [superhitTest:pointwithEvent:event];
//點擊了父類以外的地方所以返回nil
if(view ==nil) {
//點轉化成在self.button內的坐標
CGPointtempoint = [self.buttonconvertPoint:pointfromView:self];
//判斷是不是在self.button.bounds的范圍內如果是就響應事件
if(CGRectContainsPoint(self.button.bounds, tempoint))
{
view =self.button;
}
}
returnview;
}