mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
feat: Zombie 探针
This commit is contained in:
@@ -20,7 +20,7 @@ iOS 图形界面的显示是一个复杂的流程,一部分数据通过 Core G
|
||||
|
||||
## Core Animation
|
||||
|
||||

|
||||

|
||||
|
||||
可以看到 Core Animation pipeline 由4部分组成:Application 层的 Core Animation 部分、Render Server 中的 Core Animation 部分、GPU 渲染、显示器显示。
|
||||
|
||||
|
||||
@@ -255,6 +255,96 @@ static inline bool _objc_isTaggedPointer(const void * _Nullable ptr)
|
||||
1000
|
||||
```
|
||||
|
||||
tips:某些对象虽然是 TaggedPointer 类型,但是打印 class 发现不是,猜测可能是系统用类簇隐藏了某些实现细节。比如下面
|
||||
|
||||

|
||||
|
||||
针对 NSNumber 的 TaggedPoniter 的 case,查看 class 打印出 `__NSCFNumber`。但根据源码和内存高地址位分析确实是 TaggedPoniter。
|
||||
|
||||
疑问是:为什么 NSNumber 的 TaggedPpinter case 下打印 class 是 `__NSCFNumber`。如果是类簇隐藏细节实现,为什么同样 KVO 也改变了 isa,但是命名是一个新的名字,而不是类簇的实现?
|
||||
|
||||
和朋友讨论后有2种观点(观点不是独立的,而是并且同时成立的。对错难以判定,仅供参考):
|
||||
|
||||
- 类簇,为了隐藏细节实现
|
||||
|
||||
- KVO 和当前 case 不一致。类簇是系统类的设计,KVO 是针对开发者写的对象所以没有类簇,只能动态生成类,改变原类的 isa,命名为 `NSKVONotifying_***` 这样的规则。
|
||||
|
||||
## 类簇
|
||||
|
||||
类簇(Class Cluster )是抽象工厂模式在 OC 数组中的实现,NSArray、NSNumber、NSString、NSDictionary 都有体现。借口简单性和拓展性的权衡体现。系统隐藏了较多实现细节,只暴露出简单接口。
|
||||
|
||||
```objectivec
|
||||
- (void)classCus
|
||||
{
|
||||
id obj1 = [NSArray alloc]; // __NSPlaceholderArray
|
||||
id obj2 = [NSMutableArray alloc]; // __NSPlaceholderArray
|
||||
id obj3 = [obj1 init]; // __NSArray0
|
||||
id obj4 = [obj2 init]; // __NSArrayM
|
||||
NSLog(@"%@ %@ %@ %@", obj1, obj2, obj3, obj4);
|
||||
}
|
||||
```
|
||||
|
||||
调用 alloc 之后产生的是 `__NSPlaceholderArray` 不符合预期。继续调用 init 发现满足期望了。所以猜测 `__NSPlaceholderArray` 是一个中间对象,后续的 init 方法就是给中间对象发消息,再由它做工厂,生成真的对象,这里的 `__NSArray0`、`__NSArrayM` 对应 NSArray、NSMutableArray
|
||||
|
||||
Foundation用了静态实例地址方式来实现,伪代码如下:
|
||||
|
||||
```objectivec
|
||||
static __NSPlacehodlerArray *GetPlaceholderForNSArray() {
|
||||
static __NSPlacehodlerArray *instanceForNSArray;
|
||||
if (!instanceForNSArray) {
|
||||
instanceForNSArray = [[__NSPlacehodlerArray alloc] init];
|
||||
}
|
||||
return instanceForNSArray;
|
||||
}
|
||||
|
||||
static __NSPlacehodlerArray *GetPlaceholderForNSMutableArray() {
|
||||
static __NSPlacehodlerArray *instanceForNSMutableArray;
|
||||
if (!instanceForNSMutableArray) {
|
||||
instanceForNSMutableArray = [[__NSPlacehodlerArray alloc] init];
|
||||
}
|
||||
return instanceForNSMutableArray;
|
||||
}
|
||||
// NSArray实现
|
||||
+ (id)alloc {
|
||||
if (self == [NSArray class]) {
|
||||
return GetPlaceholderForNSArray()
|
||||
}
|
||||
}
|
||||
// NSMutableArray实现
|
||||
+ (id)alloc {
|
||||
if (self == [NSMutableArray class]) {
|
||||
return GetPlaceholderForNSMutableArray()
|
||||
}
|
||||
}
|
||||
// __NSPlacehodlerArray实现
|
||||
- (id)init {
|
||||
if (self == GetPlaceholderForNSArray()) {
|
||||
self = [[__NSArrayI alloc] init];
|
||||
}
|
||||
else if (self == GetPlaceholderForNSMutableArray()) {
|
||||
self = [[__NSArrayM alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
```
|
||||
|
||||
另外 iOS Foundation 对静态不可变空对象(当前 case 为数组)做了优化
|
||||
|
||||
```objectivec
|
||||
NSArray *a1 = [[NSArray alloc] init];
|
||||
NSArray *a2 = [[NSArray alloc] init];
|
||||
NSArray *a3 = [[NSArray alloc] init];
|
||||
(lldb) p a1
|
||||
(__NSArray0 *) $0 = 0x0000000109f50a10 @"0 elements"
|
||||
(lldb) p a2
|
||||
(__NSArray0 *) $1 = 0x0000000109f50a10 @"0 elements"
|
||||
(lldb) p a3
|
||||
(__NSArray0 *) $2 = 0x0000000109f50a10 @"0 elements"
|
||||
(lldb)
|
||||
```
|
||||
|
||||
若干个不可变的空数组间没有任何特异性,返回一个静态对象。
|
||||
|
||||
## OC 对象内存管理
|
||||
|
||||
iOS 中使用引用计数来管理 OC 对象的内存。一个新创建的 OC 对象引用计数默认是1,当引用计数减为 0,OC 对象就会销毁,释放其占用的内存空间
|
||||
@@ -947,9 +1037,9 @@ struct FinishARCDealloc : EHScopeStack::Cleanup {
|
||||
|
||||
- ARC 模式下 `[super dealloc]` 由 llvm 编译器自动插入(CodeGen)
|
||||
|
||||
## 自动释放池底层原理探索
|
||||
## AutoreleasePool 底层原理探索
|
||||
|
||||
上 Demo
|
||||
### 单 AutoreleasePool 的 case
|
||||
|
||||
```objectivec
|
||||
int main(int argc, const char * argv[]) {
|
||||
@@ -1069,6 +1159,8 @@ static inline void *push() {
|
||||
}
|
||||
```
|
||||
|
||||
### 多 AutoreleasePool 的 case
|
||||
|
||||
来个骚一些的例子
|
||||
|
||||
```objectivec
|
||||
@@ -1097,6 +1189,8 @@ main 方法内部3个 autoreleasepool 底层怎么样工作的?
|
||||
|
||||
紧接着第二个大括号结束,第二个结构体对象析构函数执行,内部调用 `AutoreleasePoolPage::pop` 方法,会从最后一个入栈的对象开始发送 release 消息,直到遇到 `POOL_BOUNDARY` 对象。
|
||||
|
||||
所以,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响
|
||||
|
||||
第一个同理。
|
||||
|
||||
小窍门,对于上述原理的分析可以用源码中看到的 `AutoreleasePoolPage` 对象的 `printAll` 方法。
|
||||
@@ -1851,9 +1945,33 @@ static inline id *autoreleaseFast(id obj) {
|
||||
|
||||
查看 NSObject autorelease 方法调用链路可以看到最后还是调用 AutoreleasePoolPage 的 add 方法(会判断有没有页、有没有满)
|
||||
|
||||
## autorelease 对象什么时候调用 release 方法
|
||||
### 容器类会自动添加 AutoreleasePool
|
||||
|
||||
其实也就是 autorelease 和 RunLoop 的关系。
|
||||
系统容器类,在使用 block 枚举器的时候,内部会自动创建 AutoreleasePool
|
||||
|
||||
```objectivec
|
||||
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
||||
@autoreleasepool {
|
||||
<#statements#>
|
||||
}
|
||||
}];
|
||||
```
|
||||
|
||||
所以,我们老老实实写的 for、while 循环中需要手加局部 AutoreleasePool。推荐使用系统提供的容器类的 block 枚举器。
|
||||
|
||||
### autorelease 对象什么时候调用 release 方法
|
||||
|
||||
每当进行一次`objc_autoreleasePoolPush`调用时,runtime向当前的AutoreleasePoolPage中add进一个`哨兵对象`,值为0(也就是个nil),那么这一个page就变成了下面的样子:
|
||||
|
||||

|
||||
|
||||
`objc_autoreleasePoolPush`的返回值正是这个哨兵对象的地址,被`objc_autoreleasePoolPop(哨兵对象)`作为入参,于是:
|
||||
|
||||
1. 根据传入的哨兵对象地址找到哨兵对象所处的page
|
||||
2. 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次`- release`消息,并向回移动`next`指针到正确位置
|
||||
3. 补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
|
||||
|
||||
其次,AutoreleasePool 和 RunLoop 的也有关系
|
||||
|
||||
iOS 在主线程的 Runloop 中注册了2个 Observer
|
||||
|
||||
@@ -1877,4 +1995,146 @@ iOS 在主线程的 Runloop 中注册了2个 Observer
|
||||
|
||||
- 11 没任务将要休眠,调用 `objc_autoreleasePoolPop`
|
||||
|
||||
可以看到 objc_autoreleasePoolPush、objc_autoreleasePoolPop 成对调用,贯穿 RunLoop
|
||||
可以看到 objc_autoreleasePoolPush、objc_autoreleasePoolPop 成对调用,贯穿 RunLoop
|
||||
|
||||
## 内存问题典型 case
|
||||
|
||||
### OC 中有没有不对内存进行强持有的集合类型?
|
||||
|
||||
NSHashMap、NSMapTable 都可以描述 key、value 的内存修饰。
|
||||
|
||||
数组有 NSPointerArray 内部持有的是对象的指针,并非直接保存对象。不过 oc 转指针需要加 `(__bridge void*)` 进行修饰。NSPointerArray 的构造方法中可以通过 NSPointerFunctionsOptions 来声明内存的控制。
|
||||
|
||||
```objectivec
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
Person *p1 = [[Person alloc] init];
|
||||
Person *p2 = [[Person alloc] init];
|
||||
Person *p3 = [[Person alloc] init];
|
||||
NSPointerArray *arrays = [[NSPointerArray alloc] initWithOptions:NSPointerFunctionsWeakMemory];
|
||||
// NSMutableArray *array = [NSMutableArray array];
|
||||
// [array addObject:p1];
|
||||
// [array addObject:p2];
|
||||
// [array addObject:p3];
|
||||
[arrays addPointer:(__bridge void *)p1];
|
||||
[arrays addPointer:(__bridge void *)p2];
|
||||
[arrays addPointer:(__bridge void *)p3];
|
||||
p1 = nil;
|
||||
p2 = nil;
|
||||
// 断点设置到 NSLog,可以看到 Person 马上释放了
|
||||
NSLog(@"%@", arrays);
|
||||
}
|
||||
2022-05-24 21:57:27.071793+0800 TTTTW[63427:2087468] -[Person dealloc]
|
||||
2022-05-24 21:57:27.071916+0800 TTTTW[63427:2087468] -[Person dealloc]
|
||||
(lldb)
|
||||
```
|
||||
|
||||
### NSError 内存泄漏的 case
|
||||
|
||||
同事问了一个问题,下面的代码存在什么问题?
|
||||
|
||||
据说是 Zoom 这个公司的面试题,看了下其实就是考察 NSError 有没有踩过坑。怎么理解呢
|
||||
|
||||
```objectivec
|
||||
- (BOOL)isZoomUserWithUserID:(NSInteger)userID error:(NSError **)error
|
||||
{
|
||||
@autoreleasepool {
|
||||
NSString *errorMessage = [[NSString alloc] initWithFormat:@"the user is not zoom user"];
|
||||
if (userID == 100) {
|
||||
*error = [NSError errorWithDomain:@"com.test" code:userID userInfo:@{NSLocalizedDescriptionKey: errorMessage}];
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self test];
|
||||
}
|
||||
|
||||
- (void)test {
|
||||
for (NSInteger index = 0; index <= 100; index++) {
|
||||
NSString *str;
|
||||
str = [NSString stringWithFormat:@"welcome to zoom:%ld", index];
|
||||
str = [str stringByAppendingString:@" user"];
|
||||
NSError *error = NULL;
|
||||
if ([self isZoomUserWithUserID:index error:&error]) {
|
||||
NSLog(@"%@", str);
|
||||
} else {
|
||||
NSLog(@"%@", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这段代码运行会 crash,信息如下
|
||||
|
||||

|
||||
|
||||
原因是 NSError 构造方法内部会加 autorelease。源码如下
|
||||
|
||||
```objectivec
|
||||
#define AUTORELEASE(object) [(id)(object) autorelease]
|
||||
+ (id) errorWithDomain: (NSErrorDomain)aDomain
|
||||
code: (NSInteger)aCode
|
||||
userInfo: (NSDictionary*)aDictionary
|
||||
{
|
||||
NSError *e = [self allocWithZone: NSDefaultMallocZone()];
|
||||
|
||||
e = [e initWithDomain: aDomain code: aCode userInfo: aDictionary];
|
||||
return AUTORELEASE(e);
|
||||
}
|
||||
```
|
||||
|
||||
MRC 下的 `[(id)(object) autorelease]` 等价于 ARC 下的 `id __autoreleasing obj`
|
||||
|
||||
所以这个问题的本质就是 `autoreleasepool` 和 `__autoreleasing` 的问题
|
||||
|
||||
> `__autoreleasing` is used to denote arguments that are passed by reference (`id *`) and are autoreleased on return.
|
||||
|
||||
用 `__autoreleasing` 修饰的变量会被添加到当前的 autoreleasepool 中。
|
||||
|
||||
方法的 Out Parameters 参数会自动添加 __autoreleasing 属性。当方法参数里面有 Out Parameters 参数时,就是有指针的指针类型时,编译器会自动为参数加上`__autoreleasing` 属性。改如下
|
||||
|
||||
```objectivec
|
||||
- (BOOL)isZoomUserWithUserID:(NSInteger)userID error:(NSError **)error
|
||||
{
|
||||
NSError *temp;
|
||||
@autoreleasepool {
|
||||
NSString *errorMessage = [[NSString alloc] initWithFormat:@"the user is not zoom user"];
|
||||
if (userID == 100) {
|
||||
temp = [NSError errorWithDomain:@"com.test" code:userID userInfo:@{NSLocalizedDescriptionKey: errorMessage}];
|
||||
}
|
||||
}
|
||||
*error = temp;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self test];
|
||||
}
|
||||
|
||||
- (void)test {
|
||||
for (NSInteger index = 0; index <= 100; index++) {
|
||||
NSString *str;
|
||||
str = [NSString stringWithFormat:@"welcome to zoom:%ld", index];
|
||||
str = [str stringByAppendingString:@" user"];
|
||||
NSError * __autoreleasing error = NULL;
|
||||
if ([self isZoomUserWithUserID:index error:&error]) {
|
||||
NSLog(@"%@", str);
|
||||
} else {
|
||||
NSLog(@"%@", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我写了个僵尸对象检测工具,效果如下
|
||||
|
||||

|
||||
|
||||
可以定位僵尸对象,并且打印出具体堆栈,并模拟系统行为调用 `abort` 。对监控原理和工具实现感兴趣的可以查看这里[带你打造一套 APM 监控系统-内存监控之野指针/内存泄漏监控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md#zombieSniffer)
|
||||
|
||||
Demo [👇这里](https://github.com/FantasticLBP/BlogDemos/tree/master/僵尸对象探针)
|
||||
@@ -22,11 +22,11 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
|
||||
|
||||
### 1. 屏幕绘制原理
|
||||
|
||||

|
||||

|
||||
|
||||
讲讲老式的 CRT 显示器的原理。 CRT 电子枪按照上面方式,从上到下一行行扫描,扫面完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;当一帧画面绘制完成后,电子枪恢复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(Vertical synchronization),简称 VSync。显示器通常以固定的频率进行刷新,这个固定的刷新频率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏,但是原理保持不变。
|
||||
|
||||

|
||||

|
||||
|
||||
通常,屏幕上一张画面的显示是由 CPU、GPU 和显示器是按照上图的方式协同工作的。CPU 根据工程师写的代码计算好需要现实的内容(比如视图创建、布局计算、图片解码、文本绘制等),然后把计算结果提交到 GPU,GPU 负责图层合成、纹理渲染,随后 GPU 将渲染结果提交到帧缓冲区。随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器显示。
|
||||
|
||||
@@ -36,7 +36,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
|
||||
|
||||
为了解决这个问题,GPU 通常有一个机制叫垂直同步信号(V-Sync),当开启垂直同步信号后,GPU 会等到视频控制器发送 V-Sync 信号后,才进行新的一帧的渲染和帧缓冲区的更新。这样的几个机制解决了画面撕裂的情况,也增加了画面流畅度。但需要更多的计算资源
|
||||
|
||||

|
||||

|
||||
|
||||
答疑
|
||||
|
||||
@@ -48,7 +48,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
|
||||
|
||||
揭秘。请看下图
|
||||
|
||||

|
||||

|
||||
|
||||
当第一次 V-Sync 信号到来时,先渲染好一帧图像放到帧缓冲区,但是不展示,当收到第二个 V-Sync 信号后读取第一次渲染好的结果(视频控制器的指针指向第一个帧缓冲区),并同时渲染新的一帧图像并将结果存入第二个帧缓冲区,等收到第三个 V-Sync 信号后,读取第二个帧缓冲区的内容(视频控制器的指针指向第二个帧缓冲区),并开始第三帧图像的渲染并送入第一个帧缓冲区,依次不断循环往复。
|
||||
|
||||
@@ -56,7 +56,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
|
||||
|
||||
### 2. 卡顿产生的原因
|
||||
|
||||

|
||||

|
||||
|
||||
VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPU,GPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内,CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃(也叫掉帧),等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
|
||||
|
||||
@@ -75,7 +75,7 @@ RunLoop 负责监听输入源进行调度处理。比如网络、输入设备、
|
||||
|
||||
RunLoop 状态如下图
|
||||
|
||||

|
||||

|
||||
|
||||
第一步:通知 Observers,RunLoop 要开始进入 loop,紧接着进入 loop
|
||||
|
||||
@@ -251,9 +251,9 @@ if (sourceHandledThisLoop && stopAfterHandle) {
|
||||
}
|
||||
```
|
||||
|
||||
完整且带有注释的 RunLoop 代码见[此处](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CFRunLoop.c)。 Source1 是 RunLoop 用来处理 Mach port 传来的系统事件的,Source0 是用来处理用户事件的。收到 Source1 的系统事件后本质还是调用 Source0 事件的处理函数。
|
||||
完整且带有注释的 RunLoop 代码见[此处](./../assets/CFRunLoop.c)。 Source1 是 RunLoop 用来处理 Mach port 传来的系统事件的,Source0 是用来处理用户事件的。收到 Source1 的系统事件后本质还是调用 Source0 事件的处理函数。
|
||||
|
||||

|
||||

|
||||
RunLoop 6 个状态
|
||||
|
||||
```Objective-C
|
||||
@@ -286,13 +286,13 @@ WatchDog 在不同状态下具有不同的值。
|
||||
|
||||
通过 `long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)` 方法判断是否阻塞主线程,`Returns zero on success, or non-zero if the timeout occurred.` 返回非 0 则代表超时阻塞了主线程。
|
||||
|
||||

|
||||

|
||||
|
||||
可能很多人纳闷 RunLoop 状态那么多,为什么选择 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting?因为大部分卡顿都是在 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting 之间。比如 Source0 类型的 App 内部事件等
|
||||
|
||||
Runloop 检测卡顿流程图如下:
|
||||
|
||||

|
||||

|
||||
|
||||
关键代码如下:
|
||||
|
||||
@@ -371,7 +371,7 @@ while (self.isCancelled == NO) {
|
||||
在计算机科学中,调用堆栈是一种栈类型的数据结构,用于存储有关计算机程序的线程信息。这种栈也叫做执行堆栈、程序堆栈、控制堆栈、运行时堆栈、机器堆栈等。调用堆栈用于跟踪每个活动的子例程在完成执行后应该返回控制的点。
|
||||
|
||||
维基百科搜索到 “Call Stack” 的一张图和例子,如下
|
||||

|
||||

|
||||
上图表示为一个栈。分为若干个栈帧(Frame),每个栈帧对应一个函数调用。下面蓝色部分表示 `DrawSquare` 函数,它在执行的过程中调用了 `DrawLine` 函数,用绿色部分表示。
|
||||
|
||||
可以看到栈帧由三部分组成:函数参数、返回地址、局部变量。比如在 DrawSquare 内部调用了 DrawLine 函数:第一先把 DrawLine 函数需要的参数入栈;第二把返回地址(控制信息。举例:函数 A 内调用函数 B,调用函数 B 的下一行代码的地址就是返回地址)入栈;第三函数内部的局部变量也在该栈中存储。
|
||||
@@ -464,11 +464,11 @@ static mach_port_t main_thread_id;
|
||||
|
||||
这里有个策略,卡顿是由于主线程某个或某组方法执行过长而造成的卡顿,所以卡顿堆栈只需要分析主线程即可,但是此时是未符号化的方法(完成流程如下)
|
||||
|
||||

|
||||

|
||||
|
||||
测试过,单次抓取主线程符号耗时大概1ms左右。因此连续抓取堆栈是可取方案。
|
||||
|
||||

|
||||

|
||||
|
||||
按照栈顶函数地址和当前堆栈的深度作为判断依据,如果某个堆栈栈顶地址和深度相同,则出现次数最多的一个堆栈,可以认为是一个卡顿堆栈,因为认为当发生卡顿的时候,函数堆栈大概率不会变化。
|
||||
|
||||
@@ -505,7 +505,7 @@ static mach_port_t main_thread_id;
|
||||
|
||||
上传这些信息到服务端后,APM 后端调用符号化服务的能力,符号化机器找到对应的 DSYM 文件,调用 atos 的指令进行符号化,如下效果。
|
||||
|
||||

|
||||

|
||||
|
||||
系统堆栈符号化的问题,也就是获取系统符号文件的问题。可以通过 ipsw,也就是刷机所需要的固件信息。
|
||||
|
||||
@@ -517,7 +517,7 @@ static mach_port_t main_thread_id;
|
||||
|
||||
服务端聚合策略
|
||||
|
||||

|
||||

|
||||
|
||||
找到最接近栈顶的符合占比条件的业务代码方法,作为卡顿堆栈归类 key。
|
||||
|
||||
@@ -551,7 +551,7 @@ curNode.costTime > (parentNode.costTime * 1.1/n) && curNode.costTime > parentNod
|
||||
|
||||
应用启动时间是影响用户体验的重要因素之一,所以我们需要量化去衡量一个 App 的启动速度到底有多快。启动分为冷启动和热启动。
|
||||
|
||||

|
||||

|
||||
|
||||
冷启动:App 尚未运行,必须加载并构建整个应用。完成应用的初始化。冷启动存在较大优化空间。冷启动时间从 `application: didFinishLaunchingWithOptions:` 方法开始计算,App 一般在这里进行各种 SDK 和 App 的基础初始化工作。
|
||||
|
||||
@@ -586,10 +586,10 @@ App 启动过程:
|
||||
- 程序执行:调用 main();调用 UIApplicationMain();调用 applicationWillFinishLaunching();
|
||||
|
||||
Pre-Main 阶段
|
||||

|
||||

|
||||
|
||||
Main 阶段
|
||||

|
||||

|
||||
|
||||
#### 2.1 加载 Dylib
|
||||
|
||||
@@ -673,9 +673,9 @@ Main 阶段
|
||||
|
||||
### 4. 精确版启动时间监控
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
进程创建:通过 sysctl 可以拿到
|
||||
|
||||
@@ -685,7 +685,7 @@ didFinishLaunching:通过 UIApplicationDidFinishLaunchingNotification 拿到
|
||||
|
||||
首屏渲染时间怎么拿?能获取到 `CA::Transaction::commit()` 时刻即可。
|
||||
|
||||

|
||||

|
||||
|
||||
对 UI 进行修改后会在主线程休眠前触发 `CA::Transaction::commit()`,其实就是打包 Render Tree,通过 IPC 发送给 Render Server。
|
||||
|
||||
@@ -697,21 +697,21 @@ didFinishLaunching:通过 UIApplicationDidFinishLaunchingNotification 拿到
|
||||
|
||||
- Layout 布局:调用 `layout` 等与布局相关的 API
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
- Display 绘制:调用 `drawRect` 等与绘制相关方法
|
||||
|
||||

|
||||

|
||||
|
||||
- Prepare:图片解码
|
||||
|
||||
- Commit:递归打包 Render Tree,通过 IPC 计数发送给 Render Server
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
断点调试完,`[BSXPCServiceConnectionProxy invokeMethod:onTarget:withMessage:forConnection:]` 底层调用的是 send 方法。send 方法调用 `BSXPCServiceConnectionMessageReply send` 方法。
|
||||
|
||||
@@ -892,14 +892,14 @@ App 内存不足时,系统会按照一定策略来腾出更多的空间供使
|
||||
|
||||
Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能一个 page 持有多个对象,也可能一个大的对象跨越多个 page。通常它是 16KB 大小,且有 3 种类型的 page。
|
||||
|
||||

|
||||

|
||||
|
||||
- Clean Memory
|
||||
Clean memory 包括 3 类:可以 `page out` 的内存、内存映射文件、App 使用到的 framework(每个 framework 都有 \_DATA_CONST 段,通常都是 clean 状态,但使用 runtime swizling,那么变为 dirty)。
|
||||
|
||||
一开始分配的 page 都是干净的(堆里面的对象分配除外),我们 App 数据写入时候变为 dirty。从硬盘读进内存的文件,也是只读的、clean page。
|
||||
|
||||

|
||||

|
||||
|
||||
- Dirty Memory
|
||||
|
||||
@@ -907,7 +907,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
|
||||
|
||||
在使用 framework 的过程中会产生 Dirty memory,使用单例或者全局初始化方法有助于帮助减少 Dirty memory(因为单例一旦创建就不销毁,一直在内存中,系统不认为是 Dirty memory)。
|
||||
|
||||

|
||||

|
||||
|
||||
- Compressed Memory
|
||||
|
||||
@@ -918,7 +918,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
|
||||
App 运行内存 = pageNumbers \* pageSize。因为 Compressed Memory 属于 Dirty memory。所以 Memory footprint = dirtySize + CompressedSize
|
||||
|
||||
设备不同,内存占用上限不同,App 上限较高,extension 上限较低,超过上限 crash 到 `EXC_RESOURCE_EXCEPTION`。
|
||||

|
||||

|
||||
|
||||
接下来谈一下如何获取内存上限,以及如何监控 App 因为占用内存过大而被强杀。
|
||||
|
||||
@@ -1711,7 +1711,7 @@ static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason)
|
||||
|
||||
FacekBook 提出排除法监控 OOM。
|
||||
|
||||

|
||||

|
||||
|
||||
- App 更新了版本
|
||||
|
||||
@@ -2158,7 +2158,7 @@ objc 源码中 dealloc 方法注释说了 dealloc 的实现会被僵尸对象所
|
||||
|
||||
开启 Instrucments 分析查看得到,调用了 `__dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下
|
||||
|
||||

|
||||

|
||||
|
||||
通过符号名称大概可以才到系统会调用 dealloc 方法时,类似 KVO,派生出一个新的僵尸类类,将当前类的 isa 指针指向僵尸类。
|
||||
|
||||
@@ -2367,7 +2367,7 @@ static void GSLogZombie(id o, SEL sel){
|
||||
|
||||
申请内存 alloc 时在内存上填 `0xAA`,释放内存 dealloc 时在内存上填 `0x55`。此种方案也会让内存泄漏偶现的 crash 变为必现。
|
||||
|
||||
### 4. 野指针监控工具
|
||||
### 4. 野指针监控工具<a name="zombieSniffer"></a>
|
||||
|
||||
##### 类 Zombie Object 方案
|
||||
|
||||
@@ -2843,7 +2843,7 @@ bool init_safe_free(void)
|
||||
|
||||
注意:ZombieProxy、ZombieObjectDetector 2个类要在 Build Phases 设置为非 ARC,加 `-fno-objc-arc`
|
||||
|
||||

|
||||

|
||||
|
||||
## 六、 App 网络监控
|
||||
|
||||
@@ -2851,7 +2851,7 @@ bool init_safe_free(void)
|
||||
|
||||
### 1. App 网络请求过程
|
||||
|
||||

|
||||

|
||||
|
||||
App 发送一次网络请求一般会经历下面几个关键步骤:
|
||||
|
||||
@@ -2889,7 +2889,7 @@ App 发送一次网络请求一般会经历下面几个关键步骤:
|
||||
|
||||
iOS 网络框架层级关系如下:
|
||||
|
||||

|
||||

|
||||
|
||||
iOS 网络现状是由 4 层组成的:最底层的 BSD Sockets、SecureTransport;次级底层是 CFNetwork、NSURLSession、NSURLConnection、WebView 是用 Objective-C 实现的,且调用 CFNetwork;应用层框架 AFNetworking 基于 NSURLSession、NSURLConnection 实现。
|
||||
|
||||
@@ -3477,11 +3477,11 @@ iOS 中 hook 技术有 2 类,一种是 NSProxy,一种是 method swizzling(
|
||||
|
||||
但是如果需要监全部的网络请求就不能满足需求了,查阅资料后发现了阿里百川有 APM 的解决方案,于是有了方案 3,对于网络监控需要做如下的处理
|
||||
|
||||

|
||||

|
||||
|
||||
可能对于 CFNetwork 比较陌生,可以看一下 CFNetwork 的层级和简单用法
|
||||
|
||||

|
||||

|
||||
|
||||
CFNetwork 的基础是 CFSocket 和 CFStream。
|
||||
|
||||
@@ -3578,9 +3578,9 @@ void printResponseData (CFDataRef responseData) {
|
||||
|
||||
NSURLSession、NSURLConnection hook 如下。
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
业界有 APM 针对 CFNetwork 的方案,整理描述下:
|
||||
|
||||
@@ -3867,7 +3867,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
|
||||
};
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
method swizzling 改进版如下
|
||||
|
||||
@@ -3894,7 +3894,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
|
||||
typedef struct objc_object *id;
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
我们来分析一下为什么修改 `isa` 可以实现目的呢?
|
||||
|
||||
@@ -4052,11 +4052,11 @@ self.didCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForN
|
||||
|
||||
HTTP 请求报文结构
|
||||
|
||||

|
||||

|
||||
|
||||
响应报文的结构
|
||||
|
||||

|
||||

|
||||
|
||||
1. HTTP 报文是格式化的数据块,每条报文由三部分组成:对报文进行描述的起始行、包含属性的首部块、以及可选的包含数据的主体部分。
|
||||
2. 起始行和手部就是由行分隔符的 ASCII 文本,每行都以一个由 2 个字符组成的行终止序列作为结束(包括一个回车符、一个换行符)
|
||||
@@ -4083,11 +4083,11 @@ HTTP 请求报文结构
|
||||
|
||||
下图是打开 Chrome 查看极课时间网页的请求信息。包括响应行、响应头、响应体等信息。
|
||||
|
||||

|
||||

|
||||
|
||||
下图是在终端使用 `curl` 查看一个完整的请求和响应数据
|
||||
|
||||

|
||||

|
||||
|
||||
我们都知道在 HTTP 通信中,响应数据会使用 gzip 或其他压缩方式压缩,用 NSURLProtocol 等方案监听,用 NSData 类型去计算分析流量等会造成数据的不精确,因为正常一个 HTTP 响应体的内容是使用 gzip 或其他压缩方式压缩的,所以使用 NSData 会偏大。
|
||||
|
||||
@@ -4624,7 +4624,7 @@ Mach 已经通过异常机制提供了底层的陷进处理,而 BSD 则在异
|
||||
|
||||
Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,并通过 `threadsignal` 将信号投递到出错的线程。
|
||||
|
||||

|
||||

|
||||
|
||||
### 2. Crash 收集方式
|
||||
|
||||
@@ -4688,7 +4688,7 @@ KSCrash 功能齐全,可以捕获如下类型的 Crash
|
||||
|
||||
流程图如下:
|
||||
|
||||

|
||||

|
||||
|
||||
对于 Mach 异常捕获,可以注册一个异常端口,该端口负责对当前任务的所有线程进行监听。
|
||||
|
||||
@@ -4999,7 +4999,7 @@ static void restoreExceptionPorts(void)
|
||||
|
||||
KSCrash 在这里的处理逻辑如下图:
|
||||
|
||||

|
||||

|
||||
|
||||
看一下关键代码:
|
||||
|
||||
@@ -5455,7 +5455,7 @@ static void setEnabled(bool isEnabled)
|
||||
|
||||
其他几个 crash 也是一样,异常信息经过包装交给 `kscm_handleException()` 函数处理。可以看到这个函数被其他几种 crash 捕获后所调用。
|
||||
|
||||

|
||||

|
||||
|
||||
```c
|
||||
/** Start general exception processing.
|
||||
@@ -5956,7 +5956,7 @@ static int64_t getReportIDFromFilename(const char* filename)
|
||||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
#### 2.7 前端 js 相关的 Crash 的监控
|
||||
|
||||
@@ -5980,7 +5980,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) {
|
||||
};
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
##### 2.7.3 React Native 异常监控
|
||||
|
||||
@@ -6003,7 +6003,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) {
|
||||
|
||||
模拟器点击 `command + d` 调出面板,选择 Debug,打开 Chrome 浏览器, Mac 下快捷键 `Command + Option + J` 打开调试面板,就可以像调试 React 一样调试 RN 代码了。
|
||||
|
||||

|
||||

|
||||
|
||||
查看到 crash stack 后点击可以跳转到 sourceMap 的地方。
|
||||
|
||||
@@ -6027,7 +6027,7 @@ Tips:RN 项目打 Release 包
|
||||
|
||||
现象:iOS 项目奔溃。截图以及日志如下
|
||||
|
||||

|
||||

|
||||
|
||||
```shell
|
||||
2020-06-22 22:26:03.318 [info][tid:main][RCTRootView.m:294] Running application todos ({
|
||||
@@ -6121,7 +6121,7 @@ global.ErrorUtils.setGlobalHandler((e) => {
|
||||
|
||||
现象:iOS 项目不奔溃。日志信息如下,对比 bundle 包中的 js。
|
||||
|
||||

|
||||

|
||||
|
||||
结论:
|
||||
|
||||
@@ -6256,7 +6256,6 @@ export type ErrorUtilsT = typeof ErrorUtils;
|
||||
所以 RN 的异常可以使用 `global.ErrorUtils` 来设置错误处理。举个例子
|
||||
|
||||
```js
|
||||
|
||||
const defaultHandler = ErrorUtils.getGlobalHandler && ErrorUtils.getGlobalHandler();
|
||||
|
||||
ErrorUtils.setGlobalHandler( (error: Error, isFatal?: boolean) => {
|
||||
@@ -6280,8 +6279,6 @@ ErrorUtils.setGlobalHandler( (error: Error, isFatal?: boolean) => {
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
红屏产生的原因是 ErrorUtils.setGlobalHandler 捕获全局错误后,调用 LogBox 来显示红屏。红屏报错逻辑涉及 RN 框架源码中2个文件,分别为 [setUpErrorHandling.js](https://github.com/facebook/react-native/blob/8bd3edec88148d0ab1f225d2119435681fbbba33/Libraries/Core/setUpErrorHandling.js#L32) 和 [ExceptionsManager.js](https://github.com/facebook/react-native/blob/b633cc130533f0731b2577123282c4530e4f0abe/Libraries/Core/ExceptionsManager.js#L98-L103),关键代码如下
|
||||
|
||||
```js
|
||||
@@ -6329,8 +6326,6 @@ if (global?.HermesInternal?.hasPromise?.()) {
|
||||
|
||||
定义配置项 `defualtRejectionTrackingOptions`,其中的 onHnhandled 回调函数,该回调函数主要来处理未被 catch 的 Promise 错误。然后通过 `global?.HermesInternal?.hasPromise` 来判断 RN 是否采用 Hermes 引擎。如果采用 Hermes 引擎,则使用 `enablePromiseRejectionTracker` 方法来捕获未被 catch 的 Promise 错误。如果不是 Hermes 引擎则使用第三方 Promise 库中的 `rejection-tracking` 文件中的 enable 方法来捕获未被 catch 的 Promise 错误。
|
||||
|
||||
|
||||
|
||||
###### 2.7.3.3 组件 render 错误
|
||||
|
||||
其实对于 RN 的异常监控中 Javascript 错误和未捕获的 Promise 错误外,还有一类需要就是 React/React Native 的 render 错误。
|
||||
@@ -6433,7 +6428,7 @@ parseJSError(line, column);
|
||||
|
||||
5. 拿脚本解析好的行号、列号、文件信息去和源代码文件比较,结果很正确。
|
||||
|
||||

|
||||

|
||||
|
||||
##### 2.7.5 SourceMap 解析系统设计
|
||||
|
||||
@@ -6725,7 +6720,7 @@ parseJSError(line, column);
|
||||
|
||||
`.DSYM` 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,发布的时候为了安全,会把调试信息存储在单独的文件,`.DSYM` 其实是一个文件目录,结构如下:
|
||||
|
||||

|
||||

|
||||
|
||||
#### 4.2 DWARF 文件
|
||||
|
||||
@@ -7257,7 +7252,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
|
||||
/Users/你自己的用户名/Library/Developer/Xcode/iOS DeviceSupport/
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
### 5. 服务端处理
|
||||
|
||||
@@ -7267,7 +7262,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
|
||||
|
||||
早期单体应用时代,几乎应用的所有功能都在一台机器上运行,出了问题,运维人员打开终端输入命令直接查看系统日志,进而定位问题、解决问题。随着系统的功能越来越复杂,用户体量越来越大,单体应用几乎很难满足需求,所以技术架构迭代了,通过水平拓展来支持庞大的用户量,将单体应用进行拆分为多个应用,每个应用采用集群方式部署,负载均衡控制调度,假如某个子模块发生问题,去找这台服务器上终端找日志分析吗?显然台落后,所以日志管理平台便应运而生。通过 Logstash 去收集分析每台服务器的日志文件,然后按照定义的正则模版过滤后传输到 Kafka 或 Redis,然后由另一个 Logstash 从 Kafka 或 Redis 上读取日志存储到 ES 中创建索引,最后通过 Kibana 进行可视化分析。此外可以将收集到的数据进行数据分析,做更进一步的维护和决策。
|
||||
|
||||

|
||||

|
||||
|
||||
上图展示了一个 ELK 的日志架构图。简单说明下:
|
||||
|
||||
@@ -7277,13 +7272,13 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
|
||||
|
||||
下图贴一个 Elasticsearch 社区分享的一个 “Elastic APM 动手实战”[主题](https://elasticsearch.cn/slides/257#page=3)的内容截图。
|
||||
|
||||

|
||||

|
||||
|
||||
##### 5.2 服务侧
|
||||
|
||||
Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化处理,以方便定位问题、crash 产生报表和后续处理。
|
||||
|
||||

|
||||

|
||||
|
||||
所以整个流程就是:客户端 APM SDK 收集 crash log -> Kafka 存储 -> Mac 机执行定时任务符号化 -> 数据回传 Kafka -> 产品侧(显示端)对数据进行分类、报表、报警等操作。
|
||||
|
||||
@@ -7295,7 +7290,7 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
|
||||
|
||||
现在很多架构设计都是微服务,至于为什么选微服务,不在本文范畴。所以 crash 日志的符号化被设计为一个微服务。架构图如下
|
||||
|
||||

|
||||

|
||||
|
||||
说明:
|
||||
|
||||
@@ -7307,19 +7302,19 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
|
||||
|
||||
- 脚手架 cli 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等)
|
||||
|
||||

|
||||

|
||||
|
||||
其中符号化服务是大前端背景下大前端团队的产物,所以是 NodeJS 实现的(单线程,所以为了提高机器利用率,就要开启多进程能力)。iOS 的符号化机器是 双核的 Mac mini,这就需要做实验测评到底需要开启几个 worker 进程做符号化服务。结果是双进程处理 crash log,比单进程效率高近一倍,而四进程比双进程效率提升不明显,符合双核 mac mini 的特点。所以开启两个 worker 进程做符号化处理。
|
||||
|
||||
下图是完整设计图
|
||||
|
||||

|
||||

|
||||
|
||||
简单说明下,符号化流程是一个主从模式,一台 master 机,多个 slave 机,master 机读取 .DSYM 和 crash 结果的 cache。「数据处理和任务调度框架」调度符号化服务(内部 2 个 symbolocate worker)同时从七牛云上获取 .DSYM 文件。
|
||||
|
||||
系统架构图如下
|
||||
|
||||

|
||||

|
||||
|
||||
## 九、Weex、Flutter 异常监控
|
||||
|
||||
@@ -7615,7 +7610,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
||||
1. 当前启动时的js 预载入流程里的JS下载失败 和 进入Weex页面后的JS拉取失败 两处的上报失败未做区分,没法实际统计加载页面时有多少JS获取失败的情况。
|
||||
2. 随着业务迭代, Weex 页面越来越多,需要做 JS 拉取相关优化,避免白屏问题
|
||||
3. 目前 JS 缓存逻辑为,每个 Weex 模块单独维护一个 LRUCache,并且会做大量的预加载,但可能大多数页面根本不会访问到。浪费了内存资源去做了一些不会被调用到的页面预加载,可能还会造成 OOM 问题
|
||||

|
||||

|
||||
|
||||
// todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能,比如某个收银员角色固定的情况下,他日常的操作行为是固定的,商品扫码、开单)
|
||||
|
||||
@@ -7627,15 +7622,15 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
||||
|
||||
可能有些人一直没有遇到过因为在子线程操作 UI,导致在开发阶段 Xcode console 输出了一堆日志,大体如下
|
||||
|
||||

|
||||

|
||||
|
||||
其实我们可以给 Xcode 打个 `Runtime Issue Breakpoint` ,type 选择 `Main Thread Checker`, 在发生子线程操作 UI 的时候就会被系统检测到并触发断点,同时可以看到堆栈情况
|
||||
|
||||

|
||||

|
||||
|
||||
效果如下
|
||||
|
||||

|
||||

|
||||
|
||||
### 2. 问题及解决方案
|
||||
|
||||
@@ -7649,7 +7644,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
||||
但是某些类有些 API 我们也不希望在子线程被调用,这时候 `libMainThreadChecker.dylib`是无法满足的。
|
||||
|
||||
对 `libMainThreadChecker.dylib` 库的汇编代码研究,发现 `libMainThreadChecker.dylib` 是通过内部 `__main_thread_add_check_for_selector` 这个方法来进行类和方法的注册的。所以如果我们同样可以通过 `dlsym` 来调用该方法,以达到对自定义类和方法的主线程调用监测。
|
||||

|
||||

|
||||
|
||||
另外该功能可以在线下 debug 阶段开启,判断是否是在 Xcode debug 状态,可以通过苹果提供的[官方判断方法](https://developer.apple.com/library/archive/qa/qa1361/_index.html#//apple_ref/doc/uid/DTS10003368)实现。
|
||||
|
||||
@@ -7663,7 +7658,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
||||
|
||||
好像用 APM 的卡顿没发现啥问题。仔细看看发现了问题,我们的卡顿只是监控主线程方法耗时。一个页面加载后可能会触发一些网络请求功能,子线程请求完网络,可能会做一些数组裁剪、组装,拼接为 UI 所需要的信息,这个时候很可能会发生卡顿,所以这种 case 下卡顿监控是无法覆盖的。用下图表示
|
||||
|
||||

|
||||

|
||||
|
||||
页面作为承载用户交互的具体战场,我们需要对页面的性能有个直观的指标。业界一般有2个指标:**页面渲染时长、页面可交互时长**。
|
||||
|
||||
@@ -7775,7 +7770,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
||||
|
||||
6. 整个 APM 的架构图如下
|
||||
|
||||

|
||||

|
||||
|
||||
说明:
|
||||
|
||||
|
||||
BIN
assets/NSErrorZombieCrash.png
Normal file
BIN
assets/NSErrorZombieCrash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
BIN
assets/NSTaggedPointerOfNSNumber.png
Normal file
BIN
assets/NSTaggedPointerOfNSNumber.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 286 KiB |
BIN
assets/ZombieSinffer.png
Normal file
BIN
assets/ZombieSinffer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 904 KiB |
BIN
assets/ZombieSniffer.png
Normal file
BIN
assets/ZombieSniffer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 904 KiB |
Reference in New Issue
Block a user