feature: App 逆向防护

This commit is contained in:
杭城小刘
2024-07-15 20:03:01 +08:00
parent 13f7457be9
commit 83fefff66b
109 changed files with 2549 additions and 672 deletions

View File

@@ -22,11 +22,11 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
### 1. 屏幕绘制原理
![老式 CRT 显示器原理](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 关系](https://github.com/FantasticLBP/knowledge-kit/raw/master/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](https://github.com/FantasticLBP/knowledge-kit/raw/master/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
揭秘。请看下图
![多缓冲区显示原理](https://github.com/FantasticLBP/knowledge-kit/raw/master/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. 卡顿产生的原因
![卡顿原因](https://github.com/FantasticLBP/knowledge-kit/raw/master/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](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 代码见[此处](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 状态](https://github.com/FantasticLBP/knowledge-kit/raw/master/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](https://github.com/FantasticLBP/knowledge-kit/raw/master/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](https://github.com/FantasticLBP/knowledge-kit/raw/master/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” 的一张图和例子,如下
![函数调用栈](https://github.com/FantasticLBP/knowledge-kit/raw/master/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;
这里有个策略,卡顿是由于主线程某个或某组方法执行过长而造成的卡顿,所以卡顿堆栈只需要分析主线程即可,但是此时是未符号化的方法(完成流程如下)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/callStackSymbolicate'.png)
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/callStackSymbolicate.png" style="zoom:30%" />
测试过单次抓取主线程符号耗时大概1ms左右。因此连续抓取堆栈是可取方案。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 的指令进行符号化,如下效果。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/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;
服务端聚合策略
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallStackGroupHash.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CallStackGroupHash.png)
找到最接近栈顶的符合占比条件的业务代码方法,作为卡顿堆栈归类 key。
@@ -551,7 +551,7 @@ curNode.costTime > (parentNode.costTime * 1.1/n) && curNode.costTime > parentNod
应用启动时间是影响用户体验的重要因素之一,所以我们需要量化去衡量一个 App 的启动速度到底有多快。启动分为冷启动和热启动。
![App 启动时间](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 的基础初始化工作。
@@ -586,10 +586,10 @@ App 启动过程:
- 程序执行:调用 main();调用 UIApplicationMain();调用 applicationWillFinishLaunching()
Pre-Main 阶段
![Pre-Main 阶段](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 阶段](https://github.com/FantasticLBP/knowledge-kit/raw/master/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
@@ -673,9 +673,9 @@ Main 阶段
### 4. 精确版启动时间监控
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AppStarupPipeline.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AppStarupPipeline.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/APMStartup.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/APMStartup.png)
进程创建:通过 sysctl 可以拿到
@@ -685,7 +685,7 @@ didFinishLaunching通过 UIApplicationDidFinishLaunchingNotification 拿到
首屏渲染时间怎么拿?能获取到 `CA::Transaction::commit()` 时刻即可。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CATransactionCommit.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CATransactionCommit.png)
对 UI 进行修改后会在主线程休眠前触发 `CA::Transaction::commit()`,其实就是打包 Render Tree通过 IPC 发送给 Render Server。
@@ -697,21 +697,21 @@ didFinishLaunching通过 UIApplicationDidFinishLaunchingNotification 拿到
- Layout 布局:调用 `layout` 等与布局相关的 API
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline1.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline1.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline2.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline2.png)
- Display 绘制:调用 `drawRect` 等与绘制相关方法
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline3.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline3.png)
- Prepare图片解码
- Commit递归打包 Render Tree通过 IPC 计数发送给 Render Server
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline4.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline5.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/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` 方法。
@@ -754,6 +754,26 @@ didFinishLaunching通过 UIApplicationDidFinishLaunchingNotification 拿到
开始时间点:一般需要 Native 配合,容器页面创建好就是开始时间点。比如 iOS 的 VC `viewDidLoad`、Android 的 `onActivityCreated`
结束时间点:结束时间一般在跨端侧,比如 RN 中的组件挂载完成 componentDidMount 回调的时刻。
### 6. 工单跟进
数据采集上报后,产生工单,自动分配到人、通知负责人和对应的群。
这些在其他篇章会讲。比如启动时间的工单信息类似:
| 阶段 | 耗时 | 方法 | 业务 | 负责人 | 操作 |
| ---- | ---- | ----------------------------- | ----- | ------ | -------------- |
| T1 | 30ms | [Appdeledate handleDBUpgrade] | Goods | @张三 | 更改状态、详情 |
## 三、 CPU 使用率监控
### 1. CPU 架构
@@ -899,14 +919,14 @@ App 内存不足时,系统会按照一定策略来腾出更多的空间供使
Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能一个 page 持有多个对象,也可能一个大的对象跨越多个 page。通常它是 16KB 大小,且有 3 种类型的 page。
![内存page种类](https://github.com/FantasticLBP/knowledge-kit/raw/master/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](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-28-iOSMemoryTypeClean.png)
![Clean memory](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryTypeClean.png)
- Dirty Memory
@@ -914,7 +934,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
在使用 framework 的过程中会产生 Dirty memory使用单例或者全局初始化方法有助于帮助减少 Dirty memory因为单例一旦创建就不销毁一直在内存中系统不认为是 Dirty memory
![Dirty memory](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-28-iOSMemoryTypeDirty.png)
![Dirty memory](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryTypeDirty.png)
- Compressed Memory
@@ -925,7 +945,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
App 运行内存 = pageNumbers \* pageSize。因为 Compressed Memory 属于 Dirty memory。所以 Memory footprint = dirtySize + CompressedSize
设备不同内存占用上限不同App 上限较高extension 上限较低,超过上限 crash 到 `EXC_RESOURCE_EXCEPTION`。
![Memory footprint](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-28-iOSMemoryFootprint.png)
![Memory footprint](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryFootprint.png)
接下来谈一下如何获取内存上限,以及如何监控 App 因为占用内存过大而被强杀。
@@ -1720,7 +1740,7 @@ static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason)
FacekBook 提出排除法监控 OOM。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Facebook-OOM.jpeg)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Facebook-OOM.jpeg)
- App 更新了版本
@@ -2070,23 +2090,25 @@ for (NSInteger index = 0; index < 10000000; index++) {
全部的讲解可以看[这里](<(https://mp.weixin.qq.com/s/4-4M9E8NziAgshlwB7Sc6g)>)。对于 Memory Graph 的实现细节感兴趣的可以看这篇[文章](https://juejin.cn/post/6895583288451465230)
## 五、野指针/内存泄漏
### 1. 概念定义
1.内存泄漏会导致 OOM什么是内存泄漏?
#### 内存泄漏会导致 OOM那什么是内存泄漏
定义:程序中已经动态分配的堆内存,由于某些原因,导致程序无法释放或者未释放内存,造成系统内存的浪费,导致程序的运行速度减慢甚至是系统奔溃等严重后果。
一般来说导致内存泄漏的原因:对象没有释放、循环引用(无法释放)
一般来说导致内存泄漏的原因:对象没有释放Core Foundation 对象需要手动调用 release 方法)、循环引用
2.什么是野指针?
#### 什么是野指针?
C 语言中声明一个指针变量但是没有初始化。任何指针变量刚被创建时不会自动成为NULL指针它的缺省值是随机的指向一个随机的内存空间。所以指针变量在创建的同时应当被初始化要么将指针设置为NULL要么让它指向合法的内存
OC 语言:指针指向的内存对象已经被释放或回收了,但是指针没有置为 nil还是指向已经回收的内存空间。
3.什么是空指针?
#### 什么是空指针?
空指针就是没有存储任何内存地址(也就是没有指向任何对象的指针),即 nil、Nil、NULL、NSNULL、0
@@ -2098,17 +2120,20 @@ OC 语言:指针指向的内存对象已经被释放或回收了,但是指
- NSNull数值类的空对象
4.内存回收的本质
#### 内存回收的本质
申请一块内存空间,其本质是向系统申请一块别人不再使用的内存空间,即已经回收的内存空间
释放一块内存空间,其本质是这个内存空间不再使用,可以由系统分配给别的对象使用。此时内存空间虽然回收了,但是原本的数据依赖的是存在的,可以理解为垃圾数据。
5.什么是僵尸对象
- OC 对象释放后,内存回收,表示这一块内存可以分配给别的对象
- 这块内存在分配给别的对象之前,仍然保留着已经释放对象的数据
#### 什么是僵尸对象?
僵尸对象就是指一个 OC 对象释放后所占用的内存还没被复写(重新分配给其他对象)前被称为僵尸对象。此时僵尸对象内存很不稳定,内存随时可能被系统分配给其他对象所使用。所以此时僵尸对象不应该访问和使用(调用对象的方法等)
6.为什么 OC 野指针 Crash 很多?
#### 为什么 OC 野指针 Crash 很多?
App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试等,但是野指针具有随机性,所以很多野指针偷偷跑到线上了。
@@ -2116,19 +2141,29 @@ App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试
- 出错分支比较难进,执行不到出错的 case所以能做的就是提高测试覆盖率
- 即使跑进有问题的分支,但是野指针指向的地址并不一定会导致 crash。因为野指针本质是一个指向已删除的对象或受限内存区域的指针。这里 OC 野指针指的是 OC 对象释放后但指针未置空导致的野指针。dealloc 执行后只是告诉系统这篇内存我不用,但是系统并没有让这块内存不能访问。
- 即使跑进有问题的分支,但是野指针指向的地址并不一定会导致 crash。因为野指针本质是一个指向已删除的对象或受限内存区域的指针。这里 OC 野指针指的是 OC 对象释放后但指针未置空导致的野指针。dealloc 执行后只是告诉系统这篇内存我不用,但是系统并没有让这块内存不能访问。访问的话,暂时是安全的,过了一会儿,由于内存紧张,可能被系统分配到其他对象去了。被填充了一个新的对象的信息。比如成员变量、方法等。再去访问就会 crash
### 2. Zombie Objects
#### 野指针可能存在的问题
Zoom Object 是 Xcode 提供的一种用来检测内存问题的对象EXC_BAD_ACCESS它可以捕获任何尝试访问坏内存的调用。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WildPointerCategory.png" style="zoom:30%" />
### 2. Zombie Object
`Zombie Object` 是 Xcode 提供的一种用来检测内存问题的工具(`EXC_BAD_ACCESS`),它可以捕获任何尝试访问坏内存的调用。
一个对象解除了它的引用,已经被释放掉,但仍可以接收消息,就叫 zombie object 。
如果给僵尸对象发送消息,那么系统会在运行期间奔溃并在 Xcode 输出错误日志,通过日志定位到野指针对象调用的方法和类名。
当野指针指向的僵尸对象所占用的内存还未被复写(未分配),此时是可以访问的
- 当野指针指向的僵尸对象所占用的内存还未被复写(未分配),此时是可以访问的
当野指针指向的僵尸对象所占用的内存被分配后,此时访问会奔溃,比如 objc_msgSend 失败、SIGBUS、SIGFPE、SIGILL 等异常。
- 当野指针指向的僵尸对象所占用的内存被分配后,此时访问会奔溃,比如 objc_msgSend 失败、SIGBUS、SIGFPE、SIGILL 等异常。
### 3. 探索 Xcode 如何实现僵尸对象检测的原理
开启僵尸对象时,不回收真正内存,而是将其 isa 变为僵尸对象,僵尸对象在内存中无法复用,所以偶现的 crash 变为必现 crash
### 3. 探索 Xcode Zombie Object 实现原理
Xcode 默认设置不检查指针指向的对象是否是僵尸对象。为什么这么设计?因为开启会影响开发效率,所以线上就会报错。
@@ -2145,31 +2180,25 @@ DemoMRC + Xcode 开启 Zombie Object
printClassInfo(person);
    [person description];
}
2022-05-14 01:35:54.521783+0800 DDD[79672:3001452] self:Person - superClass:NSObject
2022-05-14 01:35:54.522115+0800 DDD[79672:3001452] self:_NSZombie_Person - superClass:nil
2022-05-14 01:35:54.523292+0800 DDD[79672:3001452] *** -[Person description]: message sent to deallocated instance 0x6000024f1030
// console
self:Person - superClass:NSObject
self:_NSZombie_Person - superClass:nil
*** -[Person description]: message sent to deallocated instance 0x6000024f1030
```
(前提是开启了 Zombie Objects)可以看到系统在回收对象时,不是真正的回收,而是先将其转为僵尸对象,僵尸对象所在内存无法被重用,所以让不稳定复现的内存奔溃变为稳定崩溃(更好的复现问题)。
(前提是开启了 Zombie Object可以看到系统在回收对象时不是真正的回收而是先将其转为僵尸对象僵尸对象所在内存无法被重用所以让不稳定复现的内存奔溃变为稳定崩溃更好的复现问题
开启僵尸对象检测后,系统会修改对象 isa 指针,指向新生成的僵尸类,僵尸类可以响应所有的消息,但表现打印格式为 `*** -[类 sel]: message sent to deallocated instance 地址` 的日志,然后结束进程。
神奇,为什么打印出 `_NSZombie_Person` 。
```objectivec
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
```
利用 Instruments 下的 Zombies 工具检测,看看野指针的情况,系统是怎么捕获的。
objc 源码中 dealloc 方法注释说了 dealloc 的实现会被僵尸对象所替换。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/InstrumentZombiesCaptureZombieObject.png" style="zoom:30%" />
开启 Instrucments 分析查看得到,调用了 `__dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下
切换到 `Call Trees` 模式下可以看到系调用了 `_dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeZombieDetect.png)
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeZombieDetect.png" style="zoom:30%" />
通过符号名称大概可以才到系统会调用 dealloc 方法时,类似 KVO派生出一个新的僵尸类,将当前类的 isa 指针指向僵尸类。
通过符号名称大概可以系统会调用 dealloc 方法时,类似 KVO派生出一个新的僵尸类将当前类的 isa 指针指向僵尸类。
查看 Runtime 源码
@@ -2208,7 +2237,7 @@ void *objc_destructInstance(id obj) {
}
```
dealloc 方法最终调用到 object_dispose但是如果开启 Zombie Object 检测则不会执行 free。其中 objc_destructInstance 方法会清理对象的关联对象、c++ 的析构函数、对象的 ivar 清理。
dealloc 方法最终调用到 `object_dispose`,但是如果开启 Zombie Object 检测则不会执行 free。其中 `objc_destructInstance` 方法会清理对象的关联对象、c++ 的析构函数、对象的 ivar 清理。
另一方面,从 GUN 源码中窥探下 (NSObject.m 文件)
@@ -2372,10 +2401,29 @@ static void GSLogZombie(id o, SEL sel){
可以从 GUN 中看到调用对象 dealloc 方法时,内部实现通过调用 `GSMakeZombie` 方法,将类的 isa 指向为 NSZombie 类,也就是 zombieClass其中 zombieClass 在 NSObject `initialize` 方法中初始化。后续针对僵尸对象的所有方法调用,都会走 Runtime forwarding 这个机制,内部会调用 `GSLogZombie` 方法,方法会按照 `NSLog(@"*** -[%@ %@]: message sent to deallocated instance %p", c, NSStringFromSelector(sel), o);` 格式打印日志,最后根据环境变量判断,调用系统底层 `abort` 来奔溃
开启僵尸对象检测后,系统会修改对象 isa 指针,指向新生成的僵尸类,僵尸类可以响应所有的消息,但表现打印格式为 `*** -[类 sel]: message sent to deallocated instance 地址` 的日志,然后结束进程。
神奇,为什么打印出 `_NSZombie_Person` 。
```objectivec
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
```
objc 源码中 dealloc 方法注释说了 dealloc 的实现会被僵尸对象所替换。
### 4. Malloc Scribble
申请内存 alloc 时在内存上填 `0xAA`,释放内存 dealloc 时在内存上填 `0x55`。此种方案也会让内存泄漏偶现的 crash 变为必现。
### 5. 野指针监控工具<a name="zombieSniffer"></a>
##### 类 Zombie Object 方案
@@ -2384,13 +2432,16 @@ static void GSLogZombie(id o, SEL sel){
其中的僵尸对象检测做了这么几件事:
- 开启僵尸对象时,不回收真正内存,而是将其 isa 变为僵尸对象,僵尸对象在内存中无法复用,所以偶现的 crash 变为必现 crash
- 僵尸类可以响应任何消息,是通过 Runtime forawrding 实现的。表现为先打印一条日志,按照 `NSLog(@"*** -[%@ %@]: message sent to deallocated instance %p", c, NSStringFromSelector(sel), o);` 格式打印日志。随后调用系统的 `abort()` 奔溃。
- 获取有问题内存对象的类名
- 字符串拼接,类似 `_NSZombie_${ClassName}` 的形式
- 按照上面得到的类名,创建一个继承自 NSProxy 的子类。因为需要响应各种原始对象的任何方法,所以需要是 NSProxy 的子类
- hook NSObject 和 NSProxy 2个基类的 `dealloc` 方法,新的 dealloc 方法实现里修改当前类的 isa 为新创建的类
- 为了避免内存空间释放后被重写,造成野指针问题。通过字典存储被方式的对象,同时设置在 30s 后调用 dealloc 方法将字典中存储的对象释放,避免 OOM
- 新创建的类除了处理消息转发外,还需要实现 NSObject 的基础方法,比如 copy、zone、description 方法
- hook 之后,给僵尸对象发消息,最后都会调用 NSProxy 的 `forwardInvocation` 方法,其内部打印方法名称、对象地址、调用堆栈信息。最后调用 abort 方法crash 掉。
- 奔溃的时候应该将调用堆栈等案发信息保存下来,走 APM 问题跟进流程
```objectivec
```objective-c
// ZombiePoxy
#import <Foundation/Foundation.h>
@@ -2852,7 +2903,14 @@ bool init_safe_free(void)
注意ZombieProxy、ZombieObjectDetector 2个类要在 Build Phases 设置为非 ARC加 `-fno-objc-arc`
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ZombieObjectsDetector.png)
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ZombieObjectsDetector.png" style="zoom:30%" />
### 6. 方案对比
- 僵尸对象相比 `Malloc Scribble`,不需要考虑会不会崩溃的问题,只需要野指针指向僵尸对象,那么再次访问野指针就一定会奔溃
- 僵尸对象的方比如 `Malloc Scribble` 覆盖面广,可以通过 fishhook hook free 方法将 c 函数也包含在其中。
## 六、 App 网络监控
@@ -2860,7 +2918,7 @@ bool init_safe_free(void)
### 1. App 网络请求过程
![网络请求各阶段](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-03-NetworkTime.png)
![网络请求各阶段](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-03-NetworkTime.png)
App 发送一次网络请求一般会经历下面几个关键步骤:
@@ -2898,7 +2956,7 @@ App 发送一次网络请求一般会经历下面几个关键步骤:
iOS 网络框架层级关系如下:
![Network Level](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 实现。
@@ -3486,11 +3544,11 @@ iOS 中 hook 技术有 2 类,一种是 NSProxy一种是 method swizzling
但是如果需要监全部的网络请求就不能满足需求了,查阅资料后发现了阿里百川有 APM 的解决方案,于是有了方案 3对于网络监控需要做如下的处理
![network hook](https://github.com/FantasticLBP/knowledge-kit/raw/master/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](https://github.com/FantasticLBP/knowledge-kit/raw/master/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。
@@ -3587,9 +3645,9 @@ void printResponseData (CFDataRef responseData) {
NSURLSession、NSURLConnection hook 如下。
![NSURLSession Hook](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets//2020-04-13-NSURLSessionHook.jpeg)
![NSURLSession Hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//2020-04-13-NSURLSessionHook.jpeg)
![NSURLConnection Hook](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets//2020-04-13-NSURLConnectionHook.jpeg)
![NSURLConnection Hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//2020-04-13-NSURLConnectionHook.jpeg)
业界有 APM 针对 CFNetwork 的方案,整理描述下:
@@ -3864,7 +3922,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
};
```
![method swizzling](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-09-methodSwizzling.png)
![method swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-09-methodSwizzling.png)
method swizzling 改进版如下
@@ -3891,7 +3949,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
typedef struct objc_object *id;
```
![isa swizzling](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-13-isaSwizzling.png)
![isa swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-13-isaSwizzling.png)
我们来分析一下为什么修改 `isa` 可以实现目的呢?
@@ -4049,11 +4107,11 @@ self.didCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForN
HTTP 请求报文结构
![请求报文结构](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-16-HTTPRequestStructure.png)
![请求报文结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-16-HTTPRequestStructure.png)
响应报文的结构
![响应报文结构](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 个字符组成的行终止序列作为结束(包括一个回车符、一个换行符)
@@ -4080,11 +4138,11 @@ HTTP 请求报文结构
下图是打开 Chrome 查看极课时间网页的请求信息。包括响应行、响应头、响应体等信息。
![请求数据结构](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-13-HTTPDataStructure.png)
![请求数据结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-13-HTTPDataStructure.png)
下图是在终端使用 `curl` 查看一个完整的请求和响应数据
![curl查看HTTP响应](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 会偏大。
@@ -4646,7 +4704,7 @@ Mach 已经通过异常机制提供了底层的陷进处理,而 BSD 则在异
Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,并通过 `threadsignal` 将信号投递到出错的线程。
![Mach 异常处理以及转换为 Unix 信号的流程](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-19-BSDCatchSignal.png)
![Mach 异常处理以及转换为 Unix 信号的流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-19-BSDCatchSignal.png)
### 2. Crash 收集方式
@@ -4710,7 +4768,7 @@ KSCrash 功能齐全,可以捕获如下类型的 Crash
流程图如下:
![KSCrash流程图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-20-KSCrashStructure.png)
![KSCrash流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-20-KSCrashStructure.png)
对于 Mach 异常捕获,可以注册一个异常端口,该端口负责对当前任务的所有线程进行监听。
@@ -5021,7 +5079,7 @@ static void restoreExceptionPorts(void)
KSCrash 在这里的处理逻辑如下图:
![signal 处理步骤](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-20-signalCrash.png)
![signal 处理步骤](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-20-signalCrash.png)
看一下关键代码:
@@ -5381,6 +5439,16 @@ static void setEnabled(bool isEnabled)
}
```
阅读下源码,看看为什么 `NSUncaughtExceptionHandler` 可以收集 crash 信息。查看 objc 源码
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjcInitExceptionHandlerSet.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。
#### 2.5. 主线程死锁
主线程死锁的检测和 ANR 的检测有些类似
@@ -5477,7 +5545,7 @@ static void setEnabled(bool isEnabled)
其他几个 crash 也是一样,异常信息经过包装交给 `kscm_handleException()` 函数处理。可以看到这个函数被其他几种 crash 捕获后所调用。
![caller](https://github.com/FantasticLBP/knowledge-kit/raw/master/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.
@@ -5978,7 +6046,7 @@ static int64_t getReportIDFromFilename(const char* filename)
}
```
![KSCrash 存储 Crash 数据位置](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 的监控
@@ -6002,7 +6070,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) {
};
```
![h5 异常监控](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 异常监控
@@ -6025,7 +6093,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) {
模拟器点击 `command + d` 调出面板,选择 Debug打开 Chrome 浏览器, Mac 下快捷键 `Command + Option + J` 打开调试面板,就可以像调试 React 一样调试 RN 代码了。
![React Native Crash Monitor](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 的地方。
@@ -6049,7 +6117,7 @@ TipsRN 项目打 Release 包
现象iOS 项目奔溃。截图以及日志如下
![RN crash](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 ({
@@ -6143,7 +6211,7 @@ global.ErrorUtils.setGlobalHandler((e) => {
现象iOS 项目不奔溃。日志信息如下,对比 bundle 包中的 js。
![RN release log](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-23-RNReleaseLog.png)
![RN release log](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-23-RNReleaseLog.png)
结论:
@@ -6524,7 +6592,7 @@ parseJSError(line, column);
5. 拿脚本解析好的行号、列号、文件信息去和源代码文件比较,结果很正确。
![RN Log analysis](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 解析系统设计
@@ -6816,7 +6884,7 @@ parseJSError(line, column);
`.DSYM` 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,发布的时候为了安全,会把调试信息存储在单独的文件,`.DSYM` 其实是一个文件目录,结构如下:
![.DSYM文件结构](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-29-DYSMStructure.png)
![.DSYM文件结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-29-DYSMStructure.png)
#### 4.2 DWARF 文件
@@ -7348,7 +7416,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
/Users/你自己的用户名/Library/Developer/Xcode/iOS DeviceSupport/
```
![系统符号化文件](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-28-SymbolicateLib.png)
![系统符号化文件](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-28-SymbolicateLib.png)
### 5. 服务端处理
@@ -7358,7 +7426,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
早期单体应用时代,几乎应用的所有功能都在一台机器上运行,出了问题,运维人员打开终端输入命令直接查看系统日志,进而定位问题、解决问题。随着系统的功能越来越复杂,用户体量越来越大,单体应用几乎很难满足需求,所以技术架构迭代了,通过水平拓展来支持庞大的用户量,将单体应用进行拆分为多个应用,每个应用采用集群方式部署,负载均衡控制调度,假如某个子模块发生问题,去找这台服务器上终端找日志分析吗?显然台落后,所以日志管理平台便应运而生。通过 Logstash 去收集分析每台服务器的日志文件,然后按照定义的正则模版过滤后传输到 Kafka 或 Redis然后由另一个 Logstash 从 Kafka 或 Redis 上读取日志存储到 ES 中创建索引,最后通过 Kibana 进行可视化分析。此外可以将收集到的数据进行数据分析,做更进一步的维护和决策。
![ELK架构图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-14-ELK.png)
![ELK架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-14-ELK.png)
上图展示了一个 ELK 的日志架构图。简单说明下:
@@ -7368,13 +7436,13 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
下图贴一个 Elasticsearch 社区分享的一个 “Elastic APM 动手实战”[主题](https://elasticsearch.cn/slides/257#page=3)的内容截图。
![Elasticsearch & APM](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 处理流程](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 -> 产品侧(显示端)对数据进行分类、报表、报警等操作。
@@ -7386,7 +7454,7 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
现在很多架构设计都是微服务,至于为什么选微服务,不在本文范畴。所以 crash 日志的符号化被设计为一个微服务。架构图如下
![crash 符号化流程图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-17-APMServerArch.png)
![crash 符号化流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMServerArch.png)
说明:
@@ -7398,19 +7466,19 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
- 脚手架 cli 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等)
![符号化流程图](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 进程做符号化处理。
下图是完整设计图
![符号化技术设计图](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 文件。
系统架构图如下
![符号化服务架构图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-17-SymbolicateServerArch.png)
![符号化服务架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-SymbolicateServerArch.png)
## 九、Weex、Flutter 异常监控
@@ -7706,7 +7774,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
1. 当前启动时的js 预载入流程里的JS下载失败 和 进入Weex页面后的JS拉取失败 两处的上报失败未做区分没法实际统计加载页面时有多少JS获取失败的情况。
2. 随着业务迭代, Weex 页面越来越多,需要做 JS 拉取相关优化,避免白屏问题
3. 目前 JS 缓存逻辑为,每个 Weex 模块单独维护一个 LRUCache并且会做大量的预加载但可能大多数页面根本不会访问到。浪费了内存资源去做了一些不会被调用到的页面预加载可能还会造成 OOM 问题
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WeexResourcePull.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WeexResourcePull.png)
// todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能比如某个收银员角色固定的情况下他日常的操作行为是固定的商品扫码、开单
@@ -7718,15 +7786,15 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
可能有些人一直没有遇到过因为在子线程操作 UI导致在开发阶段 Xcode console 输出了一堆日志,大体如下
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2022-0204-SubThreadUIXcode1@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIXcode1@2x.png)
其实我们可以给 Xcode 打个 `Runtime Issue Breakpoint` type 选择 `Main Thread Checker` 在发生子线程操作 UI 的时候就会被系统检测到并触发断点,同时可以看到堆栈情况
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png)
效果如下
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png)
### 2. 问题及解决方案
@@ -7740,7 +7808,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
但是某些类有些 API 我们也不希望在子线程被调用,这时候 `libMainThreadChecker.dylib`是无法满足的。
对 `libMainThreadChecker.dylib` 库的汇编代码研究,发现 `libMainThreadChecker.dylib` 是通过内部 `__main_thread_add_check_for_selector` 这个方法来进行类和方法的注册的。所以如果我们同样可以通过 `dlsym` 来调用该方法,以达到对自定义类和方法的主线程调用监测。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/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)实现。
@@ -7754,7 +7822,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
好像用 APM 的卡顿没发现啥问题。仔细看看发现了问题,我们的卡顿只是监控主线程方法耗时。一个页面加载后可能会触发一些网络请求功能,子线程请求完网络,可能会做一些数组裁剪、组装,拼接为 UI 所需要的信息,这个时候很可能会发生卡顿,所以这种 case 下卡顿监控是无法覆盖的。用下图表示
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/PageLoadFullTime.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PageLoadFullTime.png)
页面作为承载用户交互的具体战场我们需要对页面的性能有个直观的指标。业界一般有2个指标**页面渲染时长、页面可交互时长**。
@@ -7819,7 +7887,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
4. 监控数据需要做内存到文件的写入处理,需要注意策略。监控数据需要存储数据库,数据库大小、设计规则等。存入数据库后如何上报,上报机制等会在另一篇文章讲:[打造一个通用、可配置的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)
5. 尽量在技术评审后,将各端的技术实现写进文档中,同步给相关人员。比如 ANR 的实现
```objective-c
/*
android 端
@@ -7865,15 +7933,19 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
```
6. 整个 APM 的架构图如下
![APM Structure](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-17-APMStructure.jpg)
![APM Structure](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMStructure.jpg)
说明:
- 埋点 SDK通过 sessionId 来关联日志数据
7. APM 技术方案本身是随着技术手段、分析需求不断调整升级的。上图的几个结构示意图是早期几个版本的目前使用的是在此基础上进行了升级和结构调整提几个关键词Hermes、Flink SQL、InfluxDB。
8. 获取到 APM 问题数据是第一步,获取之后问题需要及时上报,需要有一个数据上报系统,数据及时准确、高效的上传到服务端(这就是另一个命题,可以查看这篇文章:[打造一个通用、可配置、多句柄的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)),到 APM 后台。后台产生工单,根据业务模块分配到对应的责任人、群组。根据问题的紧急程度,报警策略和提醒频次也不一样,比如波动报警、自定义报警策略等(这也是另一个命题:智能报警策略、波动报警策略、业务线自定义可配置责任人等)。责任人及时解决问题、修改工单状态、热修复或者发布新版本,问题闭环。
## 十四、未来规划
- 监控能力继续完善
@@ -7888,8 +7960,6 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
- [iOS 保持界面流畅的技巧](https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/)
- [Call Stack](https://en.wikipedia.org/wiki/Call_stack)
- [关于函数调用栈(call stack)的个人理解](https://blog.csdn.net/VarusK/article/details/83031643)
- [获取任意线程调用栈的那些事](https://bestswifter.com/callstack/)
- [iOS 启动时间优化](https://www.zoomfeng.com/blog/launch-time.html)
- [WWDC2019 之启动时间与 Dyld3](https://www.zoomfeng.com/blog/launch-optimize-from-wwdc2019.html)
- [Apple-libmalloc](https://opensource.apple.com/tarballs/libmalloc/)