一、前言
在 iOS 全埋点采集中,cell 点击事件采集通常是指对 UITableViewCell 和 UICollectionViewCell 的用户点击行为进行采集。
cell 的点击是通过协议中的方法实现的,因此我们对 UITableView 的协议方法 - tableView:didSelectRowAtIndexPath: 和 UICollectionView 的协议方法 - collectionView:didSelectItemAtIndexPath: 进行 hook 即可达到采集的目的。
在 iOS 中对方法进行 hook,最简单的方式就是通过 Method Swizzling[1] 交换方法的 IMP,但这种方式无法完全适应 cell 点击事件采集,缺陷如下:
-
Method Swizzling 的代码需要确保只执行一次,但代理对象可能会被设置多次;
-
代理对象存在子类继承时,需要区分子类是否重写了要交换的方法;
-
诸如 RxSwift、Texture 等三方库使用消息转发时,则无法进行方法交换。
正是因为存在上述缺陷,我们不得不寻找其他 hook 方案。
二、方案
2.1 概述
Method Swizzling 交换方法是对整个类及其子类都生效的,那么是否存在一种 hook 方案只作用于当前的代理对象呢?答案是肯定的。
我们的采集方案是在获取代理对象后,基于该代理对象的类,创建一个独一无二的子类,该子类继承自原来的类。在子类中对 - tableView:didSelectRowAtIndexPath: 和 - collectionView:didSelectItemAtIndexPath: 方法进行重写,然后将代理对象的 isa 指针指向新建的子类,最后只需要在该代理对象释放的同时释放新建的子类即可。
这样就能够对 cell 点击事件进行采集,并且没有对点击方法进行交换,也就不存在 Method Swizzling 的相关问题。
2.2 原理
hook 原理如图 2-1 所示,在我们更改了代理对象的 isa 指针后,当用户点击 cell 时系统会优先调用我们子类重写的 - tableView:didSelectRowAtIndexPath: 或 - collectionView:didSelectItemAtIndexPath: 方法。此时可以进行事件采集,然后调用父类中的方法,完成消息的转发。
图 2-1 代理对象的 isa 指针变化
2.3 实现
2.3.1. 获取代理
由于获取代理对象仅需要 hook UITableView 和 UICollectionView 的 - setDelegate: 方法,要 hook 的类是已知的,因此我们可以使用 Method Swizzling:
SEL selector = NSSelectorFromString(@"sensorsdata_setDelegate:");
[UITableView sa_swizzleMethod:@selector(setDelegate:) withMethod:selector error:NULL];
[UICollectionView sa_swizzleMethod:@selector(setDelegate:) withMethod:selector error:NULL];
在 - sensorsdata_setDelegate: 方法中即可获取代理对象:
- (void)sensorsdata_setDelegate:(id <UITableViewDelegate>)delegate {
[self sensorsdata_setDelegate:delegate];
if (delegate == nil) {
return;
}
// 使用委托类去 hook 点击事件方法
[SADelegateProxy proxyWithDelegate:delegate];
}
2.3.2. 创建子类
动态创建子类,需要使用 runtime[2] 的 objc_allocateClassPair 接口,定义如下:
OBJC_EXPORT Class _Nullable
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name,