mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
docs: Data uploader SDK && Readme
This commit is contained in:
@@ -7,7 +7,17 @@
|
||||
|
||||
|
||||
|
||||
## 0x01. 埋点手段
|
||||
## 前置说明
|
||||
|
||||
看到我在下面的代码段,有些命名风格、简写、分类、方法的命名等,我简单做个说明。
|
||||
|
||||
- 埋点 SDK 叫 `UserAnalysis`,我们规定类的命名一般用 SDK 的名字缩写,当前情况下缩写为 `UA`
|
||||
- 给 Category 命名,规则为 `类名 + SDK 前缀缩写的小写格式 + 下划线 + 驼峰命名格式的功能描述`。比如给 NSDate 增加一个获取毫秒时间戳的分类,那么类名为 `NSDate+ua_TimeStamp`
|
||||
- 给 Category 的方法命名,规则为 `SDK 前缀缩写的小写格式 + 下划线 + 驼峰命名格式的功能描述`。比如给 NSDate 增加一个根据当前时间获取毫秒时间戳的方法,那么方法名为 `+ (long long)ua_currentTimestamp;`
|
||||
|
||||
|
||||
|
||||
## 一. 埋点手段
|
||||
|
||||
业界中对于代码埋点主要有3种主流的方案:代码手动埋点、可视化埋点、无痕埋点。简单说说这几种埋点方案。
|
||||
|
||||
@@ -17,7 +27,7 @@
|
||||
|
||||
|
||||
|
||||
## 0x02. 技术选型
|
||||
## 二. 技术选型
|
||||
|
||||
|
||||
|
||||
@@ -39,11 +49,11 @@
|
||||
|
||||
**可视化埋点的出现,是为解决代码埋点流程复杂、成本高、新开发的页面(H5、或者服务端下发的 json 去生成相应页面)不能及时拥有埋点能力**
|
||||
|
||||
前端在「埋点编辑模式」下,以“可视化”的方式去配置、绑定关键业务模块的路径到前端可以唯一确定到view的xpath过程。
|
||||
前端在「埋点编辑模式」下,以“可视化”的方式去配置、绑定关键业务模块的路径到前端可以唯一确定到 view 的xpath 过程。
|
||||
|
||||
用户每次操作的控件,都生成一个 **xpath** 字符串,然后通过接口将 xpath 字符串(view在前端系统中的唯一定位。以 iOS 为例,App名称、控制器名称、一层层view、同类型view的序号:“GoodCell.21.RetailTableView.GoodsViewController.*baoApp”)到真正的业务模块(“宝App-商城控制器-分销商品列表-第21个商品被点击了”)的映射关系上传到服务端。xpath 具体是什么在下文会有介绍。
|
||||
用户每次操作的控件,都生成一个 **xpath** 字符串,然后通过接口将 xpath 字符串(view在前端系统中的唯一定位。以 iOS 为例,App名称、控制器名称、一层层view、同类型view的序号:`GoodCell.21.RetailTableView.GoodsViewController.***App` 到真正的业务模块(“某App-商城控制器-分销商品列表-第21个商品被点击了”)的映射关系上传到服务端。xpath 具体是什么在下文会有介绍。
|
||||
|
||||
之后操作 App 就生成对应的 xpath 和埋点数据(开发者通过技术手段将从服务端获取的关键数据塞到前端的 UI 控件上。 iOS 端为例, UIView 的 accessibilityIdentifier 属性可以设置我们从服务端获取的埋点数据)上传到服务端。
|
||||
之后操作 App 就生成对应的 xpath 和埋点数据(开发者通过技术手段将从服务端获取的关键数据塞到前端的 UI 控件上。 iOS 端为例, UIView 的 `accessibilityIdentifier` 属性可以设置我们从服务端获取的埋点数据)上传到服务端。
|
||||
|
||||
优点:数据量相对准确、后期数据分析成本低
|
||||
|
||||
@@ -67,13 +77,13 @@
|
||||
|
||||
怎么说呢?对于关键的业务开发结束上线后、通过可视化方案(类似于一个界面,想想看 Dreamwaver,你在界面上拖拖控件,简单编辑下就可以生成对应的 HTML 代码)点击一下绑定对应关系到服务端。
|
||||
|
||||
那么这个对应关系是什么?我们需要唯一定位一个前端元素,那么想到的办法就是不管 Native 和 Web 前端,控件或者元素来说就是一个树形层级,DOM tree 或者 UI tree,所以我们通过技术手段定位到这个元素,以 Native iOS 为例子,假如点击商品详情页的加入购物车按钮会根据 UI 层级结构生成一个唯一标识 “addCartButton.GoodsViewController.GoodsView.*BaoApp” 。但是用户在使用 App 的时候,上传的是这串东西的 MD5到服务端。
|
||||
那么这个对应关系是什么?我们需要唯一定位一个前端元素,那么想到的办法就是不管 Native 和 Web 前端,控件或者元素来说就是一个树形层级,DOM tree 或者 UI tree,所以我们通过技术手段定位到这个元素,以 Native iOS 为例子,假如点击商品详情页的加入购物车按钮会根据 UI 层级结构生成一个唯一标识 `addCartButton.GoodsViewController.GoodsView.*BaoApp`。但是用户在使用 App 的时候,上传的是这串东西的 MD5 到服务端。
|
||||
|
||||
这么做有2个原因:服务端数据库存储这串很长的东西不是很好;埋点数据被劫持的话直接看到明文不太好。所以 MD5 再上传。
|
||||
|
||||
|
||||
|
||||
## 0x03. 操刀就干
|
||||
## 三. 操刀就干
|
||||
|
||||
一言以蔽之就是:**AOP -> Event Collector -> Event Cache -> Data Upload**
|
||||
- AOP:通过 runtime hook 的能力做到提供合适的时机去生成点击事件的数据
|
||||
@@ -95,45 +105,26 @@
|
||||
|
||||
以 iOS 为例。我们会想到 **AOP(Aspect Oriented Programming)**面向切面编程思想。动态地在函数调用前后插入相应的代码,在 Objective-C 中我们可以利用 Runtime 特性,用 **Method Swizzling** 来 hook 相应的函数
|
||||
|
||||
为了给所有类方便地 hook,我们可以给 NSObject 添加个 Category,名字叫做 NSObject+MethodSwizzling
|
||||
为了给所有类方便地 hook,我们可以给 NSObject 添加个 Category,名字叫做 `NSObject+u a_MethodSwizzling`
|
||||
|
||||
```objective-c
|
||||
#pragma mark - public Method
|
||||
+ (void)lbp_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
|
||||
+ (void)ua_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
|
||||
{
|
||||
class_swizzleInstanceMethod(self, originalSelector, swizzledSelector);
|
||||
ua_swizzleInstanceMethod(self, originalSelector, swizzledSelector);
|
||||
}
|
||||
|
||||
+ (void)lbp_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
|
||||
+ (void)ua_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
|
||||
{
|
||||
//类方法实际上是储存在类对象的类(即元类)中,即类方法相当于元类的实例方法,所以只需要把元类传入,其他逻辑和交互实例方法一样。
|
||||
Class class2 = object_getClass(self);
|
||||
class_swizzleInstanceMethod(class2, originalSelector, swizzledSelector);
|
||||
ua_swizzleInstanceMethod(class2, originalSelector, swizzledSelector);
|
||||
}
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL)
|
||||
void ua_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL)
|
||||
{
|
||||
/*
|
||||
Class class = [self class];
|
||||
//原有方法
|
||||
Method originalMethod = class_getInstanceMethod(class, originalSelector);
|
||||
//替换原有方法的新方法
|
||||
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
|
||||
//先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况
|
||||
BOOL didAddMethod = class_addMethod(class,originalSelector,
|
||||
method_getImplementation(swizzledMethod),
|
||||
method_getTypeEncoding(swizzledMethod));
|
||||
if (didAddMethod) {//添加成功:表明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
|
||||
class_replaceMethod(class,swizzledSelector,
|
||||
method_getImplementation(originalMethod),
|
||||
method_getTypeEncoding(originalMethod));
|
||||
} else {//添加失败:表明源SEL已经有IMP,直接将两个SEL的IMP交换即可
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod);
|
||||
}
|
||||
*/
|
||||
|
||||
Method originMethod = class_getInstanceMethod(class, originalSEL);
|
||||
Method replaceMethod = class_getInstanceMethod(class, replacementSEL);
|
||||
|
||||
@@ -166,10 +157,10 @@ void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSE
|
||||
|
||||
|
||||
```objective-c
|
||||
static char *lbp_viewController_open_time = "lbp_viewController_open_time";
|
||||
static char *lbp_viewController_close_time = "lbp_viewController_close_time";
|
||||
static char *ua_viewController_open_time = "ua_viewController_open_time";
|
||||
static char *ua_viewController_close_time = "ua_viewController_close_time";
|
||||
|
||||
@implementation UIViewController (lbpka)
|
||||
@implementation UIViewController (uaka)
|
||||
|
||||
// load 方法里面添加 dispatch_once 是为了防止手动调用 load 方法。
|
||||
+ (void)load
|
||||
@@ -177,36 +168,16 @@ static char *lbp_viewController_close_time = "lbp_viewController_close_time";
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@autoreleasepool {
|
||||
[[self class] lbp_swizzleMethod:@selector(viewWillAppear:) swizzledSelector:@selector(lbp_viewWillAppear:)];
|
||||
[[self class] lbp_swizzleMethod:@selector(viewWillDisappear:) swizzledSelector:@selector(lbp_viewWillDisappear:)];
|
||||
[[self class] ua_swizzleMethod:@selector(viewWillAppear:) swizzledSelector:@selector(ua_viewWillAppear:)];
|
||||
[[self class] ua_swizzleMethod:@selector(viewWillDisappear:) swizzledSelector:@selector(ua_viewWillDisappear:)];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - add prop
|
||||
#pragma mark - private method
|
||||
|
||||
- (void)setOpenTime:(NSDate *)openTime
|
||||
{
|
||||
objc_setAssociatedObject(self,&lbp_viewController_open_time, openTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
- (NSDate *)getOpenTime
|
||||
{
|
||||
return objc_getAssociatedObject(self, &lbp_viewController_open_time);
|
||||
}
|
||||
|
||||
- (void)setCloseTime:(NSDate *)closeTime
|
||||
{
|
||||
objc_setAssociatedObject(self,&lbp_viewController_close_time, closeTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
- (NSDate *)getCloseTime
|
||||
{
|
||||
return objc_getAssociatedObject(self, &lbp_viewController_close_time);
|
||||
}
|
||||
|
||||
- (void)lbp_viewWillAppear:(BOOL)animated
|
||||
- (void)ua_viewWillAppear:(BOOL)animated
|
||||
{
|
||||
NSString *className = NSStringFromClass([self class]);
|
||||
NSString *refer = [NSString string];
|
||||
@@ -227,21 +198,19 @@ static char *lbp_viewController_close_time = "lbp_viewController_close_time";
|
||||
[UserTrackDataCenter openPage:[self getPageUrl:className] fromPage:refer];
|
||||
}
|
||||
|
||||
[self lbp_viewWillAppear:animated];
|
||||
[self ua_viewWillAppear:animated];
|
||||
}
|
||||
|
||||
- (void)lbp_viewWillDisappear:(BOOL)animated
|
||||
- (void)ua_viewWillDisappear:(BOOL)animated
|
||||
{
|
||||
NSString *className = NSStringFromClass([self class]);
|
||||
if ([self getPageUrl:className]) {
|
||||
[self setCloseTime:[NSDate dateWithTimeIntervalSinceNow:0]];
|
||||
[UserTrackDataCenter leavePage:[self getPageUrl:className] spendTime:[self p_calculationTimeSpend]];
|
||||
}
|
||||
[self lbp_viewWillDisappear:animated];
|
||||
[self ua_viewWillDisappear:animated];
|
||||
}
|
||||
|
||||
#pragma mark - private method
|
||||
|
||||
- (NSString *)p_calculationTimeSpend
|
||||
{
|
||||
|
||||
@@ -259,6 +228,30 @@ static char *lbp_viewController_close_time = "lbp_viewController_close_time";
|
||||
return [NSString stringWithFormat:@"%d",second];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - getter && setter
|
||||
|
||||
- (void)setOpenTime:(NSDate *)openTime
|
||||
{
|
||||
objc_setAssociatedObject(self,&ua_viewController_open_time, openTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
- (NSDate *)getOpenTime
|
||||
{
|
||||
return objc_getAssociatedObject(self, &ua_viewController_open_time);
|
||||
}
|
||||
|
||||
- (void)setCloseTime:(NSDate *)closeTime
|
||||
{
|
||||
objc_setAssociatedObject(self,&ua_viewController_close_time, closeTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
- (NSDate *)getCloseTime
|
||||
{
|
||||
return objc_getAssociatedObject(self, &ua_viewController_close_time);
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
@@ -295,19 +288,17 @@ static char *lbp_viewController_close_time = "lbp_viewController_close_time";
|
||||
|
||||
### 5. 同类型的 view 的唯一定位问题
|
||||
|
||||
有个问题,比如我们点击的元素是 UITableViewCell,那么它虽然可以定位到类似于这个标示 xxApp.GoodsViewController.GoodsTableView.GoodsCell,同类型的 Cell 有多个,所以单凭借这个字符串是没有办法定位具体的那个 Cell 被点击了。
|
||||
有个问题,比如我们点击的元素是 UITableViewCell,那么它虽然可以定位到类似于这个标示 `xxApp.GoodsViewController.GoodsTableView.GoodsCell`,同类型的 Cell 有多个,所以单凭借这个字符串是没有办法定位具体的那个 Cell 被点击了。
|
||||
|
||||
当然有解决方案啦。
|
||||
|
||||
- 找出当前元素在父层同类型元素中的索引。根据当前的元素遍历当前元素的父级元素的子元素,如果出现相同的元素,则需要判断当前元素是所在层级的第几个元素
|
||||
|
||||
对当前的控件元素的父视图的全部子视图进行遍历,如果存在和当前的控件元素同类型的控件,那么需要判断当前控件元素在同类型控件元素中的所处的位置,那么则可以唯一定位。举例:GoodsCell-3.GoodsTableView.GoodsViewController.xxApp
|
||||
|
||||
|
||||
对当前的控件元素的父视图的全部子视图进行遍历,如果存在和当前的控件元素同类型的控件,那么需要判断当前控件元素在同类型控件元素中的所处的位置,那么则可以唯一定位。举例:`GoodsCell-3.GoodsTableView.GoodsViewController.xxApp`
|
||||
|
||||
```Objective-c
|
||||
//UIResponder分类
|
||||
- (NSString *)lbp_identifierKa
|
||||
//UIResponder分类
|
||||
- (NSString *)ua_identifierKa
|
||||
{
|
||||
// if (self.xq_identifier_ka == nil) {
|
||||
if ([self isKindOfClass:[UIView class]]) {
|
||||
@@ -316,7 +307,7 @@ static char *lbp_viewController_close_time = "lbp_viewController_close_time";
|
||||
NSMutableString *str = [NSMutableString string];
|
||||
//特殊的 加减购 因为带有spm但是要区分加减 需要带TreeNode
|
||||
NSString *className = [NSString stringWithUTF8String:object_getClassName(view)];
|
||||
if (!view.accessibilityIdentifier || [className isEqualToString:@"lbpButton"]) {
|
||||
if (!view.accessibilityIdentifier || [className isEqualToString:@"uaButton"]) {
|
||||
[str appendString:sameViewTreeNode];
|
||||
[str appendString:@","];
|
||||
}
|
||||
@@ -382,7 +373,7 @@ static char *lbp_viewController_close_time = "lbp_viewController_close_time";
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -391,7 +382,9 @@ static char *lbp_viewController_close_time = "lbp_viewController_close_time";
|
||||
|
||||
### 6. 同类型的view,但是点击的意义却不一样。如何唯一标识?
|
||||
|
||||
问题5说明的是在一个界面上有多个不同的 view,他们的类型是同一种(CycleBannerView,但是数据源不一样,那么当数据源长度大于1的时候会轮播,下面会展示 UIPageControl。如果数据源是1个,那么就不会轮播和展示 UIPageControl)。情况6是同一种类型的 View,但是根据展示的内容不一样,点击的意义也不一样。也就是运营需要去知道用户到底点击的是哪一个。如下图所示,「立即抢购」和「分享赚佣金」是同一种类型的 View,但是点击意义不一样,需要我们需要唯一标识出来。之前的方法通过 **“viewPath 配合同类型的 view 去加索引值“** 的方式还是没有办法唯一标识出来。所以想到一个方案,给 NSObject 添加一个分类,在分类里面添加一个协议。让需要复用但需要唯一标识的 view 去实现协议方法,因为是给 NSObject 分类添加的协议,所以 view 不需要去指定遵循。
|
||||
问题5说明的是在一个界面上有多个不同的 view,他们的类型是同一种(`CycleBannerView`,但是数据源不一样,那么当数据源长度大于1的时候会轮播,下面会展示 `UIPageControl`。如果数据源是1个,那么就不会轮播和展示 `UIPageControl`)。
|
||||
|
||||
情况6是同一种类型的 View,但是根据展示的内容不一样,点击的意义也不一样。也就是运营需要去知道用户到底点击的是哪一个。如下图所示,「立即抢购」和「分享赚佣金」是同一种类型的 View,但是点击意义不一样,需要我们需要唯一标识出来。之前的方法通过 **「viewPath 配合同类型的 view 去加索引值」** 的方式还是没有办法唯一标识出来。所以想到一个方案,给 NSObject 添加一个分类,在分类里面添加一个协议。让需要复用但需要唯一标识的 view 去实现协议方法,因为是给 NSObject 分类添加的协议,所以 view 不需要去指定遵循。
|
||||
|
||||

|
||||
|
||||
@@ -406,7 +399,7 @@ static char *lbp_viewController_close_time = "lbp_viewController_close_time";
|
||||
|
||||
|
||||
```objective-c
|
||||
//NSObject+UniqueIdentify.h
|
||||
//NSObject+uaUniqueIdentify.h
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@@ -425,8 +418,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
//NSObject+UniqueIdentify.m
|
||||
#import "NSObject+UniqueIdentify.h"
|
||||
//NSObject+ua_UniqueIdentify.m
|
||||
#import "NSObject+ua_UniqueIdentify.h"
|
||||
|
||||
@implementation NSObject (UniqueIdentify)
|
||||
|
||||
@@ -459,7 +452,7 @@ NSString *const ShareToAward = @"分享赚佣金";
|
||||
|
||||
```objective-c
|
||||
//UIResponder Category 生成 viewPath
|
||||
- (NSString *)lbp_identifierKa
|
||||
- (NSString *)ua_identifierKa
|
||||
{
|
||||
// if (self.xq_identifier_ka == nil) {
|
||||
if ([self isKindOfClass:[UIView class]]) {
|
||||
@@ -468,7 +461,7 @@ NSString *const ShareToAward = @"分享赚佣金";
|
||||
NSMutableString *str = [NSMutableString string];
|
||||
//特殊的 加减购 因为带有spm但是要区分加减 需要带TreeNode
|
||||
NSString *className = [NSString stringWithUTF8String:object_getClassName(view)];
|
||||
if (!view.accessibilityIdentifier || [className isEqualToString:@"lbpButton"]) {
|
||||
if (!view.accessibilityIdentifier || [className isEqualToString:@"uaButton"]) {
|
||||
[str appendString:sameViewTreeNode];
|
||||
[str appendString:@","];
|
||||
}
|
||||
@@ -499,9 +492,13 @@ NSString *const ShareToAward = @"分享赚佣金";
|
||||
|
||||
|
||||
|
||||
### 7. 疑惑点
|
||||
### 7. 特殊 case
|
||||
|
||||
根据在同一个 view 上会有多个 subview,那么生成的 xpath 会携带在同类型 views 中的索引,所以一个登录、注册按钮的 xpath 可能为 ...btn1、...btn2。那么在版本A上线后运行了一段时间,上传并统计了数据。过了一段时间版本迭代,UI 搞事情,把登录和注册按钮的位置欢乐,变成了注册、登录。按照之前的逻辑生成的 xpath 为 ...btn1、...btn2。那么新的 xpath 虽然唯一,但是点击产生的数据会和之前的埋点数据意义不一样。别怕,你忘了还有一步绑定的逻辑。绑定的这一步会把每次开发的功能,通过可视化界面去将 xpath 和功能名称绑定一下。看下面的动图。所以不用担心虽然生成了唯一的 xpath,但是 App 在不同版本之间 UI 控件位置更换造成之前的统计数据在分析的时候不准确的问题。因为在绑定的时候就将新的 xpath 和功能名称进行了绑定,接口携带版本号。所以分析的时候注意版本号就好了。sql 一句话的事情。
|
||||
根据在同一个 view 上会有多个 subview,那么生成的 xpath 会携带在同类型 views 中的索引,所以一个登录、注册按钮的 xpath 可能为 `btn1.LoginView.LoginViewController.*baoApp`、`bt21.LoginView.LoginViewController.*baoApp`。
|
||||
|
||||
问题来了,当版本 A 上线后运行了一段时间,上传并统计了数据。但是过了一段时间版本迭代,UI 为了 KPI 搞事情,把登录和注册按钮的位置换了一下,变成了注册、登录。按照之前的逻辑生成的 xpath 还是 `btn1.LoginView.LoginViewController.*baoApp`、`bt21.LoginView.LoginViewController.*baoApp`。那么新的 xpath 虽然唯一,但是点击产生的数据会和之前的埋点数据意义不一样。别怕,你忘了还有一步绑定的逻辑。可视化绑定这个步骤会把每次开发的功能,通过可视化界面去将 `xpath` 和功能模块名称绑定一下。
|
||||
|
||||
看下面的动图。所以不用担心虽然生成了唯一的 `xpath`,但是 App 在不同版本之间 UI 控件位置更换造成之前的统计数据在分析的时候不准确的问题。因为在绑定的时候就将新的 `xpath` 和功能名称进行了绑定,接口携带版本号。所以分析的时候注意版本号就好了。sql 一句话的事情。
|
||||
|
||||
|
||||
|
||||
@@ -509,24 +506,22 @@ NSString *const ShareToAward = @"分享赚佣金";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 8. 数据如何处理
|
||||
|
||||
|
||||
#### A. 如何处理业务数据
|
||||
|
||||
利用系统提供的 **accessibilityIdentifier** 官方给出的解释是标识用户界面元素的字符串
|
||||
|
||||
> */**
|
||||
>
|
||||
> *A string that identifies the user interface element.*
|
||||
>
|
||||
> *default == nil*
|
||||
>
|
||||
> **/*
|
||||
>
|
||||
> **@property**(**nullable**, **nonatomic**, **copy**) NSString *accessibilityIdentifier NS_AVAILABLE_IOS(5_0);
|
||||
#### 1. 如何处理业务数据
|
||||
|
||||
利用系统提供的 `accessibilityIdentifier` 官方给出的解释是标识用户界面元素的字符串
|
||||
|
||||
```objective-c
|
||||
/*
|
||||
A string that identifies the user interface element.
|
||||
default == nil
|
||||
*/
|
||||
@property(nullable, nonatomic, copy) NSString *accessibilityIdentifier API_AVAILABLE(ios(5.0));
|
||||
```
|
||||
|
||||
服务端下发唯一标识
|
||||
|
||||
@@ -538,11 +533,11 @@ cell.accessibilityIdentifier = [[[SDGGoodsCategoryServices sharedInstance].categ
|
||||
|
||||
|
||||
|
||||
#### B. 基础数据
|
||||
#### 2. 基础数据
|
||||
|
||||
设计上分为2个 pod 库,一个是 TriggerKit(专门用来 hook 机会需要的所有事件,页面停留时间、页面标识、view标识),另一个是 Appmonitor(专门用来提供基础数据、埋点数据的维护、上传机制)。所以在 Appmonitor 里面有个类叫做 UserTrackDataCenter 的类,专门提供一些基础数据(系统版本、操作系统、地理位置、网络等信息)。
|
||||
设计上分为2个 pod 库,一个是 `UserAnalysis`(专门用来 hook 机会需要的所有事件,页面停留时间、页面标识、view标识),另一个是 AppMonitor(专门用来提供基础数据、埋点数据的维护、上传机制)。所以在 AppMonitor 里面有个类叫做 UserTrackDataCenter 的类,专门提供一些基础数据(系统版本、操作系统、地理位置、网络等信息)。
|
||||
|
||||
对外暴露出一些方法,用来将埋点数据交给 Appmonitor 去维护埋点数据,达到合适的“机制”再去上传埋点数据到服务端。
|
||||
对外暴露出一些方法,用来将埋点数据交给 AppMonitor 去维护埋点数据,达到合适的“机制”再去上传埋点数据到服务端。
|
||||
|
||||
```objective-c
|
||||
+ (void)clickEventUuid:(NSString *)uuid otherParam:(NSDictionary *)otherParam spmContent:(NSDictionary *)spmContent
|
||||
@@ -562,11 +557,9 @@ cell.accessibilityIdentifier = [[[SDGGoodsCategoryServices sharedInstance].categ
|
||||
|
||||
### 9. 曝光时间的统计
|
||||
|
||||
|
||||
|
||||
曝光的意义是什么?
|
||||
|
||||
我们的产品中可能有合作伙伴的广告,我们需要收取服务费。那如何计价?CPM(cost per Mille)每千人成本、CPC(cost per click)每点击成本、CPA(cost per action)每行动成本,根据这些指标来计算价格。或者自己的产品中运营人员在商城中投放了一次新的活动,为了这次活动在某个钻石展位放了设计人员精心设计的炫酷 Banner。这次活动后运营人员想分析在这个图片的作用下有多少人点击了这个活动页。
|
||||
我们的产品中可能有合作伙伴的广告,我们需要收取服务费。那如何计价?`CPM`(cost per Mille)每千人成本、`CPC`(cost per click)每点击成本、`CPA`(cost per action)每行动成本,根据这些指标来计算价格。或者自己的产品中运营人员在商城中投放了一次新的活动,为了这次活动在某个钻石展位放了设计人员精心设计的炫酷 Banner。这次活动后运营人员想分析在这个图片的作用下有多少人点击了这个活动页。
|
||||
|
||||
|
||||
|
||||
@@ -576,7 +569,7 @@ cell.accessibilityIdentifier = [[[SDGGoodsCategoryServices sharedInstance].categ
|
||||
|
||||
|
||||
|
||||
#### A. 有效曝光的判断
|
||||
#### 1. 有效曝光的判断
|
||||
|
||||
显示在屏幕可见区域如何判断?一个 View 显示在屏幕可见区域内,那么它肯定是经过从未初始化到初始化,再到设置 Frame 或者 Bounds 或者 Alpha 或者 Hidden 的。且它的根 view 一定是 UIWindow 对象。所以上面这句话进行分析整理就是下面的条件
|
||||
|
||||
@@ -584,7 +577,7 @@ cell.accessibilityIdentifier = [[[SDGGoodsCategoryServices sharedInstance].categ
|
||||
- alpha 小于 0.1 或者 hidden 为 YES
|
||||
- 根视图为 window
|
||||
|
||||
对于上面的三点可以用 AOP 进行判断。 hook 掉相应的方法,然后处理判断是否在可见区域内显示。最后的一个点经过一番查找,看到了一个 api `didMoveToWindow` ,根据它可以判断 view 是否显示到屏幕中(文档中说明:当它的 window 对象发送改变的时候会调用 view 的 didMoveToWindow 方法)。
|
||||
对于上面的三点可以用 AOP 进行判断。 hook 掉相应的方法,然后处理判断是否在可见区域内显示。最后的一个点经过一番查找,看到了一个 api `- (void)didMoveToWindow;` ,根据它可以判断 view 是否显示到屏幕中(文档中说明:当它的 window 对象发送改变的时候会调用 view 的 `didMoveToWindow` 方法)。
|
||||
|
||||
```
|
||||
Tells the view that its window object changed.
|
||||
@@ -596,7 +589,7 @@ The window property may be nil by the time that this method is called, indicatin
|
||||
|
||||
|
||||
|
||||
#### B. 曝光代码的执行效率优化
|
||||
#### 2. 曝光代码的执行效率优化
|
||||
|
||||
设想一下,某个复杂的页面可能是一个大的 UIViewController 顶部是店铺的基本信息,下面是 2个 UIViewController:左侧负责展示商品的一级、二级、三级分类,且负责选中和未选中的 UI 效果;右侧负责展示商品信息(顶部有商品的排序查找 ,下面是商品展示的 UICollectionView)。由于页面结构复杂,UI 层级嵌套严重,所以代码层面不注意的话,页面上计算量会比较大,CPU 负荷严重,直接影响着手机的 `耗电量`。改进的手段是在合适的地方提前 return 掉(比如 hidden 等于 YES 或者 aplha 小于 0.1 的时候)。
|
||||
|
||||
@@ -620,17 +613,15 @@ iOS 11 以下 UITableViewWrapperView 大小为屏幕中第一个完整的屏幕
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 10. 数据的上报
|
||||
|
||||
|
||||
|
||||
数据通过上面的办法收集完了,那么如何及时、高效的上传到后端,给运营分析、处理呢?
|
||||
|
||||
App 运行期间用户会点击非常多的数据,如果实时上传的话对于网络的利用率较低,所以需要考虑一个机制去控制用户产生的埋点数据的上传。
|
||||
|
||||
|
||||
|
||||
思路是这样的。对外部暴露出一个接口,用来将产生的数据往数据中心存储。用户产生的数据会先保存到 AppMonitor 的内存中去,设置一个临界值(memoryEventMax = 50),如果存储的值达到设置的临界值 memoryEventMax,那么将内存中的数据写入文件系统,以 zip 的形式保存下来,然后上传到埋点系统。如果没有达到临界值但是存在一些 App 状态切换的情况,这时候需要及时保存数据到持久化。当下次打开 App 就去从本地持久化的地方读取是否有未上传的数据,如果有就上传日志信息,成功后删除本地的日志压缩包。
|
||||
思路是这样的。对外部暴露出一个接口,用来将产生的数据往数据中心存储。用户产生的数据会先保存到 `AppMonitor` 的内存中去,设置一个临界值(memoryEventMax = 50),如果存储的值达到设置的临界值 memoryEventMax,那么将内存中的数据写入文件系统,以 zip 的形式保存下来,然后上传到埋点系统。如果没有达到临界值但是存在一些 App 状态切换的情况,这时候需要及时保存数据到持久化。当下次打开 App 就去从本地持久化的地方读取是否有未上传的数据,如果有就上传日志信息,成功后删除本地的日志压缩包。
|
||||
|
||||
App 应用状态的切换策略如下:
|
||||
|
||||
@@ -722,6 +713,12 @@ App 应用状态的切换策略如下:
|
||||
}
|
||||
```
|
||||
|
||||
其实 App 内部的数据上报有很多场景,比如 [APM 监控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md) 数据的上报、埋点数据的上报、业务线的数据上报等等,所以可以设计为[一个通用、可配置的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)。
|
||||
|
||||
|
||||
|
||||
### 11. 总结
|
||||
|
||||
使用的时候就是在 hook 系统事件的时候,去调用统计页面上传数据
|
||||
|
||||
```objective-c
|
||||
@@ -736,31 +733,29 @@ App 应用状态的切换策略如下:
|
||||
总结下来关键步骤:
|
||||
|
||||
1. hook 系统的各种事件(UIResponder、UITableView、UICollectionView代理事件、UIControl事件、UITapGestureRecognizers)、hook 应用程序、控制器生命周期。在做本来的逻辑之前添加额外的监控代码
|
||||
2. 对于点击的元素按照视图树生成对应的唯一标识(addCartButton.GoodsView.GoodsViewController) 的 md5 值
|
||||
3. 在业务开发完毕,进入埋点的编辑模式,将 md5 和关键的页面的关键事件(运营、产品想统计的关键模块:App层级、业务模块、关键页面、关键操作)给绑定起来。比如 addCartButton.GoodsView.GoodsViewController.tbApp 对应了 tbApp-商城模块-商品详情页-加入购物车功能。
|
||||
2. 对于点击的元素按照视图树生成对应的唯一标识 `addCartButton.GoodsView.GoodsViewController.**App` 的 md5 值
|
||||
3. 在业务开发完毕,进入埋点的编辑模式,将 md5 和关键的页面的关键事件(运营、产品想统计的关键模块:App层级、业务模块、关键页面、关键操作)给绑定起来。比如 `addCartButton.GoodsView.GoodsViewController.**App` 对应了 `**App-商城模块-商品详情页-加入购物车功能`。
|
||||
4. 将所需要的数据存储下来
|
||||
5. 设计机制等到合适的时机去上传数据
|
||||
|
||||
|
||||
|
||||
## 四. 演示一个完整的埋点上报流程
|
||||
|
||||
|
||||
## 举例说明一个完整的埋点上报流程
|
||||
|
||||
埋伏模块分为2个pod组件库,TriggerKit 负责拦截系统事件,拿到埋点数据。Appmonitor 负责收集埋点数据,本地持久化或内存储存,等到合适时机去上传埋点数据。
|
||||
埋伏模块分为2个pod组件库,`UserAnalysis` 负责拦截系统事件,拿到埋点数据。`AppMonitor` 负责收集埋点数据,本地持久化或内存储存,等到合适时机去上传埋点数据。
|
||||
|
||||
|
||||
|
||||
1. 通过接口获取数据,给对应的 view 的 accessibilityIdentifier 属性绑定埋点数据
|
||||
1. 通过接口获取数据,给对应的 view 的 `accessibilityIdentifier` 属性绑定埋点数据
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
2. hook 系统事件,点击拿到 view,获取 accessibilityIdentifier 属性值
|
||||
2. hook 系统事件,点击拿到 view,获取 `accessibilityIdentifier` 属性值
|
||||
|
||||

|
||||
|
||||
3. 将数据向的数据中心发送,数据中心处理数据(埋点数据结合App基础信息,图上 UserTrackDataCenter 对象)。根据情况将数据存储到内存或者本地,等到合适的时机去上传
|
||||
3. 将数据向的数据中心发送,数据中心处理数据(埋点数据结合App基础信息,图上 `UserTrackDataCenter` 对象)。根据情况将数据存储到内存或者本地,等到合适的时机去上传
|
||||
|
||||

|
||||
Reference in New Issue
Block a user