先附上参考的文章:
iOS无埋点数据SDK实践之路
iOS无埋点数据统计实践
该方案的统计功能?
- APP进入前台
- 按钮点击 (点击次数、按钮名称、点击方法)
- cell点击
- 界面停留时长
- APP进入后台
如何实现无埋点
利用运行时机制,将类原生方法替换成用户自定义的方法,相当于强行在原本调用栈中插入一个方法,我们在其中插入一段统计代码即可。
如何替换方法:Method Swizzling
函数的调用涉及到3个重要的点:Class、SEL、IMP,Calss作为类型,Method由SEL和IMP组成。我们通过交换Method的IMP达到替换被调用函数的目的。
1 2 3 4 5 6 7 8 9 10 11
| 核心方法: - (void)sel_exchangeFirstSel:(SEL)sel1 secondSel:(SEL)sel2 { [self sel_exchangeClass:[self class] FirstSel:sel1 secondSel:sel2]; }
- (void)sel_exchangeClass:(Class)Class FirstSel:(SEL)sel1 secondSel:(SEL)sel2 { Method firstMethod = class_getInstanceMethod(Class, sel1); Method secondMethod = class_getInstanceMethod(Class, sel2); method_exchangeImplementations(firstMethod, secondMethod);
}
|
注: 这里主要交换的方法
系统类 |
类方法 |
UIControl |
sendAction:to:forEvent |
UIGestureRecognizer |
addTarget:action: |
initWithTarget:action: |
|
UIView |
addGestureRecognizer |
UITableView |
setDelegate |
tableView:didSelectRowAtIndexPath: |
|
NSNotificationCenter |
postNotification |
postNotificationName:object:userInfo: |
|
UIViewController |
viewDidAppear: |
viewDidDisappear: |
|
swizzling函数的时机
+(void)load函数在你动态加载或者静态引用了这个类的时候,该函数就会被执行,它并不需要你显示的去创建一个类后才会执行,同时它只会执行一次,几乎是完美的swizzling时机。
1 2 3 4 5 6 7 8 9 10 11
| 举例 UIControl
+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //创建新的sendAction:to:forEvent:方法 [self sel_exchangeFirstSel:@selector(sendAction:to:forEvent:) secondSel:@selector(ch_sendAction:to:forEvent:)]; });
}
|
加入数据统计代码
在swizzling成功后,我们在其中加入统计代码。为了保证响应链的完整,我们还需要调用替换过的方法,让事件传递下去,不去影响系统的处理。
- 这里采用
内联函数
将该方法包起来使用 用于过滤黑名单(不参与统计的控制器)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| /** 黑名单 不需要追踪的控制器 */ #import <UIKit/UIKit.h> UIKIT_STATIC_INLINE BOOL kShouldTrackClass(Class aClass){ static NSSet *blacklistedClasses = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // NSString *path = [[NSBundle mainBundle] pathForResource:@"BlackListed" ofType:@"plist"]; // NSArray *blacklistedClassNames = [NSArray arrayWithContentsOfFile:path]; NSArray *blacklistedClassNames = [LogDAO sharedInstance].blackListArray; NSMutableSet *transformedClasses = [NSMutableSet setWithCapacity:blacklistedClassNames.count]; for (NSString *className in blacklistedClassNames) { [transformedClasses addObject:NSClassFromString(className)]; } blacklistedClasses = [transformedClasses copy]; }); return ![blacklistedClasses containsObject:aClass]; }
|
统计方式:操作数据库
- 数据库地址:
/var/mobile/Containers/Data/Application/······/Library/Caches/TheOnlineTax/Database/log.sqlite
- 数据库表名为
info_log
- 表中的参数 也是模型中的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| typedef NS_ENUM(NSUInteger, LogType) { LogTypeNone = 0, // 未知 LogTypeLaunch, // app 启动或前台 LogTypeButtonClick, // 按钮点击 LogTypeCellClick, // cell点击 LogTypeVCRemainTime,// 界面停留时间 LogTypeTerminated, // app 终止或后台 };
@property (nonatomic, copy) NSString *vc_id;// 控制器名称 @property (nonatomic) NSTimeInterval remainTime; //停留时间 @property (nonatomic, copy) NSString *btn_id;// 按钮名称 @property (nonatomic, copy) NSString *functionName;// 方法名称 @property (nonatomic, assign) NSInteger num;// 点击次数 @property (nonatomic) NSTimeInterval lastTime; //最后执行时间 @property (nonatomic, assign) LogType logType; //记录类型
|
- 插入之前会判断是否存在 若存在只更新表
- 按钮次数 是通过 vc_id 和 btn_id 进行查表 累加所得
- 具体逻辑可看LogDAO文件
数据上传服务器
因没有后台存储这些统计出来的数据,暂时使用后端云Bmob存储
1 2 3
| 目前设计 (上传成功数据库的表里数据清空) 1.是APP进入后台自动触发触发 2.用户退出登录操作触发
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| NSString *userID = @"0";//必须默认0 if ([AppDelegate trackGetUserID].length>0) { userID = [AppDelegate trackGetUserID]; } NSDictionary *dic = @{ //模型数组转成字典数组 @"list" : [LogDTO mj_keyValuesArrayWithObjectArray:[[LogDAO sharedInstance] getAllData]], //应用名称 @"productName" : kDisplayName, //bundleId @"productID" : [[NSBundle mainBundle] bundleIdentifier], //应用版本号 @"version" : kVersion, //设备版本 @"osVersion" : [NSString stringWithFormat:@"%.1f",[DeviceAndSystemTool systemVersion]], //设备机型 @"osDeviceType" : KStringIsEmpty([DeviceAndSystemTool getDeviceName]), //网络类型 @"networkType" : KStringIsEmpty([DeviceAndSystemTool networkTypeName]), //运营商 @"isp" : KStringIsEmpty([DeviceAndSystemTool wsd_telephonyNetworkInfo]), //定位(省市区) @"gps" : KStringIsEmpty(self.strLocationInfo) };
|
- APP启动会先获取服务器的数据 若有只在原有基础上更新不会新增一条数据(改动是TrackData和updateAT字段)会根据UserID字段区分数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| /** 新增一条数据 */
BmobObject *trackBmob = [BmobObject objectWithClassName:@"TrackAction"]; [trackBmob setObject:dic forKey:@"TrackData"]; [trackBmob setObject:[AppDelegate trackGetUserID] forKey:@"UserID"]; [trackBmob saveInBackgroundWithResultBlock:^(BOOL isSuccessful, NSError *error) { //进行操作 if (isSuccessful) { if (handler) { handler(YES,nil); } }else{ if (handler) { handler(NO,error); } } }];
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| /** 更新一条数据 */
//创建查表对象 BmobQuery *bquery = [BmobQuery queryWithClassName:@"TrackAction"]; //设置查询中该字段是有值的结果 [bquery whereKeyExists:@"UserID"]; //设置查询中该字段 值是否相等的结果 [bquery whereKey:@"UserID" equalTo:[AppDelegate trackGetUserID]]; //查询 [bquery findObjectsInBackgroundWithBlock:^(NSArray *array, NSError *error) { if (error){ //进行错误处理 }else{ if (array) { BmobObject *obj = array[0];
NSString *userID = @"0";//必须默认0 if ([AppDelegate trackGetUserID].length>0) { userID = [AppDelegate trackGetUserID]; } NSDictionary *dic = @{ //模型数组转成字典数组 @"list" : [LogDTO mj_keyValuesArrayWithObjectArray:[[LogDAO sharedInstance] getAllData]], //userid @"userid" : KStringIsEmpty(userID), //应用名称 @"productName" : kDisplayName, //bundleId @"productID" : [[NSBundle mainBundle] bundleIdentifier], //应用版本号 @"version" : kVersion, //设备版本 @"osVersion" : [NSString stringWithFormat:@"%.1f",[DeviceAndSystemTool systemVersion]], //设备机型 @"osDeviceType" : KStringIsEmpty([DeviceAndSystemTool getDeviceName]), //网络类型 @"networkType" : KStringIsEmpty([DeviceAndSystemTool networkTypeName]), //运营商 @"isp" : KStringIsEmpty([DeviceAndSystemTool wsd_telephonyNetworkInfo]), //定位(省市区) @"gps" : KStringIsEmpty(self.strLocationInfo) };
BmobObject *obj1 = [BmobObject objectWithoutDataWithClassName:obj.className objectId:obj.objectId]; //设置cheatMode为YES [obj1 setObject:dic forKey:@"TrackData"]; //异步更新数据 [obj1 updateInBackground]; } } }];
|
引用方法:
目前没有集成到pod上 只能手动导入 这是相关文件:GitHub
1 2 3 4 5 6 7 8 9
| 【 使用说明 】 1.需要依赖 pod 'MJExtension' 和 pod 'AFNetworking' 请确保项目中已有 2.将文件拖入项目中 3.需在pch文件中引用 #import "TrackAction.h" 4.需在AppDelegate.m文件中 a.调用 createTablesNeeded 创建数据库 b.重写 trackGetUserID 配置userID c.重写 trackServiceURL 配置上传服务器的地址 5.可添加不需要统计的控制器在BlackListed.plist
|
更新部分【重要】
引用方法
- 手动导入 这是相关文件:GitHub
- 引用svn私有库
pod 'ASTrackAction','1.0.0'
使用说明
1 2 3 4
| 【 使用说明 】 1.依赖 pod 'MJExtension' 和 pod 'AFNetworking' 2.需在pch文件中引用 #import "TrackAction.h" 3.需在AppDelegate里的方法实现 配置track信息的方法 在LogDAO类中 (不开启埋点功能 注释即可)
|
上传的拼接数据
修改为
1.先判断是开启埋点功能
2.再判断是否自定义服务器地址 若有则AFN请求 若无则上传至Bmob后端云
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| if (!self.isOpenTracker) { return; }
NSString *userID = @"0";//必须默认0 if (self.userId.length>0) { userID = self.userId; } NSDictionary *dic = @{ //模型数组转成字典数组 @"list" : [LogDTO mj_keyValuesArrayWithObjectArray:[[LogDAO sharedInstance] getAllData]], //userid @"userid" : KStringIsEmpty(userID), //应用名称 @"productName" : kDisplayName, //bundleId @"productID" : [[NSBundle mainBundle] bundleIdentifier], //应用版本号 @"version" : kVersion, //设备版本 @"osVersion" : [NSString stringWithFormat:@"%.1f",[DeviceAndSystemTool systemVersion]], //设备机型 @"osDeviceType" : KStringIsEmpty([DeviceAndSystemTool getDeviceName]), //网络类型 @"networkType" : KStringIsEmpty([DeviceAndSystemTool networkTypeName]), //运营商 @"isp" : KStringIsEmpty([DeviceAndSystemTool wsd_telephonyNetworkInfo]), //定位(省市区) @"gps" : KStringIsEmpty(self.strLocationInfo) };
if (!ISEMPTY(self.serviceURL)) { #pragma mark - AFN
[TrackActionServiceManager postParameter:dic Success:^(NSURLSessionDataTask *operation, id responseObject) {
if (handler) { handler(YES,nil); }
} Failure:^(NSURLSessionDataTask *operation, NSError *error) {
if (handler) { handler(NO,error); } }]; }else{
#pragma mark - 后端云SDK
BmobObject *trackBmob = [BmobObject objectWithClassName:@"TrackAction"]; [trackBmob setObject:dic forKey:@"TrackData"]; [trackBmob setObject:self.userId forKey:@"UserID"]; [trackBmob saveInBackgroundWithResultBlock:^(BOOL isSuccessful, NSError *error) { //进行操作 if (isSuccessful) { if (handler) { handler(YES,nil); } }else{ if (handler) { handler(NO,error); } } }]; }
|