前言

​ 我们先了解 UIViewController 生命周期相关的内容和 iOS 的“黑魔法” Method Swizzling。然后再了解页面浏览事件($AppViewScreen)全埋点的实现原理

一、UIViewController 生命周期

​ 众所周知,每一个 UIViewController 都管理着一个由多个视图组成的树形结构,其中根视图保存在 UIViewController 的 view 属性中。UIViewController 会懒加载它所管理的视图集,直到第一次访问 view 属性时,才会去加载或者创建 UIViewController 的视图集。

有以下几种常用的方式加载或者创建 UIViewController 的视图集:

  • 使用 Storyboard
  • 使用 Nib 文件
  • 使用代码,即重写 - loadView

​ 以上这些方法,最终都会创建出合适的根视图并保存在 UIViewController 的 view 属性中,这是 UIViewController 生命周期的第一步。当 UIViewController 的根视图需要展示在页面上时,会调用 - viewDidLoad 方法。在这个方法中,我们可以做一些对象初始化相关的工作。

​ 需要注意的是:此时,视图的 bounds 还没有确定。对于使用代码创建视图,- viewDidLoad 方法会在 -loadView 方法调用结束之后运行;如果使用的是 Stroyboard 或者 Nib 文件创建视图,- viewDidLoad 方法则会在 - awakeFromNib 方法之后调用。

​ 当 UIViewController 的视图在屏幕上的显示状态发生变化时,UIViewController 会自动回调一些方法,确保子类能够响应到这些变化。如下图所示,它展示了 UIViewController 在不同的显示状态时会回调不同的方法。

​ 在 UIViewController 被销毁之前,还会回调 - dealloc 方法,我们一般通过重写这个方法来主动释放不能被 ARC 自动释放的资源。

​ 我们现在对 UIViewController 的整个生命周期有了一些基本了解。那么,我们如何去实现页面浏览事件( $AppViewScreen 事件)的全埋点呢?

​ 通过 UIViewController 的生命周期可知,当执行到 - viewDidAppear: 方法时,表示视图已经在屏幕上渲染完成,也即页面已经显示出来了,正等待用户进行下一步操作。因此,- viewDidAppear: 方法就是我们触发页面浏览事件的最佳时机。如果想要实现页面浏览事件的全埋点,需要使用 iOS 的“黑魔法” Method Swizzling 相关的技术。

二、Method Swizzling 黑魔法

​ Method Swizzling,顾名思义,就是交换两个方法的实现。简单的来说,就是利用 Objective-C runtime 的动态绑定特性,把一个方法的实现与另一个方法的实现进行交换。

2.1 Method Swizzling 基础

​ 在 Objective-C 的 runtime 中,一个类是用一个名为 objc_class 的结构体表示的,它的定义如下:

struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif } OBJC2_UNAVAILABLE;

​ 在上面的结构体中,虽然有很多字段在 OBJC2 中已经废弃了(OBJC2_UNAVAILABLE),但是了解这个结构体还是有助于我们理解 Method Swizzling 的底层原理。我们从上述结构体中可以发现,有一个 objc_method_list 指针,它保存着当前类的所有方法列表。同时,objc_method_list 也是一个结构体,它的定义如下:

struct objc_method_list {
struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}

​ 在上面的结构体中,有一个 objc_method 字段,我们再来看看 objc_method 这个结构体:

struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}

​ 从上面的结构体中可以看出,一个方法由下面三个部分组成:

  • method_name:方法名
  • method_types:方法类型
  • method_imp:方法实现

使用 Method Swizzling 交换方法,其实就是修改了 objc_method 结构体中的 method_imp,也即改变了 method_name 和 method_imp 的映射关系,如下图所示。

那我们如何改变 method_name 和 method_imp 的映射关系呢?在 Objective-C 的 runtime 中,提供了很多非常方便使用的函数,让我们可以很简单的就能实现 Method Swizzling,即改变 method_name 和 method_imp 的映射关系,从而达到交换方法的效果。

2.2 实现 Method Swizzling 的相关函数

  1. Method class_getInstanceMethod

    // 返回目标类 aClass、方法名为 aSelector 的实例方法
    // aClass :目标类
    // aSelector: 方法名
    OBJC_EXPORT Method _Nullable
    class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
  2. BOOL class_addMethod

    // 给目标类 aClass 添加一个新的方法,同时包括方法的实现
    // aClass: 目标类
    // aSelector: 要添加方法的方法名
    // imp: 要添加方法的方法实现
    // types: 方法实现的编码类型
    OBJC_EXPORT BOOL
    class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
    const char * _Nullable types)
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
  3. IMP method_getImplementation

    // 返回方法实现的指针
    // 目标方法
    OBJC_EXPORT IMP _Nonnull
    method_getImplementation(Method _Nonnull m)
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
  4. IMP class_replaceMethod

    // 替换目标类 aClass 的 aSelector 方法指针
    // aClass: 目标类
    // aSelector: 目前方法的方法名
    // imp:新方法的方法实现
    // types: 方法实现的编码类型
    OBJC_EXPORT IMP _Nullable
    class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
    const char * _Nullable types)
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
  5. void method_exchangeImplementations

    // 交换2个方法的实现指针
    // m1: 交换方法1
    // m2: 交换方法2
    OBJC_EXPORT void
    method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

2.3 实现 Method Swizzling

第一步 创建 NSObject 的分类 NSObject+SASwizzler

第二步 在 NSObject+SASwizzler.h 声明方法交换方法

/// 交换方法名为 originalSEL 和方法名为 alternateSEL 两个方法实现
/// @param originalSEL 原始的方法名称
/// @param alternateSEL 要交换的方法名称
+ (BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL;

第三步 在 NSObject+SASwizzler.m 实现方法的交换

+ (BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL {

    // 获取原始方法
Method originalMethod = class_getInstanceMethod(self, originalSEL);
// 当原始的方法不存在时,返回NO,表示 Swizzler 失败
if (!originalMethod) {
return NO;
} // 获取要交换的方法
Method alternateMethod = class_getInstanceMethod(self, alternateSEL);
// 当交换的方法不存在时,返回NO,表示 Swizzler 失败
if (!alternateMethod) {
return NO;
} // 交换两个方法的实现
method_exchangeImplementations(originalMethod, alternateMethod);
return YES;
}

三、实现界面预览事件全埋点

​ 利用方法交换,来交换 UIViewController 的 -viewDidAppear: 方法,然后在方法交换中触发 $AppViewScreen 事件,来实现界面预览的全埋点。

3.1 实现步骤

第一步:在 SensorsSDK 项目中,新增一个 UIViewController 类别 UIViewController+SensorsData

第二步:在 UIViewController+SensorsData.m 类别新增交换方法 - sensorsdata_viewDidAppear:,然后再交换方法中调用原始方法,并触发 $AppViewScreen 事件

- (void)sensorsdata_viewDidAppear:(BOOL)animated {
// 调用原始方法, 即 - viewDidAppear
[self sensorsdata_viewDidAppear:animated]; // 触发 $AppViewScreen 事件
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
[properties setValue:NSStringFromClass([self class]) forKey:@"$screen_name"];
[properties setValue:self.navigationItem.title forKey:@"$title"];
[[SensorsAnalyticsSDK sharedInstance] track:@"$AppViewScreen" properties:properties];
}

第三步: 在 UIViewController+SensorsData.m 中重写 + load 类方法,并在 + load 类方法中调用 NSObject+SASwizzler 的类方法交换

+ (void)load {
[UIViewController sensorsdata_swizzleMethod:@selector(viewDidAppear:) withMethod:@selector(sensorsdata_viewDidAppear:)];
}

第四步 : 测试验证

{
"event" : "$AppViewScreen",
"time" : 1648626597682,
"propeerties" : {
"$model" : "x86_64",
"$manufacturer" : "Apple",
"$lib_version" : "1.0.0",
"$os" : "iOS",
"$app_version" : "1.0",
"$screen_name" : "ViewController",
"$os_version" : "15.2",
"$lib" : "iOS"
}
}

3.2 优化

问题:在应用程序启动过程中,会触发多余的 $AppViewScreen ,我们可以引入黑名单的机制,即在黑名单里配置那些 UIViewController 及子类不触发 $AppViewScreen 事件。

第一步 创建一个 sensorsdata_black_list.plist 文件,并把 root 类型改成 Array,该文件就是黑名单文件,然后在黑名单文件中添加控制器,如图所示:

第二步 在 UIViewController+SensorsData.m 文件中新增 - shouldTrackAppViewScreen 方法,用来判断当前控制器是否在黑名单中。

static NSString * const kSensorsDataBlackListFileName = @"sensorsdata_black_list";

// 黑名单
- (BOOL)shouldTrackAppViewScreen {
static NSSet *blackList = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *path = [[NSBundle bundleForClass:SensorsAnalyticsSDK.class] pathForResource:kSensorsDataBlackListFileName ofType:@"plist"];
NSArray *classNames = [NSArray arrayWithContentsOfFile:path];
NSMutableSet *set = [NSMutableSet setWithCapacity:classNames.count];
for (NSString *className in classNames) {
[set addObject:NSClassFromString(className)];
}
blackList = [set copy];
});
for (Class cla in blackList) {
if ([self isKindOfClass:cla]) {
return NO;
}
}
return YES;
}

第三步 在触发 $AppViewScreen 事件之前,判断是否在黑名单中

- (void)sensorsdata_viewDidAppear:(BOOL)animated {
// 调用原始方法, 即 - viewDidAppear
[self sensorsdata_viewDidAppear:animated]; // 触发 $AppViewScreen 事件
if ([self shouldTrackAppViewScreen]) {
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
[properties setValue:NSStringFromClass([self class]) forKey:@"$screen_name"];
[properties setValue:self.navigationItem.title forKey:@"$title"];
[[SensorsAnalyticsSDK sharedInstance] track:@"$AppViewScreen" properties:properties];
}
}

第四步 测试验证

​ 运行Demo,所添加到黑名单中的 controller 不会发送 $AppViewScreen 事件。

3.4 遗留问题

​ 按照目前的方案实现 $AppViewScreen 事件的全埋点,会有2个问题:

应用程序热启动是(从后台恢复),第一个界面没有触发 $AppViewScreen 事件。原因是这个界面没有再次执行 - viewDidAppear: 方法

要求 UIViewController 的子类不重写 -viewDidAppear:方法,一旦重写,必须调用[super viewDidAppear:animated], 否则不会触发 $AppViewScreen 事件。原因是直接交换了 UIViewController 的 - viewDidAppear: 方法。

iOS全埋点解决方案-界面预览事件的更多相关文章

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

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

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

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

  3. iOS全埋点解决方案-控件点击事件

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

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

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

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

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

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

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

  7. iOS全埋点解决方案-APP和H5打通

    前言 ​ 所谓的 APP 和 H5 打通,是指 H5 集成 JavaScript 数据采集 SDK 后,H5 触发的事件不直接同步给服务器,而是先发给 APP 端的数据采集 SDK,经过 APP 端数 ...

  8. iOS全埋点解决方案-数据存储

    前言 ​ SDK 需要把事件数据缓冲到本地,待符合一定策略再去同步数据. 一.数据存储策略 ​ 在 iOS 应用程序中,从 "数据缓冲在哪里" 这个纬度看,缓冲一般分两种类型. 内 ...

  9. WPF的路由事件、冒泡事件、隧道事件(预览事件)

    本文摘要: 1:什么是路由事件: 2:中断事件路由: 3:自定义路由事件: 4:为什么需要自定义路由事件: 5:什么是冒泡事件和预览事件(隧道事件): 1:什么是路由事件 WPF中的事件为路由事件,所 ...

随机推荐

  1. yalmip安装

    1,将yalmip解压,在matlab中添加路径. 2,yalmiptest测试是否安装成功.

  2. Weblogic补丁升级问题

    转至:https://blog.csdn.net/weixin_44659716/article/details/105132466 一.版本信息1)中间件版本 Weblogic10.3.6.02) ...

  3. Codeforces Round #750 (Div. 2) E. Pchelyonok and Segments

    传送门 题目大意: 给一个序列,可以在这个序列中从左至右选若干个段,第i段的长度为i,对于任意的段i,段内元素和S[i]<S[i+1],求在该序列中最多可以选出几段. 思路:设dp[i][j]为 ...

  4. DeepFaceLab:NVML Shared Library Not Found 解决办法

    解决办法如下 A.出现这个提示的原因是安装后的Nvidia显卡目录C:\Program Files\NVIDIA Corporation\NVSMI不存在,造成的. B.搜索文件Nvidia-SMI. ...

  5. MongoDB数据库的下载安装及配置方法

    MongoDB安装与配置步骤 MongoDB数据库之安装篇 # 1 下载MongoDB数据库 1.打开浏览器,登录"https://www.mongodb.com/try/download/ ...

  6. Tableau绘制漏斗图、甘特图、瀑布图、镶边面积图、阴影坡度图

    Tableau绘制漏斗图.甘特图.瀑布图.镶边面积图.阴影坡度图 本文首发于博客冰山一树Sankey,去博客浏览效果更好.直接右上角搜索该标题即可 一. 漏斗图 数据源 1.1 分色直条漏斗图 (1) ...

  7. JZ-009-变态跳台阶

    变态跳台阶 题目描述 一只青蛙一次可以跳上1级台阶,也可以跳上2级--它也可以跳上n级.求该青蛙跳上一个n级的台阶总共有多少种跳法. 题目链接: 变态跳台阶 代码 /** * 标题:变态跳台阶 * 题 ...

  8. C++_Leecode1 两数之和

    一.题目介绍 1.题目描述 ->给定一个整数数组nums和一个整数目标值target,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标. ->你可以假设每种输入只会对应一个答 ...

  9. 【python】kNN基础算法--推荐系统(辅助研究)

    # -*- coding:utf-8 -*- # import numpy as np #import numpy 和from numpy import *是不一样的 # # # import num ...

  10. Cloud Computing Chapter3 (云计算第三章)

    本篇文章是对课程大型软件系统设计与体系结构(双语)[又名:云计算]的课堂内容总结,适用于大连交通大学. Cloud Computing Chapter3 Understanding Cloud Com ...