前言

​ 我们主要介绍如何实现控件点击事件($AppClick)的全埋点。在介绍如何实现之前,我们需要先了解一下,在 UIKit 框架下,处理点击或拖动事件的 Target-Action 设计模式。

一、 Target-Action

​ Target-Action,也叫目标-动作模式,即当某个事件发生的时候,调用特定对象的特定方法。

​ 比如,在 LoginViewController 页面,有一个按钮,点击按钮时,会调用 LoginViewController 里的 - loginBtnOnClick 方法,“特定对象”就是 Target,“特定方法”就是 Action。也即 Target 是 LoginViewController, Action 是 - loginBtnOnClick 方法。

Target-Action 设计模式主要包含两个部分:

  • Target 对象:接收消息的对象
  • Action 方法:用于表示需要调用的方法

​ Target 对象可以是任意类型的对象。但是在 iOS 应用程序中,通常情况下会是一个控制器,而触发事件的对象和 Target 对象一样,也可以是任意对象。例如,手势识别器 UIGestureRecognizer 就可以在识别到手势后,将消息发送给另一个对象。Target-Action 设计模式,最常见的应用场景还是在控件中。iOS 中的控件都是 UIControl 类或者其子类,当用户在操作这些控件时,会将消息发送到指定的对象(Target),而对应的 Action 方法必须符合以下几种形式之一 :

- (void)doSomething;
- (void)doSomething:(id)sender;
- (void)doSomething:(id)sender forEvent:(UIEvent *)event;
- (IBAction)doSomething;
- (IBAction)doSomething:(id)sender;
- (IBAction)doSomething:(id)sender forEvent:(UIEvent *)event;

​ 其中以 IBAction 作为返回值类型的形式,是为了让该方法能在 Interface Builder 中被看到;sender 参数就是触发事件的控件本身;第二个参数 event 是 UIEvent 的对象,封装了触摸事件的相关信息。我们可以通过代码或者 Interface Builder 为一个控件添加一个 Target 对象以及相对应的 Action 方法。

​ 若想使用代码方式添加 Target-Action(我们也会用 Target-Action 表示:一个 Target 对象以及相对应的 Action 方法),可以直接调用控件对象的如下方法:

- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;

我们也可以多次调用 - addTarget:action:forControlEvents: 方法给控件添加多个 Target-Action,即使多次调用- addTarget:action:forControlEvents: 添加相同的 Target 但是不同的 Action,也不会出现相互覆盖的问题。另外,在添加 Target-Action 的时候,Target 对象也可以为 nil(默认会先在 self 里查找 Action)。

当我们为一个控件添加 Target-Action 后,控件又是如何找到 Target 对象并执行对应的 Action 方法的呢?

在 UIControl 类中有一个方法:

- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;

如果控件被用户操作(比如点击),首先会调用这个方法,并将事件转发给应用程序的 UIApplication 对象。

同时,在 UIApplication 类中也有一个类似的实例方法:

- (BOOL)sendAction:(SEL)action to:(nullable id)target from:(nullable id)sender forEvent:(nullable UIEvent *)event;

如果 Target 对象不为 nil,应用程序会让该 Target 对象调用对应的 Action 方法响应事件;如果 Target 对象为 nil,应用程序会在响应者链中搜索定义了该方法的对象,然后执行 Action 方法。

基于 Target-Action 设计模式,我们有两种方案可以实现 $AppClick 事件的全埋点。

二、实现方案

​ 通过 Target-Action 执行模式可知,在执行 Action 方法之前,会先后通过控件和 UIApplication 对象发送事件相关的信息。因此,我们可以通过 Method Swizzling 交换 UIApplication 的 - sendAction:to:from:forEvent: 方法,然后在交换后的方法中触发 $AppClick 事件,并根据 target 和 sender 采集相关的属性,即可实现 $AppClick 事件的全埋点 。

​ 对于 UIApplication 类中的 - sendAction:to:from:forEvent: 方法,我们以给 UIButton 设置 action 为例,详细介绍一下。

[button addTarget:person action:@selector(btnAction) forControlEvents:UIControlEventTouchUpInside];

参数:

  • action:Action 方法对应的 selector,即示例中的 btnAction。
  • target:Target 对象,即示例中的 person。如果 Target 为 nil,应用程序会将消息发送给第一个响应者,并从第一个响应者沿着响应链向上发送消息,直到消息被处理为止。
  • sender:被用户点击或拖动的控件,即发送 Action 消息的对象,即示例中的 button。
  • event:UIEvent 对象,它封装了触发事件的相关信息。

返回值:

如果有 responder 对象处理了此消息,返回 YES,否则返回 NO。

2.1 实现步骤

​ 通过 Method Swizzling 交换 UIApplication 类中的 -sendAction:to:from:forEvent: 方法来实现 $AppClick 事件的全埋点。

第一步:创建 UIApplication 分类 UIApplication+SensorsData

第二步:实现交换方法 -sensorsdata_sendAction:to:from:forEvent:

z#import "SensorsAnalyticsSDK.h"

- (BOOL)sensorsdata_sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event{
// 触发 $AppClick 事件
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
[[SensorsAnalyticsSDK sharedInstance] track:@"$AppClick" properties:properties]; // 调用原有的实现 即 sendAction:to:from:forEvent:
return [self sensorsdata_sendAction:action to:target from:sender forEvent:event];
}

第三步:实现 load 类方法,并在类方法中实现 - sendAction:to:from:forEvent: 方法交换

#import "NSObject+SASwizzler.h"

+ (void)load {
[UIApplication sensorsdata_swizzleMethod:@selector(sendAction:to:from:forEvent:) withMethod:@selector(sensorsdata_sendAction:to:from:forEvent:)];
}

第四步:测试验证,在Demo 中添加 button 按钮,点击按钮

{
"event" : "$AppClick",
"time" : 1648696085563,
"propeerties" : {
"$model" : "x86_64",
"$manufacturer" : "Apple",
"$lib_version" : "1.0.0",
"$os" : "iOS",
"$app_version" : "1.0",
"$os_version" : "15.2",
"$lib" : "iOS"
}
}

2.2 优化 $AppClick 事件

一般情况下,对于一个控件的点击事件,我们至少还需要采集如下信息(属性):

  • 控件类型($element_type)
  • 控件上显示的文本($element_content)
  • 控件所属页面,即 UIViewController($screen_name)

基于目前的方案,我们来看如何实现采集以上三个属性。

1、获取控件类型

​ 获取控件类型相对比较简单,我们可以直接使用控件的 class 名称来代表当前控件的类型,比如可通过如下方式获取控件的 class 名称:

NSString *elementType = NSStringFromClass([sender class]);

2、获取显示属性

​ 需要根据特定的控件调用相应的方法。

第一步:在 UIView 的类别 SensorsData 中新增 sensorsdata_elementContent 属性。

@interface UIView (SensorsData)

@property (nonatomic, copy, readonly) NSString *sensorsdata_elementType;

@property (nonatomic, copy, readonly) NSString *sensorsdata_elementContent;

@end

- (NSString *)sensorsdata_elementContent {
return nil;
}

第二步:在 UIView+SensorsData 分类中新增 UIButton 的类别 SensorsData,并实现 -sensorsdata_elementContent 方法

#pragma mark - UIButton
@interface UIButton (SensorsData) @end
@implementation UIButton (SensorsData)

- (NSString *)sensorsdata_elementContent {
return self.titleLabel.text;
} @end

第三步:修改 SensorsAnalyticsSDK+Track 中 - trackAppClickWithView: properties: 方法

- (void)trackAppClickWithView:(UIView *)view properties:(nullable NSDictionary <NSString*, id> *)properties {
// 触发 $AppClick 事件
NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
// 获取控件类型
[eventProperties setValue:view.sensorsdata_elementType forKey:@"$element_type"];
// 获取控件文本
[eventProperties setValue:view.sensorsdata_elementContent forKey:@"$element_content"];
[eventProperties addEntriesFromDictionary:properties];
[[SensorsAnalyticsSDK sharedInstance] track:@"$AppClick" properties:eventProperties];
}

第四步:测试验证

{
"event" : "$AppClick",
"time" : 1648708284842,
"propeerties" : {
"$model" : "x86_64",
"$manufacturer" : "Apple",
"$element_type" : "UIButton",
"$lib_version" : "1.0.0",
"$os" : "iOS",
"$element_content" : "eeeeeee",
"$app_version" : "1.0",
"$os_version" : "15.2",
"$lib" : "iOS"
}
}

3、获取控件所属的界面

如何知道一个 UIView 所属哪个 UIViewController 呢?

这就需要借助 UIResponder 了!

大家都知道,UIResponder 类是 iOS 应用程序中专门用来响应用户操作事件的,比如:

  • Touch Events:即触摸事件
  • Motion Events:即运动事件
  • Remote Control Events:即远程控制事件

​ UIApplication、UIViewController、UIView 类都是 UIResponder 的子类,所以它们都具有响应以上事件的能力。另外,自定义的 UIView 和自定义视图控制器也都可以响应以上事件。在 iOS 应用程序中,UIApplication、UIViewController、UIView 类的对象也都是一个个响应者,这些响应者会形成一个响应者链。一个完整的响应者链传递规则(顺序)大概如下:UIView → UIViewController → RootViewController → Window → UIApplication → UIApplicationDelegate,可参考下图所示(此图来源于苹果官方网站) 。

​ 注意:对于 iOS 应用程序里实现了 UIApplicationDelegate 协议的类(通常为 AppDelegate),如果它是继承自 UIResponder,那么也会参与响应者链的传递;如果不是继承自 UIResponder(例如 NSObject),那么它就不会参与响应者链的传递。

​ 通过图可以知道,对于任意一个视图来说,都能通过响应者链找到它所在的视图控制器,也就是其所属的页面,从而可以达到获取它所属页面信息的目的。

第一步:新增 sensorsdata_viewController 属性

@interface UIView (SensorsData)

@property (nonatomic, copy, readonly) NSString *sensorsdata_elementType;

@property (nonatomic, copy, readonly) NSString *sensorsdata_elementContent;

@property (nonatomic, copy, readonly) NSString *sensorsdata_viewController;

@end

第二步:实现 实现 -sensorsdata_viewController 方法

- (NSString *)sensorsdata_viewController {
UIResponder *responder = self;
while ((responder = [responder nextResponder])) {
if ([responder isKindOfClass:[UIViewController class]]) {
return (UIViewController *)responder.class;
}
}
return nil;
}

第三步:修改 - trackAppClickWithView: properties: 方法

- (void)trackAppClickWithView:(UIView *)view properties:(nullable NSDictionary <NSString*, id> *)properties {
// 触发 $AppClick 事件
NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
// 获取控件类型
[eventProperties setValue:view.sensorsdata_elementType forKey:@"$element_type"];
// 获取控件文本
[eventProperties setValue:view.sensorsdata_elementContent forKey:@"$element_content"];
// 获取控件所在的控制器
UIViewController *vc = view.sensorsdata_viewController;
[eventProperties setValue:NSStringFromClass(vc.class) forKey:@"$screen_name"];
[eventProperties addEntriesFromDictionary:properties];
[[SensorsAnalyticsSDK sharedInstance] track:@"$AppClick" properties:eventProperties];
}

第四步:测试验证

{
"event" : "$AppClick",
"time" : 1648711998403,
"propeerties" : {
"$model" : "x86_64",
"$manufacturer" : "Apple",
"$element_type" : "UIButton",
"$lib_version" : "1.0.0",
"$os" : "iOS",
"$element_content" : "eeeeeee",
"$app_version" : "1.0",
"$screen_name" : "ViewController",
"$os_version" : "15.2",
"$lib" : "iOS"
}
}

三、遗留问题

如果,一个控件添加了多个 Target-Action,会导致多次触发 $AppClick 事件。

iOS全埋点解决方案-控件点击事件的更多相关文章

  1. iOS全埋点解决方案-UITableView和UICollectionView点击事件

    前言 在 $AppClick 事件采集中,还有两个比较特殊的控件: UITableView •UICollectionView 这两个控件的点击事件,一般指的是点击 UITableViewCell 和 ...

  2. winform自定义控件中其他遮挡控件点击事件

    自定义控件在其他窗口调用时,里面的lable阻挡了控件的点击事件 解决方法 自定义控件中lable的 点击事件 private void Lable1_Click(object sender, Eve ...

  3. Android实现监听控件点击事件

    Android实现监听控件点击事件 引言 这篇文章主要想写一下Android实现监听点击事件的几种方法,Activity和Fragment实现起来有些方法上会有些不同,这里也略做介绍. 最近一直在忙一 ...

  4. Android控件点击事件

    1. 介绍 本文介绍了Android控件的点击事件 Android控件点击(onClick)事件可以用如下三种方式来实现 2. 实现onClick方法 在layout的xml中指定onClick方法, ...

  5. iOS全埋点解决方案-应用退出和启动

    前言 ​ 通过应用程序退出事件,可以分析应用程序的平均使用时长:通过应用程序的启动事件,可以分析日活和新增.我们可以通过全埋点方式 SDK 实现应用程序的退出和启动事件. 一.全埋点的简介 ​ 目前. ...

  6. iOS全埋点解决方案-手势采集

    前言 ​ 随着科技以及业务的发展,手势的应用也越来越普及,因此对于数据采集,我们要考虑如果通过全埋点来实现手势的采集. 一.手势识别器 ​ 苹果为了降低开发者在手势事件处理方面的开发难度,定义了一个抽 ...

  7. iOS全埋点解决方案-界面预览事件

    前言 ​ 我们先了解 UIViewController 生命周期相关的内容和 iOS 的"黑魔法" Method Swizzling.然后再了解页面浏览事件($AppViewScr ...

  8. iOS全埋点解决方案-时间相关

    前言 ​ 我们使用"事件模型( Event 模型)"来描述用户的各种行为,事件模型包括事件( Event )和用户( User )两个核心实体.我们在描述用户行为时,往往只需要描述 ...

  9. iOS全埋点解决方案-采集奔溃

    前言 ​ 采集应用程序奔溃信息,主要分为以下两种场景: ​ NSException 异常 ​ Unix 信号异常 一.NSException 异常 ​ NSException 异常是 Objectiv ...

随机推荐

  1. 《手把手教你》系列基础篇(七十七)-java+ selenium自动化测试-框架设计基础-TestNG依赖测试- 上篇(详解教程)

    1.简介 今天主要是讲解和分享:TestNG中一个类中有多个测试方法的时候,多个测试方法的执行顺序或者依赖关系的问题.如果不用dependsOnMethods,testNG会自动根据@Test方法名称 ...

  2. python 之 matplotlib 练习

    import numpy as npimport matplotlib.pyplot as plt x = np.linspace(0,10,1000)# 自变量y = np.sin(x) + 1 # ...

  3. java反射获取类的成员函数,成员变量,构造函数

    package com.imooc.reflect;import javax.sound.midi.Soundbank;import java.lang.reflect.Constructor;imp ...

  4. 《前端运维》一、Linux基础--06Shell流程控制

    这章我们来学习下流程控制,简单来说就是逻辑判断和循环的写法.并不复杂,我们来简单地看下. 1.if语句 shell的if语句有两种写法,一种是shell脚本式的,一种是命令式的. if conditi ...

  5. 二维码生成工具——QRCode

    下载QRCode的源代码:https://github.com/davidshimjs/qrcodejs 引入项目中:<script type="text/javascript&quo ...

  6. Parrot Linux安装教程

    镜像下载.域名解析.时间同步请点击 阿里云开源镜像站 一.Parrot Linux介绍 Parrot 是一个由开发人员和安全专家组成的全球社区,他们共同构建一个共享的工具框架,使他们的工作更轻松.标准 ...

  7. MySQL二进制binlog日志说明以及利用binlog日志恢复数据

    MySQL的binlog日志对于mysql数据库来说是十分重要的.在数据丢失的紧急情况下,我们往往会想到用binlog日志功能进行数据恢复(定时全量备份+binlog日志恢复增量数据部分). 一.关于 ...

  8. 爬虫系列之Scrapy框架

    一 scrapy框架简介 1 介绍 (1) 什么是Scrapy? Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架,非常出名,非常强悍.所谓的框架就是一个已经被集成了各种功能(高性能 ...

  9. Java基础——方法的调用

    Java基础--方法的调用     总结: 1. 在同一个类中-- 对于静态方法,其他的静态和非静态方法都可以直接通过"方法名"或者"类名.方法名"调用它. 对 ...

  10. json知识点总结(二)--JSONObject详解

    JSONObject只是一种数据结构,可以理解为JSON格式的数据结构(key-value 结构),可以使用put方法给json对象添加元素.JSONObject可以很方便的转换成字符串,也可以很方便 ...