docs: Data uploader SDK && Readme

This commit is contained in:
杭城小刘
2020-07-06 04:55:47 +08:00
parent 27c2d9dc0f
commit 028d446ac6
12 changed files with 2743 additions and 127 deletions

View File

@@ -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 为例。我们会想到 **AOPAspect 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:@","];
}
@@ -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 不需要去指定遵循。
!["立即抢购"、"分享赚佣金"同类型view但点击意义不一样](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-11-UserTrack01.png)
@@ -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. 曝光时间的统计
曝光的意义是什么?
我们的产品中可能有合作伙伴的广告我们需要收取服务费。那如何计价CPMcost per Mille每千人成本、CPCcost per click每点击成本、CPAcost 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` 属性绑定埋点数据
![接口拿到的数据](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-05-userTrack1.png)
![绑定埋点数据到view](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-05-userTrack2.png)
2. hook 系统事件,点击拿到 view获取 accessibilityIdentifier 属性值
2. hook 系统事件,点击拿到 view获取 `accessibilityIdentifier` 属性值
![hook系统事件获取accessibilityIdentifier](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-05-userTrack3.png)
3. 将数据向的数据中心发送数据中心处理数据埋点数据结合App基础信息图上 UserTrackDataCenter 对象)。根据情况将数据存储到内存或者本地,等到合适的时机去上传
3. 将数据向的数据中心发送数据中心处理数据埋点数据结合App基础信息图上 `UserTrackDataCenter` 对象)。根据情况将数据存储到内存或者本地,等到合适的时机去上传
![拦截系统事件后将数据交给数据中心处理](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-04-05-userTrack4.png)

View File

@@ -365,7 +365,7 @@ dispatch_async(dispatch_get_global_queue(0, 0), ^{
#### 3.2 子线程 ping 主线程监听的方式
开启一个子线程创建一个初始值为0的信号量、一个初始值为 YES 的布尔值类型标志位。将设置标志位为 NO 的任务派发到主线程中去,子线程休眠阈值时间,时间到后判断标志位是否被主线程成功(值为 NO如果没成功则认为线程发生了卡顿情况,此时 dump 堆栈信息并结合数据上报机制,按照一定策略上传数据到服务器。数据上报会在 [打造功能强大、灵活可配置的数据上报组件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md) 讲
开启一个子线程创建一个初始值为0的信号量、一个初始值为 YES 的布尔值类型标志位。将设置标志位为 NO 的任务派发到主线程中去,子线程休眠阈值时间,时间到后判断标志位是否被主线程成功(值为 NO如果没成功则认为线程发生了卡顿情况,此时 dump 堆栈信息并结合数据上报机制,按照一定策略上传数据到服务器。数据上报会在 [打造功能强大、灵活可配置的数据上报组件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md) 讲
```Objective-c
while (self.isCancelled == NO) {
@@ -6578,3 +6578,5 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
- [MDN-HTTP Messages](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages)
- [DWARF 和符号化](https://junyixie.github.io/2018/09/30/dwarf和符号化/)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,124 @@
# APM
内存泄露检测
各家都是基于 Facebook 的 FBRetainCycleDetector 实现,有:
腾讯 MLeaksFinder
https://github.com/Tencent/MLeaksFinder
PLeakSniffer
https://github.com/music4kid/PLeakSniffer
LeaksMonitor
https://github.com/tripleCC/Laboratory/tree/master/LeaksMonitor
ANR 检测
ANR 检测相对比较简单,各家原理基本相同。新建一个 thread 在 threshold 的间隔时间内(一般 200 - 400毫秒循环等待一个 semaphore并从主线程 async dispatch semaphore 的 signal 函数,如果超时则报卡顿。
1. https://gist.github.com/leilee/3275b94c381114332242978fc4366591
2. https://gist.github.com/Adlai-Holler/ea7e3e98333b3b84f13d19b169ccf989
1. https://gist.github.com/mikeash/5172803 这个是纯 GCD 的实现
2. https://gist.github.com/steipete/5664345 PFPDFKit 里用的是这个
3. https://gist.github.com/jspahrsummers/419266f5231832602bec GitHub 客户端里用的
4. https://github.com/wojteklu/Watchdog 这个有用 atomic_store 和 atomic_load 实现死锁回调
5. DoKit
8. https://github.com/ming1016/study/wiki/检测iOS的APP性能的一些方法
启动优化
《Reducing Your App's Launch Time》- Apple
https://developer.apple.com/documentation/xcode/improving_your_app_s_performance/reducing_your_app_s_launch_time
《抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%》
https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247485101&idx=1&sn=abbbb6da1aba37a04047fc210363bcc9&scene=21#wechat_redirect
// 这种纬度的启动时间优化对小公司来说 ROI 太低
《美团外卖 iOS App 冷启动治理》
https://tech.meituan.com/2018/12/06/waimai-ios-optimizing-startup.html
测量进程的 uptime
https://github.com/tripleCC/Laboratory/blob/master/ProcessStartTime/ProcessStartTime/main.m
https://stackoverflow.com/questions/40649964/ios-get-self-process-start-time-after-the-fact/40677286
滴滴出行 DoraemonKit
https://github.com/didi/DoraemonKit
功能非常多,感觉侧重了 UI 的运行时调试。APM 的部分主要依赖第三方库,如 MLeakFinder。有提供问题上报机制所以会有不少额外的网络请求发送到 dokit 的API。
腾讯 Matrix
https://github.com/tencent/matrix
功能精简,只有 ANR 和内存检测。
美图秀秀 MTHawkeye
https://github.com/meitu/MTHawkeye/tree/develop/MTHawkeye
监控项目比 DoKit 多ANR 中能看到主线程堆栈信息,网络层有做一些基本优化指标的监控。
阿里巴巴 GodEye
https://github.com/zixun/GodEye
项目已经三年没有维护不过作者的小专栏有提供整体思路值得参考学习。https://xiaozhuanlan.com/godeye
# 技术博客
AloneMonkey http://www.alonemonkey.com/ https://github.com/AloneMonkey #ios #re
yulingtianxia https://yulingtianxia.com/ https://github.com/yulingtianxia #ios #ml
SatanWoo http://satanwoo.github.io/ https://github.com/SatanWoo #ios #ml
Naville Zhang https://mayuyu.io/ https://github.com/Naville #ios #re #llvm
Cocoa Oikawa https://blog.0xbbc.com/ https://github.com/BlueCocoa #ios
jmpews https://jmpews.github.io https://github.com/jmpews #ios #re
kov4l3nko https://kov4l3nko.github.io https://github.com/kov4l3nko #ios
mikeash https://www.mikeash.com/pyblog/ #ios
NSBLOGGING https://kandelvijaya.com/post/ #ios
4ch12dy http://4ch12dy.site/ https://github.com/4ch12dy
ibireme https://blog.ibireme.com/ https://github.com/ibireme/ #ios
bang http://blog.cnbang.net/ https://github.com/bang590 #ios
everettjf https://everettjf.github.io/ https://github.com/everettjf #ios #re
Gityuan http://gityuan.com/ #android #flutter
Meituan https://tech.meituan.com/ #ios #android #company
draveness https://draveness.me/ #ios #golang
chy305chy https://chy305chy.github.io/ #ios
Urinx https://urinx.github.io/v6/ https://github.com/Urinx #ios #re
bdunagan http://bdunagan.com/ #ios
la0s https://la0s.github.io/ #re #ios
caijinglong https://www.kikt.top/ #flutter
weishu http://weishu.me/ https://github.com/tiann #android #re
zixia https://github.com/huan #ml #re
xelz https://github.com/xelzmm http://xelz.info/ #re
ChiChou https://github.com/ChiChou https://blog.chichou.me/ #re
# 学习
# 资料
* 经典WWDC视频
[WWDC2016:Optimizing App Startup Time](https://developer.apple.com/videos/play/wwdc2016/406)
[WWDC2016:Optimizing I/O for Performance and Battery Life](https://developer.apple.com/videos/play/wwdc2016/719/)
[WWDC2017:App Startup Time: Past, Present, and Future](https://developer.apple.com/videos/play/wwdc2017/413/)
* 图片的加载过程
[Core Image: Performance, Prototyping, and Python](https://developer.apple.com/videos/play/wwdc2018/719/)
* Facebook的二进制优化开拓思路
[通过优化二进制布局提升iOS启动性能](https://www.bilibili.com/video/BV1NJ411w7hv)
# 文章
[如何对 iOS 启动阶段耗时进行分析](https://ming1016.github.io/2019/12/07/how-to-analyze-startup-time-cost-in-ios/)
[objc_msgSend Hook 精简学习过程](https://linux.ctolib.com/czqasngit-objc_msgSend_hook.html)
[如何对 iOS 启动阶段耗时进行分析](http://www.starming.com/2019/12/07/how-to-analyze-startup-time-cost-in-ios/)
[App 启动速度怎么做优化与监控?](https://time.geekbang.org/column/article/85331)
[监控所有的OC方法耗时](https://juejin.im/post/5d146490f265da1bc37f2065)
[Hook objc_msgSend to hotfix](https://www.dazhuanlan.com/2019/10/18/5da8a4b2a7da7/)
[iOS App启动优化—— 了解App的启动流程](https://juejin.im/post/5da830a4e51d457805049817)
[iOS App启动优化—— 使用“Time Profiler”工具监控App的启动耗时](https://juejin.im/post/5dad6bfb6fb9a04de818fcb8)
[iOS App启动优化—— 自己做一个工具监控App的启动耗时](https://juejin.im/post/5de501e0e51d4540a15879ff)
* facebook的启动优化2015年的老文章
[Optimizing Facebook for iOS start time](https://engineering.fb.com/ios/optimizing-facebook-for-ios-start-time/)
[优化 App 的启动时间](http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time/#%E5%AE%89%E5%85%A8)
[深入理解App的启动过程](https://github.com/LeoMobileDeveloper/Blogs/blob/master/iOS/What%20happened%20at%20startup%20time.md)
[iOS启动速度优化](https://github.com/BiBoyang/BoyangBlog/blob/master/File/iOS_APM_03.md)
[iOS app启动速度研究实践](https://zhuanlan.zhihu.com/p/38183046?from=1086193010&wm=3333_2001&weiboauthoruid=1690182120)
[iOS Dynamic Framework 对App启动时间影响实测](https://www.jianshu.com/p/3263009e9228)
[iOS App 启动性能优化](https://mp.weixin.qq.com/s/Kf3EbDIUuf0aWVT-UCEmbA)
[今日头条iOS客户端启动速度优化](https://juejin.im/entry/5b6061bef265da0f574dfd21)
[如何精确度量 iOS App 的启动时间](https://www.jianshu.com/p/c14987eee107)
[ iOS App冷启动治理来自美团外卖的实践 ](https://mp.weixin.qq.com/s/jN3jaNrvXczZoYIRCWZs7w)

View File

@@ -178,7 +178,7 @@ electron 分为**渲染进程和主进程**。和 Native 中的概念不一样
**解决了不稳定问题**由于进程之间是彼此隔离的,所以当一个页面或者插件奔溃时,受影响的仅仅是当前的页面或者插件进程,并不会影响到浏览器和其他的页面。也就是说解决了早期浏览器某个页面或者插件奔溃导致整个浏览器的奔溃,从而解决了不稳定问题。
**解决了不稳定问题**由于进程之间是彼此隔离的,所以当一个页面或者插件奔溃时,受影响的仅仅是当前的页面或者插件进程,并不会影响到浏览器和其他的页面。也就是说解决了早期浏览器某个页面或者插件奔溃导致整个浏览器的奔溃,从而解决了不稳定问题。
**解决了不流畅问题。** 同样Javascript 进行也是运行在渲染进程中的,所以即使当前 Javascript 阻塞了渲染进程,影响到的也只是当前的渲染页面,并不会影响到浏览器和其他页面或者插件进程(其他的页面的脚本是运行在自己的渲染进程中的)。
@@ -221,7 +221,7 @@ Chrome 团队一直在寻求新的弹性方案,既可以解决资源占用较
#### 1.4 未来面向服务的架构
2016年 Chrome 官方团队使用 “**面向服务的架构**”Services Oriented Architecture简称 SOA的思想设计了最新的 Chrome 架构。Chrome 整体架构会向现代操作系统所采用的“面向服务的架构”方向发展。
2016年 Chrome 官方团队使用“**面向服务的架构**”Services Oriented Architecture简称 SOA的思想设计了最新的 Chrome 架构。Chrome 整体架构会向现代操作系统所采用的“面向服务的架构”方向发展。
之前的各种模块会被重构成为单独的服务Services每个服务都可以运行在独立的进程中访问服务必须使用定义好的接口通过 IPC 进行通信。从而构建一个更内聚、低耦合、易于维护和拓展的系统。

View File

@@ -32,7 +32,7 @@
在几乎所有的机器上,**多字节对象都被存储为连续的字节序列**。例如在 C 语言中,一个 `int` 类型的变量 x 地址为 0x100那么其对应的地址表达式 `&x` 的值为 `0x100`,且 x 的4个字节将被存储在存储器的 `0x100`,`0x101`,`0x102`,`0x103` 位置。
字节的排列方式有2个通用规则。例如一个多位整数按照存储地址从低到高排序的字节中如果该整数的最低有效字节类似于最低有效位排在最高有效字节前面成为**小端序**;反之成为**大端序**。在计算机网络中,字节序是一个必须要考虑的因素,因为不同类型的机器可能采用不同标准的字节序,所以均需要按照网络标准进行转化。
字节的排列方式有2个通用规则。例如一个多位整数按照存储地址从低到高排序的字节中如果该整数的最低有效字节类似于最低有效位排在最高有效字节前面称为“**小端序**;反之成为**大端序**。在计算机网络中,字节序是一个必须要考虑的因素,因为不同类型的机器可能采用不同标准的字节序,所以均需要按照网络标准进行转化。
假设一个类型为 int 的变量 x位于地址 0x100 处,它的值为 0x01234567地址范围为 0x1000x103字节其内部的排列顺序由机器决定也就是和 CPU 有关,和操作系统无关。
@@ -164,6 +164,12 @@ NBO(Network Byte Order):按照从高到低的顺序存储,在网络上使用
主机字节顺序HBOHost Byte Order不同机器 HBO 不相同,与 CPU 有关。计算机存储数据有两种字节优先顺序Big Endian 和 Little Endian。Internet 以 Big Endian 顺序在网络上传输,所以对于在内部是以 Little Endian 方式存储数据的机器,在网络通信时就需要进行转换。
Big-Endian: PowerPC、IBM、Sun
Little-Endian:x86、DEC
ARM 既可以工作在大端模式,也可以工作在小端模式。
## 如何转换

View File

@@ -1,13 +1,15 @@
## **knowledge-kit**
### 目的
形式记录大前端路上的探索,主要研究基础平台开发、工程效率、质量监控保证、SDK 输出、多端融合能力、动态化、组件化、工程化能力。
形式记录大前端路上的探索主要研究基础平台开发、工程效率、质量监控保证、SDK 输出、多端融合能力、动态化、组件化、工程化能力。
技术点: iOS、Web 前端、后端、Hybrid、Node 的应用、爬虫、反爬虫、后端、数据库、算法等领域。
偶尔记录自学经济学遇到的概念或者有趣的生活现象解读.
偶尔记录自学经济学遇到的概念或者有趣的生活现象解读
## 目录
@@ -15,10 +17,14 @@
如果要方便的查看文章目录,请点击[链接](https://github.com/FantasticLBP/knowledge-kit/blob/master/SUMMARY.md)
## 反馈
定期更新博文。如果在查看文章的时候发现了问题可以提出 issue。95年小双鱼, 技术专家进阶中,有技术问题或者想聊聊程序员的事情可以通过微博联系)[微博](http://weibo.com/u/3194053975)
定期更新博文。如果在查看文章的时候发现了问题可以提出 issue。95年小双鱼,关注大前端领域,有事情可以通过[微博](http://weibo.com/u/3194053975)联系)
## 交流
如果你也是大前端路上的一名修行者,可以加我 Wechat704568245,拉你入群一起在「大前端自习室」学习交流。
如果你也是大前端路上的一名修行者可以通过[微博](http://weibo.com/u/3194053975)联系我,拉你入群一起在「大前端自习室」学习交流。

View File

@@ -82,7 +82,7 @@
* [77、iOS 打包系统构建加速](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.77.md)
* [78、上架包预检](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.78.md)
* [79、深入理解各种锁](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.79.md)
* [80、打造一个通用、可配置的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)
* [80、打造一个通用、可配置、多句柄的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)
* [81、__asm__ 重命名符号](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.81.md)
* [82、runtime](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.82.md)
* [83、NSURLProtocol 应用场景](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.83.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 KiB