iOS无埋点数据采集实践
无埋点又称全埋点或者零埋点,虽然叫法不一样,想要到达的目的是一样的,即数据收集不再需要hard code,甚至可以做到动态可配要收集的数据。这里不讨论无埋点的方案细节,只探讨实践过程中遇到的几个点。目前看到网上的资料里提出的方案还有提升的空间。下面具体分析一下。
1.UITableViewCell的点击事件
UIKIT_EXTERN NSNotificationName const UITableViewSelectionDidChangeNotification;
如通知的名字所示UITableViewCell被选中的时候会发通知,监听此通知即可收集UITableViewCell选中事件。
2. UITableViewCell展现事件
对UIKitCore.framework使用class-dump获得UITableView的方法列表。UIKitCore.framework的路径为
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/
也可以使用Xcode来dump UITableView,可以得到更详细的信息,推荐使用。首先配置lldb命令,使用命令
dclass -I UITableView
通过搜索willDisplay
发现如下三个方法:
-_createPreparedCellForGlobalRow:willDisplay:
-_createPreparedCellForRowAtIndexPath:willDisplay:
-_createPreparedCellForGlobalRow:withIndexPath:willDisplay:
我们分别来看三个方法的实现。这里使用Hopper Disassembler,在Hopper里打开UIKitCore.framework
,等分析完毕,搜索 -_createPreparedCellForGlobalRow:willDisplay:
并定位,代码如下:
可以发现内部调用了第3个方法。
再看-_createPreparedCellForRowAtIndexPath:willDisplay:
的实现:
这里也调用了第3个方法。
最后看-_createPreparedCellForGlobalRow:withIndexPath:willDisplay:
的实现:
这个方法的内容较多,截取了一部分。可以看到箭头指向的地方回调了delegate方法-tableView:willDisplayCell:forRowAtIndexPath:
。通过Hook这个方法就可以做到监听UITableViewCell的展现。
实现hook方法的时候注意32位和64位时参数的类型不同。
3.UICollectionView的点击和展现
把上面的方法应用到UICollectionView即可以找到对应方法。
-_notifyWillDisplayCellIfNeeded:forIndexPath:
-_selectItemAtIndexPath:animated:scrollPosition:notifyDelegate:
4. UIControl点击事件
@interface UIApplication
- (BOOL)sendAction:(SEL)action to:(nullable id)target from:(nullable id)sender forEvent:(nullable UIEvent *)event;
Hook UIApplication
的方法sendAction:to:from:forEvent:
即可。
5.UITapGesture, UILongPressGesture
@interface UIApplication
- (void)sendEvent:(UIEvent *)event;
这个方法会分发app里的UI事件,需要先把UIControl的事件过滤掉。然后先处理Gesture事件。在UITouchPhaseBegan的时候记录下touch里的可能相应的gesture,在UITouchPhaseEnded时找到状态为UIGestureRecognizerStatePossible的gesture,即为触发的gesture。在记录gesture的时候同时监听gesture的state
,UILongPressGesture触发时,state
变为UIGestureRecognizerStatePossible。
bool Click::handleGesture(UITouch *touch) {
if (touch.phase == UITouchPhaseBegan) {
bool handled = false;
for (UIGestureRecognizer *obj in touch.gestureRecognizers) {
if (obj.enabled) {
observeGesture(obj);
handled = true;
}
}
return handled;
} else if (touch.phase == UITouchPhaseCancelled) {
if (observedGestures.count == 0) return false;
for (UIGestureRecognizer *g in observedGestures) {
objc_setAssociatedObject(g, kGestureObserverKey, nil, OBJC_ASSOCIATION_RETAIN);
};
[observedGestures removeAllObjects];
return true;
} else {
return observedGestures.count > 0;
}
}
bool Click::handleObserve(NSString *keyPath, NSObject *object, NSDictionary *change, void *context) {
if ([keyPath isEqualToString:@"state"]) {
if (IS(UIGestureRecognizer, object)) {
auto gesture = (UIGestureRecognizer*)object;
auto state = [(UIGestureRecognizer *)object state];
auto handled = false;
if (state == UIGestureRecognizerStateEnded || state == UIGestureRecognizerStateFailed) {
if (state == UIGestureRecognizerStateEnded) {
handled = true;
}
objc_setAssociatedObject(object, kGestureObserverKey, nil, OBJC_ASSOCIATION_RETAIN);
}
[observedGestures removeObject:object];
}
return handled;
}
}
return false;
}
6.UIView的touches事件
以上排除手势事件外的即认为是UIView的touches事件,如果是UIView的子类也可以结合是否重写了父类的touches方法综合判断。