iOS 无痕埋点解决方案——事件 ID 篇(2)

当已经确定了如何通过 AOP 在业务中插入埋点代码后,即可开始采集埋点数据,然后进行上报。

构建的埋点数据可以分为两部分:

  1. 构建一个 Key-Value 数据结构存放此次埋点的数据
  2. 构建一个唯一 ID 用于标识事件,并使用 event_code 作为 key 存放步骤 1 中的数据中

本文主要描述如何生成第二点中的唯一 ID

在下文中,event code 就是事件唯一 ID

要求

用户操作事件埋点,一般用于分析用户行为、用户习惯、某个按钮的日点击量或者时间段点击量等等。为了更便捷的分析这些数据,就会对事件 ID 有一定的要求。

在每次无痕埋点数据采集过程中,都会取到一大堆乱七八糟的数据,而为了准确标识某个用户操作事件,我们必须要有统一的事件唯一 ID 生成方案,而这个方案必须满足以下条件:

  • 同一个界面,同一个按钮,使用一个 ID

不因为当前不同业务数据环境导致 ID 变化,这样有助于大数据分析。

如:使用按钮标题拼接唯一 ID(比如某个按钮标题是当前位置到某个位置的距离,这个距离会根据用户实际位置的变化而变化),这会导致同一个功能,不同的标题产生多个 ID,并且对应同一个事件。

  • 不同界面,或者同一界面的按钮,不能使用相同的 ID

如:使用按钮类名拼接唯一 ID,如果按钮被复用,就有可能导致两个事件的 ID 凑巧相同。

总之,我们要做到事件和 ID 是一一对应的关系,而不是一对多,也不是多对一。

现在,基于上述条件生成唯一 ID。

生成 delegate 埋点的唯一 ID

delegate 埋点一般为下面两种:

  1. -[UITableView tableView:didSelectRowAtIndex:]
  2. -[UICollectionViewDelegate collectionView:didSelectItemAtIndexPath:]

我们 hook 了 -[UITableView setDelegate:] 方法,创建了一个 Proxy 对象作为中间对象,伪装了实际的 delegate,并拦截了对应的点击回调方法。所以我们采集的数据可以在 -[UITableView setDelegate:] 中获取初始数据,以及在 -[Proxy tableView:didSelectRowAtIndex:] 中采集实际点击数据。

采集数据

1. setDelegate: 采集初始化数据

在设置 delegate 时,我们可以拿到 UITableView 的类名(如果被继承了的话),已经业务实际的 delegate 对象。由于这两个数据在 -[Proxy tableView:didSelectRowAtIndex:] 方法中也能拿到,所以此处不会记录这两个数据

2. tableView:didSelectRowAtIndex: 采集实际点击数据

当用户实际点击某一个 Cell 时,会触发此方法。我们可以在此方法中获取非常丰富的数据:

  • self(Proxy 对象)
  • 参数 tableView
    • tableView 可以使用 UIResponse 获取对应的 ViewController
  • 参数 indexPath
    • tableView + indexPath 可以获取对应点击的 Cell 对象
  • self.delegate(业务实际的 delegate)
  • ...

所以在此方法中我们可以至少拿到 6 个数据,接下来进行分析,使用这 6 个数据拼接事件 ID。

拼接事件

首先明确,当我们拿到某个对象时,代表我们可以拿到两个数据:1. 该对象的地址,2. 该对象的类名。由于地址的随机性很大,为了保证上文中的条件,所以不会使用该对象的地址来拼接事件。

Proxy 对象

Proxy 对象是由埋点 SDK 生成的,所以类名一成不变,故 Proxy 对象不能拿来拼接事件 ID。

TableView 对象

TableView 大部分是 UITableView,由于基本不会去继承他,所以不会使用 TableView 的类名。

ViewController 对象

ViewController 一般为自定义的,所以类名也是根据业务实际情况来定,故 ViewController 的类名可以作为唯一 ID 的一部分。

IndexPath 对象

NSIndexPath 是标识行数,由于 TableView 行数可变,不确定。故如果使用 IndexPath 里的数据拼接 ID,将会产生大量不同的名字。所以 IndexPath 对象不能用。但是为了准确标识用户点击了哪一行的 cell,可以将 IndexPath 当做别的参数来上报。

Cell 对象

大部分的 Cell 都是自定义的,所以类名也是根据视图样式来定,故 Cell 的类名可以作为唯一 ID 的一部分。

综述,我们可以拼接 ViewController 和 Cell 来拼接 ID。但是如果一个 VC 中出现了两个 TableView(如外卖 app 的菜单页面),或者近似的两个 TableView。故再加一个 TableView.delegate.className。

最终事件 ID 如下:

VCClassName#DelegateClassName#CellClassName

@implementation MyTableViewDelegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    //  转发给业务
    if ([self.delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) {
        [self.delegate tableView:tableView didSelectRowAtIndexPath:indexPath];
    }
    
    //埋点
    NSString *event_code = ({
        UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
        NSString *viewController = ({
            UIResponder *responder = tableView;
            while (responder) {
                responder = responder.nextResponder;
                if ([responder isKindOfClass:[UIViewController class]]) {
                    break;
                } else if ([responder isKindeOfClass:[UIWindow class]]) {
                    break;
                }
            }
            NSStringFromClass([responder class]);
        });
        NSString *targetName = NSStringFromClass([self.delegate class]);
        NSString *cellName = NSStringFromClass([cell class]);
        [NSString stringWithFormat:@"%@#%@#%@", viewController, targetName, cellName];
    });
        
    [Tracker trackEvent:event_code];
}

@end

举例:

UIViewController#UIViewController#UITableViewCell

UIViewController#MenuView#MenuCell

生成 Target-Action 埋点的唯一 ID

Target-Action 是手势和 UIControl 的回调,一般使用如下代码

  1. -[UIControl addTarget:action:events:]
  2. -[UIGestureRecognizer initWithTarget:action:]

我们 hook 了 -[UIControl addTarget:action:events:] 方法,创建了一个 Action 对象作为附属对象,和实际的 target 一同添加到 UIControl 中。当 UIControl 触发了事件,就会同时向业务对象和 Action 对象发送消息,从而产生埋点。故我们可以在 -[UIControl addTarget:action:events:] 方法中获取到 target、action、event。还能从 -[Action action:] 方法中获取实时埋点数据。

采集

-[UIControl addTarget:action:events:]

在此方法中,我们可以获取到 UIControl 类名,target 对象,action 方法名,events 事件名。由于后三个数据在下面的方法中无法获取,所以会记录后三个数据到 Action 对象中,供 Action 对象在触发下面的方法时获取对应数据:

//  In UIControl+Hook.m

- (void)hook_addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)events {

    // Call origin method
    [self hook_addTarget:target action:action forControlEvents:events];
    
    //  Create Action object
    MyTargetAction *action = [[MyTargetAction alloc] init];
    action.targetName = NSStringFromClass([target class]);
    action.action = NSStringFromSelector(action);
    action.events = events;

    // Add Action object
    [self hook_addTarget:action action:@selector(action:) forControlEvents:events];
}

-[Action action:]

此方法是实际埋点执行的方法,由于此方法只能获取 self(Action 对象)和 sender(UIControl 对象),故实际埋点数据还是依赖前一个方法临时保存的数据。

此时我们可以在方法中构成埋点数据。

  • self(Action 对象)
  • sender(UIControl 对象)
    • VC(可以根据 UIControl 获取所在 VC)
  • self.targetName(target 类名)
  • self.action(action 方法名)
  • self.events(events 值)

拼接事件

Action 对象

此对象是 SDK 内部对象,无任何信息

sender

控件对象,大部分按钮不会继承,所以也不会有信息。

self.targetName

响应者类名,此类一般为 VC 的类名,或者某个 View 的类名,故此信息可用于拼接。

self.action

响应方法名,于前一个相同,但不同事件一般会有不同方法回调,所以方法名也可以作为唯一事件 ID。

self.events

事件类型,不同控件不同事件,但按钮基本为 UIControlEventsTouchUpInside,如果要区分不同控件则可以加入,本文只考虑按钮情况。故不加入此信息

最终事件 ID 如下:

VCClassName#TargetClassName#ActionName

//  In MyTargetAction.m

- (void)action:(UIControl *)sender {
    NSString *event_code = ({
        NSString *viewController = ({
            UIResponder *responder = sender;
            while (responder) {
                responder = responder.nextResponder;
                if ([responder isKindOfClass:[UIViewController class]]) {
                    break;
                } else if ([responder isKindeOfClass:[UIWindow class]]) {
                    break;
                }
            }
            NSStringFromClass([responder class]);
        });
        [NSString stringWithFormat:@"%@#%@#%@", viewController, self.targetName, self.actionName];
    });
        
    [Tracker trackEvent:event_code];
}

举例:

UIViewController#UIViewController#onClick:

UIViewController#MenuItemCell#onClickAdd:

总结

我们尽可能采集了事件数据,拼接成了事件唯一 ID。事件唯一 ID 的拼接可以根据实际的埋点需求来定,并非一成不变。

以上就是 iOS 端无痕埋点解决方案事件 ID 部分的实现。

在接下来的篇幅中,我将介绍如何在埋点中携带 UI 控件上获取不到的业务数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容