ios无痕埋点_iOS进阶补完计划--打点上报、无痕埋点

最近研习了美团等大厂的一些埋点方案。

还要感谢大神《xuhaoranLeo》的指点。(既然大神没空写博客、但我可以代劳哈)。

本文的宗旨是尽量全面、精简、满足我能想到尽量多的埋点需求。

主要通过以下这些方面来谈谈中埋点那些事:

打点/上报的大概流程

日志记录类型

日志应该带有的数据

打点的具体方式

何时上报

具体实现(iOS)

打点/上报的大概流程

打点:当发生需要收集的行为/状态时、将其记录在日记中。

上报:选择合适的时机将日志上报。

日志记录类型

根据业务需要大致可以具有以下类型

页面/产品曝光

用户点击

性能打点(数据库操作效率、APP运行卡顿)

网络监控

日志应该带有的数据

一切分析时用得到的数据例子:

行为(点击、浏览)、用户(uid)、业务信息(gid、gtype)等。

关键业务的性能监听(其实性能打点我比较推荐单独进行、毕竟这是开发关心的、产品分析并不需要):

网络请求失败率、错误码

数据层操作耗时、App卡顿堆栈

打点的具体方式

代码埋点:具体业务代码处、手动添加埋点代码。比如衡量图片上传、数据解析、OI操作的时间等

声明埋点: 通过将事件标识、业务字段作为属性添加在响应控件上。简化代码埋点的代码量。

无痕埋点:获取全部操作、通过plist文件、决定需要上报的指定操作《美团:Mixpanel》

无埋点:上报所有操作、由服务器筛选《GrowingIO》

具体实现:

由于每个项目的需求不同、具体实现也不一样。

这里只大概理顺思路。

周期内记录:

既然是统一上报、就需要在上报之前将本次周期中所有的指定操作记录下来。

每次操作中。由一个指定的模型(json)进行存储。

而整个周期中。我们采用一个单例、单例中有一个数组对单次模型进行存储。

/*

* 数据存储模型

*/

@interface KTBehaviorData : NSObject

@property (nonatomic, strong) NSString *op_type; // 1点击事件 2页面事件 3IO操作

@property (nonatomic, strong) NSString *page_code; // 页面Id

@property (nonatomic, strong) NSString *event_code; // 事件Id

@property (nonatomic, strong) NSDictionary *object_dic; // 内容Id

@property (nonatomic, strong) NSString *op_time; // 点击事件操作时间

@property (nonatomic, strong) NSString *start_time; // 页面事件开始时间

@property (nonatomic, strong) NSString *end_time; // 页面事件结束时间

@end

@interface KTBehaviorUpLoadData : NSObject

@property (nonatomic, strong) NSString *app_type; //

@property (nonatomic, strong) NSString *app_version;

@property (nonatomic, strong) NSString *os_type; // 1苹果iOS

@property (nonatomic, strong) NSString *os_version; // 系统版本

@property (nonatomic, strong) NSString *device_id; // 设备id

@property (nonatomic, strong) NSString *user_id; // 用户id

@property (nonatomic, strong) NSString *login_account; // 用户账号

@property (nonatomic, strong) NSString *screen; // 屏幕分辨率...

@property (nonatomic, strong) NSMutableArray *datas;

@end

存储&&上报:

在APP结束时归档存储、APP启动时上传给服务器、上传失败则将归档数据重新写入单例追加。

写入

@implementation KTBehaviorDataManager

+ (void)load {

//杀死程序 (但当程序位于后台呗杀死不执行)

__block id observer1 = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {

NSLog(@"杀死程序---将数据写入本地");

//将数据写入本地

[[KTBehaviorDataManager sharedManager] writeBehaviorData];

[[NSNotificationCenter defaultCenter] removeObserver:observer1];

}];

//程序切换至后台

__block id observer2 = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {

NSLog(@"程序切换至后台---将数据写入本地");

//将数据写入本地

[[KTBehaviorDataManager sharedManager] writeBehaviorData];

[[NSNotificationCenter defaultCenter] removeObserver:observer2];

}];

}

上传

@implementation KTBehaviorDataUpLoader

+ (void)load {

//程序启动、上报记录

__block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {

[KTBehaviorDataUpLoader upLoadData];

[[NSNotificationCenter defaultCenter] removeObserver:observer];

}];

}

打点:

打点的方式有很多、但本质上都一样。只是打点的代码书写位置不同而已。

这里有一点需要注意一下:

在将捕获的信息写入manager的时候、记得加上安全保障。因为整个app里有很多地方都将会对manager进行操作、虽然出现资源抢夺的问题不大、但是并不代表永远不会。

#import "KTBehaviorDataManager.h"

- (void)pushKTBehaviorDataWithModel:(KTBehaviorData *)model {

//线程锁、保证数据完整性

@synchronized(self) {

[self.data.datas addObject:model];

}

}

代码埋点

看着多、但如果你把代码封装一下。就会发现少很多了

- (void)submitBtnClick {

KTBehaviorData *data = [[KTBehaviorData alloc] init];

data.op_type = @"2";

data.page_code = @"push";

data.event_code = @"submitBtnClick";

data.object_id = @{@"title":@"xx",@"content":@"xx"};

data.op_time = [NSDate getCurrentTimeStamp];

[[KTBehaviorDataManager sharedManager] pushKTBehaviorDataWithModel:data];

}

当然、你可以把打点的方法抽离一下、更精简一些而不使用Model。不过到了方法内部之后、都一样。

[[KTBehaviorDataManager sharedManager] pushKTBehaviorDataWithPageId:@"xxx" objectId:@"xxx"];

稍微高级点、一个记录图片上传速度的埋点。

- (void)upLoadPic {

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

// Do the work in background

KTBehaviorData * data = [KTBehaviorData new];

data.op_type = @"3";

data.page_code = @"ViewController";

data.event_code = @"upLoadPic";

data.start_time =[KTBehaviorData getNowTimeTimestamp];

//图片上传

[NSThread sleepForTimeInterval:5];

data.end_time = [KTBehaviorData getNowTimeTimestamp];

[[KTBehaviorDataManager sharedManager] pushKTBehaviorDataWithModel:data];

NSLog(@"页面IO埋点----%@",[data dicValue]);

});

}

这样、就完成了一次提交按钮被点击的记录。包括时间、控制器、事件、参数等。

只要你的模型结构足够健壮、我们完全可以用一个模型记录APP内的各种事件。

结合刚才说的写入&&上报。大概这样的效果

上报

声明埋点

通过runtime为控件动态添加属性。

然后在创建控件时为属性赋值。

KTBehaviorData *parameter = [[KTBehaviorData alloc] init];

parameter.bid = @"bid";

parameter.lab = @{@"poi_id":@"1"};

button.kt_clickParams = parameter;

然后在事件发生时进行记录。

无痕埋点

简而言之、有两点。

替换方法:通过swizzle对事件进行hook。

这里、我提供两种方式。

1、通过类别Hook原生方法:网上最普遍的方式。对event事件、table代理、页面生命周期等方法进行Hook、但是无法直接对业务参数进行捕获。

解决方案可以通过对NSObject扩展出一个打点专用结构体来获取、但是本质上需要污染了业务代码。

2、hook指定Class中的指定方法:然后在指定方法中通过获取class指定属性值的方式捕获参数。这要感谢《xuhaoranLeo》提供的方案。

在下文中我会对两种方式进行说明并且举例。

筛选记录:通过plist文件。通过文件名:pageId、方法名:enevtId等方式、自动为模型参数赋值。

替换方法:

通过类别Hook原生方法

现在还在这个阶段大家对swizzle应用都比较频繁了、没什么必要解释太多。直接贴代码吧

举个例子

页面进出、停留时间:

@implementation UIViewController (KTHook)

+ (void)load {

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

SEL originalSelector1 = @selector(viewWillAppear:);

SEL swizzledSelector1 = @selector(kt_viewWillAppear:);

[KTHook swizzlingInClass:[self class] originalSelector:originalSelector1 swizzledSelector:swizzledSelector1];

SEL originalSelector2 = @selector(viewWillDisappear:);

SEL swizzledSelector2 = @selector(kt_viewWillDisappear:);

[KTHook swizzlingInClass:[self class] originalSelector:originalSelector2 swizzledSelector:swizzledSelector2];

});

}

#pragma mark - Method Swizzling

- (void)kt_viewWillAppear:(BOOL)animated

{

NSLog(@"进入");

[[KTBehaviorDataManager sharedManager]pushKTBehaviorDataWithPageId:NSStringFromClass([self class]) time:[KTBehaviorData getNowTimeTimestamp]];

[self kt_viewWillAppear:animated];

}

- (void)kt_viewWillDisappear:(BOOL)animated

{

NSLog(@"离开");

[[KTBehaviorDataManager sharedManager]pushKTBehaviorDataWithPageId:NSStringFromClass([self class]) time:[KTBehaviorData getNowTimeTimestamp]];

[self kt_viewWillDisappear:animated];

}

页面停留时间

同理、我们通过对UIControl的Event事件、UITableView代理等进行hook、进行无痕埋点。

具体方式网上有很多、千篇一律。我就不写了、因为不符合我想要获取页面参数的需求、贴出两个教学帖想要这么实现的可以自取。

《iOS 打点方案设计》、《iOS动态性(二)可复用而且高度解耦的用户统计埋点实现》

hook指定Class中的指定方法

思路就是上面写的。实现的代码也不难、hook过SDK文件的童鞋应该都知道。这里为了方便、我们用了一个封装好的工具。《Aspects》

@implementation NSObject (KTAspectsHook)

+ (void)load {

__block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {

[self setupBehaviorObj];

[[NSNotificationCenter defaultCenter] removeObserver:observer];

}];

}

#pragma mark - private method

//hook所有需要打点的对象方法

- (void)setupBehaviorObj {

Class clazz = NSClassFromString(@"ViewController");

//具体事件方法

SEL selector = NSSelectorFromString(@"upLoadPic");

[clazz aspect_hookSelector:selector withOptions:AspectPositionBefore usingBlock:^(id aspectInfo) {

NSLog(@"ViewController中upLoadPic方法被调用、参数:aaa==%@",[[aspectInfo instance] valueForKey:@"aaa"]);

} error:NULL];

}

@end

打印:

2018-01-24 14:43:21.419519+0800 kTBehaviorDemo[4587:363679] ViewController中upLoadPic方法被调用、参数:aaa==我是参数aaa

这样、调用者、调用方法、参数。三大要素就都已经可以获取到了。

但如何进行批量埋点?

筛选记录

用plist、这个网上也很多帖子。之前提的两个帖子也都提及了。

上段代码可以修改如下:

+ (void)setupBehaviorObj {

NSDictionary *behaviorPlist = [self getBehaviorEvents];

for (NSString * className in behaviorPlist) {

//需要hook的Class

Class clazz = NSClassFromString(className);

//对应Class需要hook的方法名

NSDictionary *events = behaviorPlist[className];

if (events[kBehaviorEvents]) {

//事件数组

for (NSDictionary *event in events[kBehaviorEvents]) {

//具体事件方法

SEL selector = NSSelectorFromString(event[kBehaviorEventSelectorName]);

[clazz aspect_hookSelector:selector withOptions:AspectPositionBefore usingBlock:^(id aspectInfo) {

//获取参数

NSMutableDictionary * parameterDic = [NSMutableDictionary new];

if (event[kBehaviorParameter]) {

NSDictionary * dic = [NSObject properties_apsWithObj:[aspectInfo instance]];

for (NSString * parameterStr in event[kBehaviorParameter]) {

if ([dic valueForKey:parameterStr]) {

[parameterDic setValue:[dic valueForKey:parameterStr] forKey:parameterStr];

}

}

}

KTBehaviorData * data = [KTBehaviorData new];

data.op_time = [KTBehaviorData getNowTimeTimestamp];

data.event_code = event[kBehaviorEventId];

data.object_dic = parameterDic;

data.page_code = event[kBehaviorPageId];

data.op_type = event[kBehaviorType];

[[KTBehaviorDataManager sharedManager]pushKTBehaviorDataWithModel:data];

} error:NULL];

}

}

}

}

控制台信息:

这样、只要你的plist足够健壮。确实可以做到几乎完全无痕的埋点。

结束语:

年前比较忙、但开了帖总要填完。所以可能有些错别字和语法坑。

每个项目的需求不同、情况也不同。所以这只是个demo、希望能为大家提供一个思路、并没有封装成一个SDK。

不同的情况、可以用不同的打点方案。所谓无痕、并不一定是最好的、太暴力了。

还有就是当项目很庞大的时候、进行hook操作、会不会影响性能。如果影响了、有没有什么改进的方式。

如果你有什么好的想法、或者是项目中有什么更好的方案。还望指教。

补充:

经测。

当导入方法为300时、肉眼无感。

当导入方法为3000时、约1s。

当导入方法为30000时、约15s。

由于在+load中加载、这段时间会算入app启动白屏的时间内。

最后

本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果不吝赐教小弟更加感谢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值