docs: image url

This commit is contained in:
FantasticLBP
2026-01-02 10:28:57 +08:00
parent 7ac7513900
commit 7843661458
29 changed files with 719 additions and 719 deletions

View File

@@ -22,11 +22,11 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
### 1. 屏幕绘制原理
![老式 CRT 显示器原理](./../assets/2020-02-04-ios_screen_scan.png)
![老式 CRT 显示器原理](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-04-ios_screen_scan.png)
讲讲老式的 CRT 显示器的原理。 CRT 电子枪按照上面方式从上到下一行行扫描扫面完成后显示器就呈现一帧画面随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步显示器或者其他硬件会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行准备进行扫描时显示器会发出一个水平同步信号horizonal synchronization简称 HSync当一帧画面绘制完成后电子枪恢复到原位准备画下一帧前显示器会发出一个垂直同步信号Vertical synchronization简称 VSync。显示器通常以固定的频率进行刷新这个固定的刷新频率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏,但是原理保持不变。
![显示器和 CPU、GPU 关系](./../assets/2020-02-02-screen_display_gpu.png)
![显示器和 CPU、GPU 关系](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-02-screen_display_gpu.png)
通常,屏幕上一张画面的显示是由 CPU、GPU 和显示器是按照上图的方式协同工作的。CPU 根据工程师写的代码计算好需要显实的内容(比如视图创建、布局计算、图片解码、文本绘制等),然后把计算结果提交到 GPUGPU 负责图层合成、纹理渲染,随后 GPU 将渲染结果提交到帧缓冲区。随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器显示。
@@ -36,7 +36,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
为了解决这个问题GPU 通常有一个机制叫垂直同步信号V-Sync当开启垂直同步信号后GPU 会等到视频控制器发送 V-Sync 信号后,才进行新的一帧的渲染和帧缓冲区的更新。这样的几个机制解决了画面撕裂的情况,也增加了画面流畅度。但需要更多的计算资源
![IPC唤醒 RunLoop](./../assets/2020-02-08-ios_vsync_runloop.png)
![IPC唤醒 RunLoop](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-08-ios_vsync_runloop.png)
答疑
@@ -48,7 +48,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
揭秘。请看下图
![多缓冲区显示原理](./../assets/2020-02-04-Comparison_double_triple_buffering.png)
![多缓冲区显示原理](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-04-Comparison_double_triple_buffering.png)
当第一次 V-Sync 信号到来时,先渲染好一帧图像放到帧缓冲区,但是不展示,当收到第二个 V-Sync 信号后读取第一次渲染好的结果(视频控制器的指针指向第一个帧缓冲区),并同时渲染新的一帧图像并将结果存入第二个帧缓冲区,等收到第三个 V-Sync 信号后,读取第二个帧缓冲区的内容(视频控制器的指针指向第二个帧缓冲区),并开始第三帧图像的渲染并送入第一个帧缓冲区,依次不断循环往复。
@@ -56,7 +56,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
### 2. 卡顿产生的原因
![卡顿原因](./../assets/2020-02-04-ios_frame_drop.png)
![卡顿原因](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-04-ios_frame_drop.png)
VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 AppApp 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPUGPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃(也叫掉帧),等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
@@ -75,7 +75,7 @@ RunLoop 负责监听输入源进行调度处理。比如网络、输入设备、
RunLoop 状态如下图
![RunLoop](./../assets/4.png)
![RunLoop](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/4.png)
第一步:通知 ObserversRunLoop 要开始进入 loop紧接着进入 loop
@@ -251,9 +251,9 @@ if (sourceHandledThisLoop && stopAfterHandle) {
}
```
完整且带有注释的 RunLoop 代码见[此处](./../assets/CFRunLoop.c)。 Source1 是 RunLoop 用来处理 Mach port 传来的系统事件的Source0 是用来处理用户事件的。收到 Source1 的系统事件后本质还是调用 Source0 事件的处理函数。
完整且带有注释的 RunLoop 代码见[此处](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CFRunLoop.c)。 Source1 是 RunLoop 用来处理 Mach port 传来的系统事件的Source0 是用来处理用户事件的。收到 Source1 的系统事件后本质还是调用 Source0 事件的处理函数。
![RunLoop 状态](./../assets/2020-02-05-RunLoop.png)
![RunLoop 状态](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-05-RunLoop.png)
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-ANR](./../assets/2020-04-04-APM-RunLoopANR.jpg)
![RunLoop-ANR](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-APM-RunLoopANR.jpg)
可能很多人纳闷 RunLoop 状态那么多,为什么选择 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting因为大部分卡顿都是在 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting 之间。比如 Source0 类型的 App 内部事件等
Runloop 检测卡顿流程图如下:
![RunLoop ANR](./../assets/2020-04-04-ANRRunloop.png)
![RunLoop ANR](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-ANRRunloop.png)
关键代码如下:
@@ -371,7 +371,7 @@ while (self.isCancelled == NO) {
在计算机科学中,调用堆栈是一种栈类型的数据结构,用于存储有关计算机程序的线程信息。这种栈也叫做执行堆栈、程序堆栈、控制堆栈、运行时堆栈、机器堆栈等。调用堆栈用于跟踪每个活动的子例程在完成执行后应该返回控制的点。
维基百科搜索到 “Call Stack” 的一张图和例子,如下
![函数调用栈](./../assets/2020-02-08-StackFrame.png)
![函数调用栈](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-08-StackFrame.png)
上图表示为一个栈。分为若干个栈帧Frame每个栈帧对应一个函数调用。下面蓝色部分表示 `DrawSquare` 函数,它在执行的过程中调用了 `DrawLine` 函数,用绿色部分表示。
可以看到栈帧由三部分组成:函数参数、返回地址、局部变量。比如在 DrawSquare 内部调用了 DrawLine 函数:第一先把 DrawLine 函数需要的参数入栈;第二把返回地址(控制信息。举例:函数 A 内调用函数 B调用函数 B 的下一行代码的地址就是返回地址)入栈;第三函数内部的局部变量也在该栈中存储。
@@ -464,11 +464,11 @@ static mach_port_t main_thread_id;
这里有个策略,卡顿是由于主线程某个或某组方法执行过长而造成的卡顿,所以卡顿堆栈只需要分析主线程即可,但是此时是未符号化的方法(完成流程如下)
<img src="./../assets/callStackSymbolicate.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/callStackSymbolicate.png" style="zoom:30%" />
测试过单次抓取主线程符号耗时大概1ms左右。因此连续抓取堆栈是可取方案。
![](./../assets/callstackCostTime.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/callstackCostTime.png)
按照栈顶函数地址和当前堆栈的深度作为判断依据,如果某个堆栈栈顶地址和深度相同,则出现次数最多的一个堆栈,可以认为是一个卡顿堆栈,因为认为当发生卡顿的时候,函数堆栈大概率不会变化。
@@ -505,7 +505,7 @@ static mach_port_t main_thread_id;
上传这些信息到服务端后APM 后端调用符号化服务的能力,符号化机器找到对应的 DSYM 文件,调用 atos 的指令进行符号化,如下效果。
![](./../assets/LagStackSymbolicate.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LagStackSymbolicate.png)
系统堆栈符号化的问题,也就是获取系统符号文件的问题。可以通过 ipsw也就是刷机所需要的固件信息。
@@ -517,7 +517,7 @@ static mach_port_t main_thread_id;
服务端聚合策略
![](./../assets/CallStackGroupHash.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CallStackGroupHash.png)
找到最接近栈顶的符合占比条件的业务代码方法,作为卡顿堆栈归类 key。
@@ -632,7 +632,7 @@ Xcode 增加环境变量,添加 `DYLD_PRINT_STATISTICS` 设为1来查看 pr
应用启动时间是影响用户体验的重要因素之一,所以我们需要量化去衡量一个 App 的启动速度到底有多快。启动分为冷启动和热启动。
![App 启动时间](./../assets/2020-03-30-APMAppLaunch.png)
![App 启动时间](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-03-30-APMAppLaunch.png)
冷启动App 尚未运行,必须加载并构建整个应用。完成应用的初始化。冷启动存在较大优化空间。冷启动时间从 `application: didFinishLaunchingWithOptions:` 方法开始计算App 一般在这里进行各种 SDK 和 App 的基础初始化工作。
@@ -669,10 +669,10 @@ App 启动过程:
- 程序执行:调用 main();调用 UIApplicationMain();调用 applicationWillFinishLaunching()
Pre-Main 阶段
![Pre-Main 阶段](./../assets/2020-02-10-AppSpeed-PreMain.png)
![Pre-Main 阶段](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-10-AppSpeed-PreMain.png)
Main 阶段
![Main 阶段](./../assets/2020-02-10-AppSpeed-Main.png)
![Main 阶段](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-10-AppSpeed-Main.png)
#### 2.1 加载 Dylib
@@ -760,9 +760,9 @@ QA为什么 `+ initialize` 需要搭配 `dispatch_once`?因为 `+initialize`
### 4. 精确版启动时间监控
![](./../assets/AppStarupPipeline.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AppStarupPipeline.png)
![](./../assets/APMStartup.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/APMStartup.png)
进程创建:通过 sysctl 可以拿到
@@ -772,7 +772,7 @@ didFinishLaunching通过 UIApplicationDidFinishLaunchingNotification 拿到
首屏渲染时间怎么拿?能获取到 `CA::Transaction::commit()` 时刻即可。
![](./../assets/CATransactionCommit.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CATransactionCommit.png)
对 UI 进行修改后会在主线程休眠前触发 `CA::Transaction::commit()`,其实就是打包 Render Tree通过 IPC 发送给 Render Server。
@@ -784,21 +784,21 @@ didFinishLaunching通过 UIApplicationDidFinishLaunchingNotification 拿到
- Layout 布局:调用 `layout` 等与布局相关的 API
![](./../assets/CoreAnimationPipeline1.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline1.png)
![](./../assets/CoreAnimationPipeline2.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline2.png)
- Display 绘制:调用 `drawRect` 等与绘制相关方法
![](./../assets/CoreAnimationPipeline3.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline3.png)
- Prepare图片解码
- Commit递归打包 Render Tree通过 IPC 计数发送给 Render Server
![](./../assets/CoreAnimationPipeline4.png)
![](./../assets/CoreAnimationPipeline5.png)
![](./../assets/CoreAnimationPipeline6.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline4.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline5.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline6.png)
断点调试完,`[BSXPCServiceConnectionProxy invokeMethod:onTarget:withMessage:forConnection:]` 底层调用的是 send 方法。send 方法调用 `BSXPCServiceConnectionMessageReply send` 方法。
@@ -1006,14 +1006,14 @@ App 内存不足时,系统会按照一定策略来腾出更多的空间供使
Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能一个 page 持有多个对象,也可能一个大的对象跨越多个 page。通常它是 16KB 大小,且有 3 种类型的 page。
![内存page种类](./../assets/2020-02-28-iOSMemoryType.png)
![内存page种类](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryType.png)
- Clean Memory
Clean memory 包括 3 类:可以 `page out` 的内存、内存映射文件、App 使用到的 framework每个 framework 都有 \_DATA_CONST 段,通常都是 clean 状态,但使用 runtime swizling那么变为 dirty
一开始分配的 page 都是干净的(堆里面的对象分配除外),我们 App 数据写入时候变为 dirty。从硬盘读进内存的文件也是只读的、clean page。
![Clean memory](./../assets/2020-02-28-iOSMemoryTypeClean.png)
![Clean memory](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryTypeClean.png)
- Dirty Memory
@@ -1021,7 +1021,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
在使用 framework 的过程中会产生 Dirty memory使用单例或者全局初始化方法有助于帮助减少 Dirty memory因为单例一旦创建就不销毁一直在内存中系统不认为是 Dirty memory
![Dirty memory](./../assets/2020-02-28-iOSMemoryTypeDirty.png)
![Dirty memory](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryTypeDirty.png)
- Compressed Memory
@@ -1032,7 +1032,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
App 运行内存 = pageNumbers \* pageSize。因为 Compressed Memory 属于 Dirty memory。所以 Memory footprint = dirtySize + CompressedSize
设备不同内存占用上限不同App 上限较高extension 上限较低,超过上限 crash 到 `EXC_RESOURCE_EXCEPTION`。
![Memory footprint](./../assets/2020-02-28-iOSMemoryFootprint.png)
![Memory footprint](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryFootprint.png)
接下来谈一下如何获取内存上限,以及如何监控 App 因为占用内存过大而被强杀。
@@ -1827,7 +1827,7 @@ static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason)
FacekBook 提出排除法监控 OOM。
![](./../assets/Facebook-OOM.jpeg)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Facebook-OOM.jpeg)
- App 更新了版本
@@ -2232,7 +2232,7 @@ App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试
#### 野指针可能存在的问题
<img src="./../assets/WildPointerCategory.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WildPointerCategory.png" style="zoom:30%" />
### 2. Zombie Object
@@ -2279,11 +2279,11 @@ self:_NSZombie_Person - superClass:nil
利用 Instruments 下的 Zombies 工具检测,看看野指针的情况,系统是怎么捕获的。
<img src="./../assets/InstrumentZombiesCaptureZombieObject.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/InstrumentZombiesCaptureZombieObject.png" style="zoom:30%" />
切换到 `Call Trees` 模式下可以看到系调用了 `_dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下
<img src="./../assets/XcodeZombieDetect.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeZombieDetect.png" style="zoom:30%" />
通过符号名称大概可以猜系统会在调用 dealloc 方法时,类似 KVO派生出一个新的僵尸类将当前类的 isa 指针指向僵尸类。
@@ -2990,7 +2990,7 @@ bool init_safe_free(void)
注意ZombieProxy、ZombieObjectDetector 2个类要在 Build Phases 设置为非 ARC加 `-fno-objc-arc`
<img src="./../assets/ZombieObjectsDetector.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ZombieObjectsDetector.png" style="zoom:30%" />
### 6. 方案对比
@@ -3005,7 +3005,7 @@ bool init_safe_free(void)
### 1. App 网络请求过程
![网络请求各阶段](./../assets/2020-04-03-NetworkTime.png)
![网络请求各阶段](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-03-NetworkTime.png)
App 发送一次网络请求一般会经历下面几个关键步骤:
@@ -3043,7 +3043,7 @@ App 发送一次网络请求一般会经历下面几个关键步骤:
iOS 网络框架层级关系如下:
![Network Level](./../assets/2020-04-05-NetworkLevel.png)
![Network Level](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-05-NetworkLevel.png)
iOS 网络现状是由 4 层组成的:最底层的 BSD Sockets、SecureTransport次级底层是 CFNetwork、NSURLSession、NSURLConnection、WebView 是用 Objective-C 实现的,且调用 CFNetwork应用层框架 AFNetworking 基于 NSURLSession、NSURLConnection 实现。
@@ -3631,11 +3631,11 @@ iOS 中 hook 技术有 2 类,一种是 NSProxy一种是 method swizzling
但是如果需要监全部的网络请求就不能满足需求了,查阅资料后发现了阿里百川有 APM 的解决方案,于是有了方案 3对于网络监控需要做如下的处理
![network hook](./../assets/2020-04-04-network_monitor.png)
![network hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-network_monitor.png)
可能对于 CFNetwork 比较陌生,可以看一下 CFNetwork 的层级和简单用法
![CFNetwork Structure](./../assets/2020-04-04-CFNetworkStructure.png)
![CFNetwork Structure](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-CFNetworkStructure.png)
CFNetwork 的基础是 CFSocket 和 CFStream。
@@ -3732,9 +3732,9 @@ void printResponseData (CFDataRef responseData) {
NSURLSession、NSURLConnection hook 如下。
![NSURLSession Hook](./../assets//2020-04-13-NSURLSessionHook.jpeg)
![NSURLSession Hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//2020-04-13-NSURLSessionHook.jpeg)
![NSURLConnection Hook](./../assets//2020-04-13-NSURLConnectionHook.jpeg)
![NSURLConnection Hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//2020-04-13-NSURLConnectionHook.jpeg)
业界有 APM 针对 CFNetwork 的方案,整理描述下:
@@ -4009,7 +4009,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
};
```
![method swizzling](./../assets/2020-04-09-methodSwizzling.png)
![method swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-09-methodSwizzling.png)
method swizzling 改进版如下
@@ -4036,7 +4036,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
typedef struct objc_object *id;
```
![isa swizzling](./../assets/2020-04-13-isaSwizzling.png)
![isa swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-13-isaSwizzling.png)
我们来分析一下为什么修改 `isa` 可以实现目的呢?
@@ -4194,11 +4194,11 @@ self.didCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForN
HTTP 请求报文结构
![请求报文结构](./../assets/2020-05-16-HTTPRequestStructure.png)
![请求报文结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-16-HTTPRequestStructure.png)
响应报文的结构
![响应报文结构](./../assets/2020-05-16-HTTPResponseStructure.png)
![响应报文结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-16-HTTPResponseStructure.png)
1. HTTP 报文是格式化的数据块,每条报文由三部分组成:对报文进行描述的起始行、包含属性的首部块、以及可选的包含数据的主体部分。
2. 起始行和手部就是由行分隔符的 ASCII 文本,每行都以一个由 2 个字符组成的行终止序列作为结束(包括一个回车符、一个换行符)
@@ -4225,11 +4225,11 @@ HTTP 请求报文结构
下图是打开 Chrome 查看极课时间网页的请求信息。包括响应行、响应头、响应体等信息。
![请求数据结构](./../assets/2020-05-13-HTTPDataStructure.png)
![请求数据结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-13-HTTPDataStructure.png)
下图是在终端使用 `curl` 查看一个完整的请求和响应数据
![curl查看HTTP响应](./../assets/2020-05-14-HTTPRequestStructure.png)
![curl查看HTTP响应](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-14-HTTPRequestStructure.png)
我们都知道在 HTTP 通信中,响应数据会使用 gzip 或其他压缩方式压缩,用 NSURLProtocol 等方案监听,用 NSData 类型去计算分析流量等会造成数据的不精确,因为正常一个 HTTP 响应体的内容是使用 gzip 或其他压缩方式压缩的,所以使用 NSData 会偏大。
@@ -4875,7 +4875,7 @@ Mach 已经通过异常机制提供了底层的陷进处理,而 BSD 则在异
Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,并通过 `threadsignal` 将信号投递到出错的线程。
![Mach 异常处理以及转换为 Unix 信号的流程](./../assets/2020-05-19-BSDCatchSignal.png)
![Mach 异常处理以及转换为 Unix 信号的流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-19-BSDCatchSignal.png)
### 2. Crash 收集方式
@@ -4939,7 +4939,7 @@ KSCrash 功能齐全,可以捕获如下类型的 Crash
流程图如下:
![KSCrash流程图](./../assets/2020-05-20-KSCrashStructure.png)
![KSCrash流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-20-KSCrashStructure.png)
对于 Mach 异常捕获,可以注册一个异常端口,该端口负责对当前任务的所有线程进行监听。
@@ -5250,7 +5250,7 @@ static void restoreExceptionPorts(void)
KSCrash 在这里的处理逻辑如下图:
![signal 处理步骤](./../assets/2020-05-20-signalCrash.png)
![signal 处理步骤](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-20-signalCrash.png)
看一下关键代码:
@@ -5612,9 +5612,9 @@ static void setEnabled(bool isEnabled)
阅读下源码,看看为什么 `NSUncaughtExceptionHandler` 可以收集 crash 信息。查看 objc 源码
<img src="./../assets/ObjcInitExceptionHandlerSet.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjcInitExceptionHandlerSet.png" style="zoom:30%" />
<img src="./../assets/ObjcExceptionHandlerExplore.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjcExceptionHandlerExplore.png" style="zoom:30%" />
发现入口函数 `_objc_init` 中调用可设置异常处理的 `exception_init` 方法。内部通过底层 API `std::set_terminate(&_objc_terminate)` 来设置异常处理的 handler。
@@ -5716,7 +5716,7 @@ static void setEnabled(bool isEnabled)
其他几个 crash 也是一样,异常信息经过包装交给 `kscm_handleException()` 函数处理。可以看到这个函数被其他几种 crash 捕获后所调用。
![caller](./../assets/2020-06-03-KSCrashCaller.png)
![caller](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-03-KSCrashCaller.png)
```c
/** Start general exception processing.
@@ -6217,7 +6217,7 @@ static int64_t getReportIDFromFilename(const char* filename)
}
```
![KSCrash 存储 Crash 数据位置](./../assets/2020-05-31-KSCrashStoreCrashData.png)
![KSCrash 存储 Crash 数据位置](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-31-KSCrashStoreCrashData.png)
#### 2.7 前端 js 相关的 Crash 的监控
@@ -6241,7 +6241,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) {
};
```
![h5 异常监控](./../assets/2020-06-19-JSErrorCatch.png)
![h5 异常监控](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-19-JSErrorCatch.png)
##### 2.7.3 React Native 异常监控
@@ -6264,7 +6264,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) {
模拟器点击 `command + d` 调出面板,选择 Debug打开 Chrome 浏览器, Mac 下快捷键 `Command + Option + J` 打开调试面板,就可以像调试 React 一样调试 RN 代码了。
![React Native Crash Monitor](./../assets/2020-06-19-ReactNativeCrashMonitor.png)
![React Native Crash Monitor](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-19-ReactNativeCrashMonitor.png)
查看到 crash stack 后点击可以跳转到 sourceMap 的地方。
@@ -6288,7 +6288,7 @@ TipsRN 项目打 Release 包
现象iOS 项目奔溃。截图以及日志如下
![RN crash](./../assets/2020-06-22-RNUncaughtCrash.png)
![RN crash](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-22-RNUncaughtCrash.png)
```shell
2020-06-22 22:26:03.318 [info][tid:main][RCTRootView.m:294] Running application todos ({
@@ -6382,7 +6382,7 @@ global.ErrorUtils.setGlobalHandler((e) => {
现象iOS 项目不奔溃。日志信息如下,对比 bundle 包中的 js。
![RN release log](./../assets/2020-06-23-RNReleaseLog.png)
![RN release log](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-23-RNReleaseLog.png)
结论:
@@ -6763,7 +6763,7 @@ parseJSError(line, column);
5. 拿脚本解析好的行号、列号、文件信息去和源代码文件比较,结果很正确。
![RN Log analysis](./../assets/2020-06-23-RNCrashLogAnalysis.png)
![RN Log analysis](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-23-RNCrashLogAnalysis.png)
##### 2.7.5 SourceMap 解析系统设计
@@ -7055,7 +7055,7 @@ parseJSError(line, column);
`.DSYM` 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,发布的时候为了安全,会把调试信息存储在单独的文件,`.DSYM` 其实是一个文件目录,结构如下:
![.DSYM文件结构](./../assets/2020-05-29-DYSMStructure.png)
![.DSYM文件结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-29-DYSMStructure.png)
#### 4.2 DWARF 文件
@@ -7587,7 +7587,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
/Users/你自己的用户名/Library/Developer/Xcode/iOS DeviceSupport/
```
![系统符号化文件](./../assets/2020-05-28-SymbolicateLib.png)
![系统符号化文件](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-28-SymbolicateLib.png)
### 5. 服务端处理
@@ -7597,7 +7597,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
早期单体应用时代,几乎应用的所有功能都在一台机器上运行,出了问题,运维人员打开终端输入命令直接查看系统日志,进而定位问题、解决问题。随着系统的功能越来越复杂,用户体量越来越大,单体应用几乎很难满足需求,所以技术架构迭代了,通过水平拓展来支持庞大的用户量,将单体应用进行拆分为多个应用,每个应用采用集群方式部署,负载均衡控制调度,假如某个子模块发生问题,去找这台服务器上终端找日志分析吗?显然台落后,所以日志管理平台便应运而生。通过 Logstash 去收集分析每台服务器的日志文件,然后按照定义的正则模版过滤后传输到 Kafka 或 Redis然后由另一个 Logstash 从 Kafka 或 Redis 上读取日志存储到 ES 中创建索引,最后通过 Kibana 进行可视化分析。此外可以将收集到的数据进行数据分析,做更进一步的维护和决策。
![ELK架构图](./../assets/2020-06-14-ELK.png)
![ELK架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-14-ELK.png)
上图展示了一个 ELK 的日志架构图。简单说明下:
@@ -7607,13 +7607,13 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
下图贴一个 Elasticsearch 社区分享的一个 “Elastic APM 动手实战”[主题](https://elasticsearch.cn/slides/257#page=3)的内容截图。
![Elasticsearch & APM](./../assets/2020-06-17-ElastiicsearchAPM.png)
![Elasticsearch & APM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-ElastiicsearchAPM.png)
##### 5.2 服务侧
Crash log 统一入库 Kibana 时是没有符号化的所以需要符号化处理以方便定位问题、crash 产生报表和后续处理。
![crash log 处理流程](./../assets/2020-06-14-CrashLogSymbolicate.png)
![crash log 处理流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-14-CrashLogSymbolicate.png)
所以整个流程就是:客户端 APM SDK 收集 crash log -> Kafka 存储 -> Mac 机执行定时任务符号化 -> 数据回传 Kafka -> 产品侧(显示端)对数据进行分类、报表、报警等操作。
@@ -7625,7 +7625,7 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
现在很多架构设计都是微服务,至于为什么选微服务,不在本文范畴。所以 crash 日志的符号化被设计为一个微服务。架构图如下
![crash 符号化流程图](./../assets/2020-06-17-APMServerArch.png)
![crash 符号化流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMServerArch.png)
说明:
@@ -7637,19 +7637,19 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
- 脚手架 cli 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等)
![符号化流程图](./../assets/2020-06-17-symolication_flow.png)
![符号化流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-symolication_flow.png)
其中符号化服务是大前端背景下大前端团队的产物,所以是 NodeJS 实现的单线程所以为了提高机器利用率就要开启多进程能力。iOS 的符号化机器是 双核的 Mac mini这就需要做实验测评到底需要开启几个 worker 进程做符号化服务。结果是双进程处理 crash log比单进程效率高近一倍而四进程比双进程效率提升不明显符合双核 mac mini 的特点。所以开启两个 worker 进程做符号化处理。
下图是完整设计图
![符号化技术设计图](./../assets/2020-06-17-APMServerWorker.png)
![符号化技术设计图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMServerWorker.png)
简单说明下,符号化流程是一个主从模式,一台 master 机,多个 slave 机master 机读取 .DSYM 和 crash 结果的 cache。「数据处理和任务调度框架」调度符号化服务内部 2 个 symbolocate worker同时从七牛云上获取 .DSYM 文件。
系统架构图如下
![符号化服务架构图](./../assets/2020-06-17-SymbolicateServerArch.png)
![符号化服务架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-SymbolicateServerArch.png)
## 九、Weex、Flutter 异常监控
@@ -7945,7 +7945,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
1. 当前启动时的js 预载入流程里的JS下载失败 和 进入Weex页面后的JS拉取失败 两处的上报失败未做区分没法实际统计加载页面时有多少JS获取失败的情况。
2. 随着业务迭代, Weex 页面越来越多,需要做 JS 拉取相关优化,避免白屏问题
3. 目前 JS 缓存逻辑为,每个 Weex 模块单独维护一个 LRUCache并且会做大量的预加载但可能大多数页面根本不会访问到。浪费了内存资源去做了一些不会被调用到的页面预加载可能还会造成 OOM 问题
![](./../assets/WeexResourcePull.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WeexResourcePull.png)
// todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能比如某个收银员角色固定的情况下他日常的操作行为是固定的商品扫码、开单
@@ -8145,17 +8145,17 @@ UI 控件存在依赖关系比如父子组件、UI 组件树),多线程
可能有些人一直没有遇到过因为在子线程操作 UI导致在开发阶段 Xcode console 输出了一堆日志,大体如下
![](./../assets/2022-0204-SubThreadUIXcode1@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIXcode1@2x.png)
本来常见的开发都会规避这些写法,没机会看到子线程操作 UI 的问题,但是 Weex 的业务代码,检测出存在子线程操作 UI 的问题,所以还是有必要增加这个能力的。
其实我们可以给 Xcode 打个 `Runtime Issue Breakpoint` type 选择 `Main Thread Checker` 在发生子线程操作 UI 的时候就会被系统检测到并触发断点,同时可以看到堆栈情况
![](./../assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png)
效果如下
![](./../assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png)
### 2. 问题及解决方案
@@ -8169,7 +8169,7 @@ UI 控件存在依赖关系比如父子组件、UI 组件树),多线程
但是某些类有些 API 我们也不希望在子线程被调用,这时候 `libMainThreadChecker.dylib`是无法满足的。
对 `libMainThreadChecker.dylib` 库的汇编代码研究,发现 `libMainThreadChecker.dylib` 是通过内部 `__main_thread_add_check_for_selector` 这个方法来进行类和方法的注册的。所以如果我们同样可以通过 `dlsym` 来调用该方法,以达到对自定义类和方法的主线程调用监测。
![](./../assets/2022-0204-SubThreadUIMonitor@2x.PNG)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIMonitor@2x.PNG)
另外该功能可以在线下 debug 阶段开启,判断是否是在 Xcode debug 状态,可以通过苹果提供的[官方判断方法](https://developer.apple.com/library/archive/qa/qa1361/_index.html#//apple_ref/doc/uid/DTS10003368)实现。
@@ -8183,7 +8183,7 @@ UI 控件存在依赖关系比如父子组件、UI 组件树),多线程
好像用 APM 的卡顿没发现啥问题。仔细看看发现了问题,我们的卡顿只是监控主线程方法耗时。一个页面加载后可能会触发一些网络请求功能,子线程请求完网络,可能会做一些数组裁剪、组装,拼接为 UI 所需要的信息,这个时候很可能会发生卡顿,所以这种 case 下卡顿监控是无法覆盖的。用下图表示
![](./../assets/PageLoadFullTime.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PageLoadFullTime.png)
页面作为承载用户交互的具体战场我们需要对页面的性能有个直观的指标。业界一般有2个指标**页面渲染时长、页面可交互时长**。
@@ -8295,7 +8295,7 @@ UI 控件存在依赖关系比如父子组件、UI 组件树),多线程
6. 整个 APM 的架构图如下
![APM Structure](./../assets/2020-06-17-APMStructure.jpg)
![APM Structure](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMStructure.jpg)
说明: