1. 前言
手势事件采集是 iOS 点击事件采集的外围性能,手势事件采集实现思路并不简单,然而其中难点较多,本文针对这些难点逐个给出了解决方案。
上面咱们来看看如何在 iOS 中实现手势事件采集。
2. 手势介绍
Apple 提供了 UIGestureRecognizer[1] 相干的类用于解决手势操作,常见的手势如下:
UITapGestureRecognizer:点击;
UILongPressGestureRecognizer:长按;
UIPinchGestureRecognizer:捏合;
UIRotationGestureRecognizer:旋转。
UIGestureRecognizer 类定义了一组公共行为,能够为所有具体的手势识别器配置这些行为。
手势识别器可能对特定视图进行触摸响应,因而须要通过 UIView 的 – addGestureRecognizer: 办法将视图和手势进行关联。
一个手势识别器能够领有多个 Target-Action 对,这些 Target-Action 是互相独立的,手势辨认后会向每个 Target-Action 对发送音讯。
3. 采集计划
因为每个手势识别器能够关联多个 Target-Action,联合 Runtime 的 Method Swizzling,咱们能够在用户为手势增加 Target-Action 时,再额定增加一个采集事件的 Target-Action 对。
总体流程如图 3-1 所示:
图 3-1 手势事件采集流程
上面咱们来看下具体的代码实现。
Method Swizzling:
-
(void)enableAutoTrackGesture {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{[UIGestureRecognizer sa_swizzleMethod:@selector(initWithTarget:action:) withMethod:@selector(sensorsdata_initWithTarget:action:) error:NULL]; [UIGestureRecognizer sa_swizzleMethod:@selector(addTarget:action:) withMethod:@selector(sensorsdata_addTarget:action:) error:NULL];
});
}
增加采集事件的 Target-Action: - (void)sensorsdata_addTarget:(id)target action:(SEL)action {
self.sensorsdata_gestureTarget = [SAGestureTarget targetWithGesture:self];
[self sensorsdata_addTarget:self.sensorsdata_gestureTarget action:@selector(trackGestureRecognizerAppClick:)];
[self sensorsdata_addTarget:target action:action];
}
手势事件采集: - (void)trackGestureRecognizerAppClick:(UIGestureRecognizer *)gesture {
// 手势事件采集
…
}
通过 Method Swizzling 咱们可能如愿采集手势事件,但这存在一个问题:零碎的诸多行为也是通过手势进行实现的,同样会被咱们采集,但咱们的初衷是只采集用户增加的手势。
局部公有手势如表 3-1 所示:
1 UIScrollViewKnobLongPressGestureRecognizer UIScrollViewKnobLongPressGestureRecognizer -> UILongPressGestureRecognizer -> UIGestureRecognizer
2 _UIDragAddItemsGesture _UIDragAddItemsGesture -> UITapGestureRecognizer -> UIGestureRecognizer
3 _UIDragLiftGestureRecognizer _UIDragLiftGestureRecognizer -> UILongPressGestureRecognizer -> UIGestureRecognizer
4 _UIDragLiftPointerGestureRecognizer _UIDragLiftPointerGestureRecognizer -> _UIDragLiftGestureRecognizer -> UILongPressGestureRecognizer -> UIGestureRecognizer
5 UIKBProductivitySingleTapGesture UIKBProductivitySingleTapGesture -> UITapGestureRecognizer -> UIGestureRecognizer
6 UIKBProductivityDoubleTapGesture UIKBProductivityDoubleTapGesture -> UITapGestureRecognizer -> UIGestureRecognizer
7 _UIWebHighlightLongPressGestureRecognizer _UIWebHighlightLongPressGestureRecognizer -> UILongPressGestureRecognizer -> UIGestureRecognizer
8 _UISingleFingerTapExtensionGesture _UISingleFingerTapExtensionGesture -> UITapGestureRecognizer -> UIGestureRecognizer
9 WKSyntheticTapGestureRecognizer WKSyntheticTapGestureRecognizer -> UITapGestureRecognizer -> UIGestureRecognizer
10 … …
表 3-1 局部公有手势
如何不采集零碎公有手势事件,成为了亟待解决的问题。
3.1. 屏蔽零碎公有手势
零碎公有手势和公开对外的手势并没有本质区别,都继承或间接继承自 UIGestureRecognizer 类。
当手势被增加了 Target-Action 后,咱们能够通过 Target 对象归属的类所在的 Bundle 判断以后的手势是否是零碎公有手势。
零碎库的 bundle 格局如下:
/System/Library/PrivateFrameworks/UIKitCore.framework
/System/Library/Frameworks/WebKit.framework
开发者的 bundle 格局如下:
/private/var/containers/Bundle/Application/8264D420-DE23-48AC-9985-A7F1E131A52A/CDDStoreDemo.app
实现如下:
-
(BOOL)isPrivateClassWithObject:(NSObject *)obj {
if (!obj) {return NO;
}
NSString *bundlePath = [[NSBundle bundleForClass:[obj class]] bundlePath];
if ([bundlePath hasPrefix:@”/System/Library”]) {return YES;
}
return NO;
}
这里须要留神的是:该办法不适用于模拟器。
该计划可能辨别是否是零碎公有手势,但当增加的 Target 是 UIGestureRecognizer 实例对象自身时则无奈辨别是否是须要采集的手势事件,因而该计划不可行。
3.2. 仅采集点击和长按手势
调试时可能发现,大部分零碎公有手势是子类化的,且开发者很少会对手势进行子类化操作,因而咱们能够仅实现对 UITapGestureRecognizer、UILongPressGestureRecognizer 手势的采集,子类化的手势不采集。
咱们在创立 Target 对象时,对手势校验,满足条件的手势返回一个无效的 Target 对象。
-
(SAGestureTarget _Nullable)targetWithGesture:(UIGestureRecognizer )gesture {
NSString *gestureType = NSStringFromClass(gesture.class);
if ([gesture isMemberOfClass:UITapGestureRecognizer.class] ||[gesture isMemberOfClass:UILongPressGestureRecognizer.class]) {return [[SAGestureTarget alloc] init];
}
return nil;
}
4. 难点攻克
到目前为止,仿佛能够失常实现点击和长按手势的采集了。然而,事实远非如此,还有一些难点须要解决。
场景一:在开发者增加 Target-Action 后,又移除了;
场景二:在开发者增加 Target-Action 后,Target 在某些场景下被开释了;
场景三:尽管仅采集了 UITapGestureRecognizer、UILongPressGestureRecognizer,但仍存在一些零碎公有手势是未子类化的,被谬误采集;
场景四:UIAlertController 点击事件采集须要非凡解决;
场景五:对于局部手势状态须要非凡解决。
4.1. 治理 Target-Action
针对场景一和场景二,SDK 不该当采集手势事件。然而 SDK 曾经增加了 Target-Action,因而须要在采集时判断除了 SDK 增加的 Target-Action,是否还存在无效的 Target-Action,如果不存在则不该当采集手势事件。
对于 UIGestureRecognizer 零碎并未提供公开的 API 接口获取以后手势所有的 Target-Action。尽管可能通过公有 API‘_targets’获取,然而有可能对客户产生影响。因而咱们通过 hook 相干办法,本人记录 Target-Action 的数量。
新建 SAGestureTargetActionModel 类,用于治理 Target 和 Action:
@interface SAGestureTargetActionModel : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL action;
@property (nonatomic, assign, readonly) BOOL isValid;
- (instancetype)initWithTarget:(id)target action:(SEL)action;
- (SAGestureTargetActionModel _Nullable)containsObjectWithTarget:(id)target andAction:(SEL)action fromModels:(NSArray <SAGestureTargetActionModel *>)models;
@end
在 – addTarget:action: 和 – removeTarget:action: 中记录 Target 数量:
-
(void)enableAutoTrackGesture {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{... [UIGestureRecognizer sa_swizzleMethod:@selector(removeTarget:action:) withMethod:@selector(sensorsdata_removeTarget:action:) error:NULL];
});
} -
(void)sensorsdata_addTarget:(id)target action:(SEL)action {
if (self.sensorsdata_gestureTarget) {if (![SAGestureTargetActionModel containsObjectWithTarget:target andAction:action fromModels:self.sensorsdata_targetActionModels]) {SAGestureTargetActionModel *resulatModel = [[SAGestureTargetActionModel alloc] initWithTarget:target action:action]; [self.sensorsdata_targetActionModels addObject:resulatModel]; [self sensorsdata_addTarget:self.sensorsdata_gestureTarget action:@selector(trackGestureRecognizerAppClick:)]; }
}
[self sensorsdata_addTarget:target action:action];
} -
(void)sensorsdata_removeTarget:(id)target action:(SEL)action {
if (self.sensorsdata_gestureTarget) {SAGestureTargetActionModel *existModel = [SAGestureTargetActionModel containsObjectWithTarget:target andAction:action fromModels:self.sensorsdata_targetActionModels]; if (existModel) {[self.sensorsdata_targetActionModels removeObject:existModel]; }
}
[self sensorsdata_removeTarget:target action:action];
}
在事件采集时,校验是否满足采集条件: -
(void)trackGestureRecognizerAppClick:(UIGestureRecognizer *)gesture {
if ([SAGestureTargetActionModel filterValidModelsFrom:gesture.sensorsdata_targetActionModels].count == 0) {return NO;
}
// 手势事件采集
…
}
4.2. 黑名单
针对场景三,神策 SDK 减少了黑名单的配置,通过配置 View 类型来屏蔽这些手势的采集。
{
"public": [
"UIPageControl",
"UITextView",
"UITabBar",
"UICollectionView",
"UISearchBar"
],
"private": [
"_UIContextMenuContainerView",
"_UIPreviewPlatterView",
"UISwitchModernVisualElement",
"WKContentView",
"UIWebBrowserView"
]
}
在进行类型比拟时,咱们对公开和公有的类型进行了辨别解决:
公开类名应用 – isKindOfClass: 判断;
公有类名应用字符串匹配判断。
-
(BOOL)isIgnoreWithView:(UIView *)view {
…
// 公开类名应用 – isKindOfClass: 判断
id publicClasses = info[@”public”];
if ([publicClasses isKindOfClass:NSArray.class]) {for (NSString *publicClass in (NSArray *)publicClasses) {if ([view isKindOfClass:NSClassFromString(publicClass)]) {return YES;} }
}
// 公有类名应用字符串匹配判断
id privateClasses = info[@”private”];
if ([privateClasses isKindOfClass:NSArray.class]) {if ([(NSArray *)privateClasses containsObject:NSStringFromClass(view.class)]) {return YES;}
}
return NO;
}
4.3. UIAlertController 点击事件采集
UIAlertController 外部是通过手势实现用户交互操作,但其手势所在的 View 并不是用户操作的 View,且在不同的零碎版本中外部实现略有不同。
咱们通过应用不同的处理器来解决这种非凡逻辑。
新建工厂类 SAGestureViewProcessorFactory 来决定应用的处理器:
@implementation SAGestureViewProcessorFactory
-
(SAGeneralGestureViewProcessor )processorWithGesture:(UIGestureRecognizer )gesture {
NSString *viewType = NSStringFromClass(gesture.view.class);
if ([viewType isEqualToString:@”_UIAlertControllerView”]) {return [[SALegacyAlertGestureViewProcessor alloc] initWithGesture:gesture];
}
if ([viewType isEqualToString:@”_UIAlertControllerInterfaceActionGroupView”]) {return [[SANewAlertGestureViewProcessor alloc] initWithGesture:gesture];
}
return [[SAGeneralGestureViewProcessor alloc] initWithGesture:gesture];
}
@end
而后在具体的处理器中解决差别:
pragma mark – 适配 iOS 10 以前的 Alert
@implementation SALegacyAlertGestureViewProcessor
-
(BOOL)isTrackable {
if (![super isTrackable]) {return NO;
}
// 屏蔽 SAAlertController 的点击事件
UIViewController *viewController = [SAAutoTrackUtils findNextViewControllerByResponder:self.gesture.view];
if ([viewController isKindOfClass:UIAlertController.class] && [viewController.nextResponder isKindOfClass:SAAlertController.class]) {return NO;
}
return YES;
} -
(UIView *)trackableView {
NSArray <UIView *>*visualViews = sensorsdata_searchVisualSubView(@”_UIAlertControllerCollectionViewCell”, self.gesture.view);
CGPoint currentPoint = [self.gesture locationInView:self.gesture.view];
for (UIView *visualView in visualViews) {CGRect rect = [visualView convertRect:visualView.bounds toView:self.gesture.view]; if (CGRectContainsPoint(rect, currentPoint)) {return visualView;}
}
return nil;
}
@end
pragma mark – 适配 iOS 10 及当前的 Alert
@implementation SANewAlertGestureViewProcessor
-
(BOOL)isTrackable {
if (![super isTrackable]) {return NO;
}
// 屏蔽 SAAlertController 的点击事件
UIViewController *viewController = [SAAutoTrackUtils findNextViewControllerByResponder:self.gesture.view];
if ([viewController isKindOfClass:UIAlertController.class] && [viewController.nextResponder isKindOfClass:SAAlertController.class]) {return NO;
}
return YES;
} -
(UIView *)trackableView {
NSArray <UIView *>*visualViews = sensorsdata_searchVisualSubView(@”_UIInterfaceActionCustomViewRepresentationView”, self.gesture.view);
CGPoint currentPoint = [self.gesture locationInView:self.gesture.view];
for (UIView *visualView in visualViews) {CGRect rect = [visualView convertRect:visualView.bounds toView:self.gesture.view]; if (CGRectContainsPoint(rect, currentPoint)) {return visualView;}
}
return nil;
}
@end
4.4. 解决手势状态
手势识别器是由状态机驱动的,默认状态是 UIGestureRecognizerStatePossible,示意曾经筹备好开始处理事件。
状态之间的转换如图 4-1 所示:
图 4-1 手势状态转换[2]
针对全埋点,无论手势状态是 UIGestureRecognizerStateEnded 还是 UIGestureRecognizerStateCancelled 都该当采集手势事件:
-
(void)trackGestureRecognizerAppClick:(UIGestureRecognizer *)gesture {
if (gesture.state != UIGestureRecognizerStateEnded &&gesture.state != UIGestureRecognizerStateCancelled) {return;
}
// 手势事件采集
…
}
@end
5. 总结
本文介绍了 iOS 手势事件采集的一种具体实现形式,同时也介绍了针对局部难点是如何进行解决的。更多细节可参考神策 iOS SDK 源码[3],如果大家有更好的想法,欢送退出开源社区一起探讨。
- 参考文献
[1]https://developer.apple.com/d…
[2]https://developer.apple.com/d…
[3]https://github.com/sensorsdat…