流霞地

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方法综合判断。