mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
docs: refine
This commit is contained in:
@@ -58,7 +58,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
|
||||
|
||||

|
||||
|
||||
VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPU,GPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内,CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃,等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
|
||||
VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPU,GPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内,CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃(也叫掉帧),等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
|
||||
|
||||
目前 iOS 设备有双缓存机制,也有三缓冲机制,Android 现在主流是三缓冲机制,在早期是单缓冲机制。
|
||||
[iOS 三缓冲机制例子](https://ios.developreference.com/article/12261072/Metal+newBufferWithBytes+usage)
|
||||
@@ -553,8 +553,6 @@ curNode.costTime > (parentNode.costTime * 1.1/n) && curNode.costTime > parentNod
|
||||
|
||||
这样情况下,业务方可以针对特定的业务流程做性能统计分析
|
||||
|
||||
|
||||
|
||||
## 二、 App 启动时间监控
|
||||
|
||||
### 1. App 启动时间的监控
|
||||
@@ -773,6 +771,8 @@ for (int i = 0; i < threadCount; i++) {
|
||||
|
||||
## 四、 OOM 问题
|
||||
|
||||
大多数情况下 OOM 问题比 crash 问题更严重,线上稳定性问题主要是由于 OOM 造成的,因为线下可以利用 Xcode 的一些工具解决并定位 crash。线上也有类似 KSCrash 这样的优秀监控工具。但是 OOM 方面线下只有一些三方的工具,这些工具大多是基于 OC 对象引用关心实现的,所以只能判断 OC 对象。另外比较耗费性能,所以线上 OOM 问题还是比较多的。
|
||||
|
||||
### 1. 基础知识准备
|
||||
|
||||
硬盘:也叫做磁盘,用于存储数据。你存储的歌曲、图片、视频都是在硬盘里。
|
||||
@@ -1624,6 +1624,51 @@ for 循环打印出每个进程(也就是 App)的 pid、Priority、User Data
|
||||
|
||||
### 4. 如何判定发生了 OOM
|
||||
|
||||
首先我们无法通过正常手段监控 OOM,因为 XNU 源码显示发生 OOM 发送的信号为 SIGKILL,该信号无法监控。
|
||||
|
||||
```c
|
||||
/*
|
||||
* The jetsam no frills kill call
|
||||
* Return: 0 on success
|
||||
* error code on failure (EINVAL...)
|
||||
*/
|
||||
static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason) {
|
||||
int error = 0;
|
||||
error = exit_with_reason(p, W_EXITCODE(0, SIGKILL), (int *)NULL, FALSE, FALSE, jetsam_flags, jetsam_reason);
|
||||
return error;
|
||||
}
|
||||
```
|
||||
|
||||
FacekBook 提出排除法监控 OOM。
|
||||
|
||||

|
||||
|
||||
- App 更新了版本
|
||||
|
||||
- App 发生了崩溃
|
||||
|
||||
- 用户手动退出
|
||||
|
||||
- 操作系统更新了版本
|
||||
|
||||
- App 切换到后台之后进程终止
|
||||
|
||||
其实不够全,存在误判,增加下面几种 case
|
||||
|
||||
- 覆盖安装
|
||||
|
||||
- WatchDog 崩溃
|
||||
|
||||
- 后台启动
|
||||
|
||||
- XCTest/UITest 等自动化测试框架驱动
|
||||
|
||||
- 应用 exit 主动退出
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
OOM 导致 crash 前,app 一定会收到低内存警告吗?
|
||||
|
||||
做 2 组对比实验:
|
||||
@@ -2793,49 +2838,47 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
@implementation NetworkDelegateProxy
|
||||
|
||||
#pragma mark - life cycle
|
||||
+ (instancetype)sharedInstance {
|
||||
static NetworkDelegateProxy *_sharedInstance = nil;
|
||||
```
|
||||
|
||||
- (instancetype)sharedInstance {
|
||||
static NetworkDelegateProxy *_sharedInstance = nil;
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
|
||||
_sharedInstance = [NetworkDelegateProxy alloc];
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
_sharedInstance = [NetworkDelegateProxy alloc];
|
||||
|
||||
});
|
||||
|
||||
|
||||
return _sharedInstance;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - public Method
|
||||
|
||||
+ (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate
|
||||
{
|
||||
NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance];
|
||||
instance->_originalTarget = originalTarget;
|
||||
instance->_NewDelegate = newDelegate;
|
||||
return instance;
|
||||
- (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate {
|
||||
NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance];
|
||||
instance->_originalTarget = originalTarget;
|
||||
instance->_NewDelegate = newDelegate;
|
||||
return instance;
|
||||
}
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation
|
||||
{
|
||||
if ([_originalTarget respondsToSelector:invocation.selector]) {
|
||||
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation {
|
||||
if ([_originalTarget respondsToSelector:invocation.selector]) {
|
||||
|
||||
[invocation invokeWithTarget:_originalTarget];
|
||||
[((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation];
|
||||
[((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
|
||||
{
|
||||
return [_originalTarget methodSignatureForSelector:sel];
|
||||
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
|
||||
return [_originalTarget methodSignatureForSelector:sel];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
@@ -6139,7 +6182,7 @@ file_names[ 4]:
|
||||
<起始地址> <结束地址> <函数> [<文件名:行号>]
|
||||
```
|
||||
|
||||
#### 4.4 **如何获取地址?**
|
||||
#### 4.4 如何获取地址
|
||||
|
||||
image 加载的时候会进行相对基地址进行重定位,并且每次加载的基地址都不一样,函数栈 frame 的地址是重定位后的绝对地址,我们要的是重定位前的相对地址。
|
||||
|
||||
@@ -6358,21 +6401,21 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
|
||||
|
||||
Weex 由于历史原因,不做性能监控了,现有业务代码继续跑着,只业务问题和稳定性问题。
|
||||
|
||||
## Weex 异常监控
|
||||
### Weex 异常监控
|
||||
|
||||
### 背景
|
||||
#### 背景
|
||||
|
||||
Weex 由于历史原因,不能很好的统计异常。比如在页面的模版代码中绑定了 data 中的一个对象,此时对象可能并没有值,而是依赖后续的网络请求完成,对象才有了具体的值 data 改变,数据驱动,页面再次 render。所以监控代码会认为第一次 render 的时候访问对象不存在的属性。
|
||||
|
||||
真正有问题的代码和不影响业务的异常信息,都会被 Vue 官方认为是一场。基于这样的背景,我们无法 pick 出真正异常或者是开发者判空代码没写好的问题。
|
||||
|
||||
### 解决思路
|
||||
#### 解决思路
|
||||
|
||||
按照异常的等级,可以划分为影响业务和不影响业务。问题来了,什么叫做“影响业务”?这是我们自己定义的词,也就是影响用户是否正常操作 App。比如说:页面白屏、点击某个按钮无响应等等。定义为 Error。其他不影响业务的定义为 Warning。
|
||||
|
||||
### 技术实现
|
||||
#### 技术实现
|
||||
|
||||
#### Warning 异常
|
||||
##### Warning 异常
|
||||
|
||||
采用主流方案,React、Vue 都提供了框架自己的异常监控方案,由于 Weex 是在 Vue 基础上实现。包括 Native 和 Vue 的 UI 双线程机制、事件机制等等。其余我们不关心,Weex 开发中,开发和都是写 Vue 去实现页面和逻辑,所以我们监控 Vue 的异常就满足了。
|
||||
|
||||
@@ -6417,7 +6460,7 @@ Weex 由于历史原因,不能很好的统计异常。比如在页面的模版
|
||||
componentName = this.fetchComponentName(vm)
|
||||
componentPath = this.fetchComponentPath(vm)
|
||||
}
|
||||
|
||||
|
||||
let errorInfo = {
|
||||
name: err.name,
|
||||
reason: err.message,
|
||||
@@ -6442,13 +6485,13 @@ export default APM;
|
||||
|
||||
由于 Weex 代码是单独可运行和部署的,因此前端没有统一的入口,所以在发布阶段监控代码需要配合打包机,利用脚本动态插入到页面代码中。
|
||||
|
||||
#### Error 监控
|
||||
##### Error 监控
|
||||
|
||||
根据我们对“影响业务”的定义,Error 包括:页面白屏 + 事件无法响应。
|
||||
|
||||
所以我们来拆解下问题。
|
||||
|
||||
##### 页面白屏
|
||||
###### 页面白屏
|
||||
|
||||
根据 Weex SDK 整个完整流程得出,白屏包括以下几个可能:
|
||||
|
||||
@@ -6465,14 +6508,13 @@ export default APM;
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
- Weex 资源网络请求成功,但是数据内容为空
|
||||
|
||||
```objectivec
|
||||
_mainBundleLoader.onFinished = ^(WXResourceResponse *response, NSData *data) {
|
||||
// ...
|
||||
|
||||
|
||||
if (!data) {
|
||||
// Weex 资源网络请求成功,但是数据内容为空。也就是下载下来的资源无法消费,页面无法正常渲染
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Request to %@ With no data return", WeexSafeString(request.URL)];
|
||||
@@ -6484,14 +6526,13 @@ export default APM;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
- Weex 资源网络请求成功,但是数据 JSON 序列化失败
|
||||
|
||||
```objectivec
|
||||
_mainBundleLoader.onFinished = ^(WXResourceResponse *response, NSData *data) {
|
||||
// ...
|
||||
|
||||
|
||||
NSString *jsBundleString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
if (!jsBundleString) {
|
||||
// Weex 资源网络请求成功,但是网络数据 JSON 序列化失败,也就是下载下来的资源无法消费,页面无法正常渲染
|
||||
@@ -6503,8 +6544,7 @@ export default APM;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
- Weex 资源网络请求异常(网络请求 _mainBundleLoader.onFailed)
|
||||
|
||||
```objectivec
|
||||
@@ -6518,10 +6558,8 @@ export default APM;
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### 点击事件无响应
|
||||
###### 点击事件无响应
|
||||
|
||||
调试 WeexSDK 发现 SDK 内部通过给 JSContext 注册了 `callNativeModule` 方法来实现调用 Native Module 的 Method。
|
||||
|
||||
@@ -6555,7 +6593,7 @@ export default APM;
|
||||
};
|
||||
[[WeexAPM sharedInstance] reportError:errorDict];
|
||||
} @finally {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6604,7 +6642,7 @@ export default APM;
|
||||
}
|
||||
```
|
||||
|
||||
#### JS ExceptionHandler
|
||||
##### JS ExceptionHandler
|
||||
|
||||
JS 引擎中异常回调中上报错误
|
||||
|
||||
@@ -6636,6 +6674,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
||||
|
||||
统计线上数据发现,Weex 页面渲染失败率为4.09%,所以看上去页面渲染失败率还是蛮高的。目前发现失败率最高的场景为 App 启动时候按照功能模块组织的配置清单。
|
||||
类似于下面的配置:
|
||||
|
||||
```
|
||||
"//goods/detail": {
|
||||
"configParams": "",
|
||||
@@ -6648,18 +6687,15 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
||||
|
||||
因此,针对于这样的场景。我们希望针对 Weex 资源的拉取和访问机制做一些优化。
|
||||
目前有几个流程不太合理:
|
||||
|
||||
1. 当前启动时的js 预载入流程里的JS下载失败 和 进入Weex页面后的JS拉取失败 两处的上报失败未做区分,没法实际统计加载页面时有多少JS获取失败的情况。
|
||||
2. 随着业务迭代, Weex 页面越来越多,需要做 JS 拉取相关优化,避免白屏问题
|
||||
3. 目前 JS 缓存逻辑为,每个 Weex 模块单独维护一个 LRUCache,并且会做大量的预加载,但可能大多数页面根本不会访问到。浪费了内存资源去做了一些不会被调用到的页面预加载,可能还会造成 OOM 问题
|
||||

|
||||

|
||||
|
||||
// todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能,比如某个收银员角色固定的情况下,他日常的操作行为是固定的,商品扫码、开单)
|
||||
|
||||
|
||||
|
||||
### 2. Flutter 异常监控
|
||||
|
||||
|
||||
#### 2. Flutter 异常监控
|
||||
|
||||
## 九、子线程 UI 监控
|
||||
|
||||
@@ -6697,8 +6733,6 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
||||
|
||||
具体可以参考这个 [Demo](https://github.com/FantasticLBP/MainThreadChecker)
|
||||
|
||||
|
||||
|
||||
## 十、页面渲染时长统计
|
||||
|
||||
当我们的产品经理、TL、领导或者任何关心我们产品质量的某个角色问你,你们的 App 看上去好像比较卡,很难用,这时候我们心里一阵空虚,卡吗?
|
||||
@@ -6747,10 +6781,8 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
||||
|
||||
另外随着问题的暴露或者技术研究的渗入,监控方案本身是可以迭代演进的。
|
||||
|
||||
|
||||
1. 8060 会有特例,比如8060满足了,且此时主线程空了。因为某个 ImageView 在子线程上根据 URL 异步请求资源,之后会再次触发渲染。所以这个情况下,方案还是存在问题的
|
||||
|
||||
|
||||
## 十一、 APM 小结
|
||||
|
||||
1. 通常来说各个端的监控能力是不太一致的,技术实现细节也不统一。所以在技术方案评审的时候需要将监控能力对齐统一。每个能力在各个端的数据字段必须对齐(字段个数、名称、数据类型和精度),因为 APM 本身是一个闭环,监控了之后需符号化解析、数据整理,进行产品化开发、最后需要监控大盘展示等
|
||||
|
||||
Reference in New Issue
Block a user