我们先了解 UIViewController 生命周期相关的内容和 iOS 的“黑魔法” Method Swizzling。然后再了解页面浏览事件($AppViewScreen)全埋点的实现原理
众所周知,每一个 UIViewController 都管理着一个由多个视图组成的树形结构,其中根视图保存在 UIViewController 的 view 属性中。UIViewController 会懒加载它所管理的视图集,直到第一次访问 view 属性时,才会去加载或者创建 UIViewController 的视图集。
有以下几种常用的方式加载或者创建 UIViewController 的视图集:
以上这些方法,最终都会创建出合适的根视图并保存在 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,顾名思义,就是交换两个方法的实现。简单的来说,就是利用 Objective-C runtime 的动态绑定特性,把一个方法的实现与另一个方法的实现进行交换。
在 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 Swizzling 交换方法,其实就是修改了 objc_method 结构体中的 method_imp,也即改变了 method_name 和 method_imp 的映射关系,如下图所示。

那我们如何改变 method_name 和 method_imp 的映射关系呢?在 Objective-C 的 runtime 中,提供了很多非常方便使用的函数,让我们可以很简单的就能实现 Method Swizzling,即改变 method_name 和 method_imp 的映射关系,从而达到交换方法的效果。
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);
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);
IMP method_getImplementation
// 返回方法实现的指针
// 目标方法
OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
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);
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);
第一步 创建 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 事件,来实现界面预览的全埋点。
第一步:在 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"
}
}
问题:在应用程序启动过程中,会触发多余的 $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 事件。
按照目前的方案实现 $AppViewScreen 事件的全埋点,会有2个问题:
应用程序热启动是(从后台恢复),第一个界面没有触发 $AppViewScreen 事件。原因是这个界面没有再次执行 - viewDidAppear: 方法
要求 UIViewController 的子类不重写 -viewDidAppear:方法,一旦重写,必须调用[super viewDidAppear:animated], 否则不会触发 $AppViewScreen 事件。原因是直接交换了 UIViewController 的 - viewDidAppear: 方法。
在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',
这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下
我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的
是否有简单的方法来更改默认ISO格式(yyyy-mm-dd)的ActiveAdmin日期过滤器显示格式? 最佳答案 您可以像这样为日期选择器提供额外的选项,而不是覆盖js:=f.input:my_date,as::datepicker,datepicker_options:{dateFormat:"mm/dd/yy"} 关于ruby-on-rails-事件管理员日期过滤器日期格式自定义,我们在StackOverflow上找到一个类似的问题: https://s
相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声
首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有, 也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加
1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里
深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal
我正在尝试将以下SQL查询转换为ActiveRecord,它正在融化我的大脑。deletefromtablewhereid有什么想法吗?我想做的是限制表中的行数。所以,我想删除少于最近10个条目的所有内容。编辑:通过结合以下几个答案找到了解决方案。Temperature.where('id这给我留下了最新的10个条目。 最佳答案 从您的SQL来看,您似乎想要从表中删除前10条记录。我相信到目前为止的大多数答案都会如此。这里有两个额外的选择:基于MurifoX的版本:Table.where(:id=>Table.order(:id).
print"Enteryourpassword:"pass=STDIN.noecho(&:gets)puts"Yourpasswordis#{pass}!"输出:Enteryourpassword:input.rb:2:in`':undefinedmethod`noecho'for#>(NoMethodError) 最佳答案 一开始require'io/console'后来的Ruby1.9.3 关于ruby-为什么不能使用类IO的实例方法noecho?,我们在StackOverflow上