mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
feat: refine
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
## 结构
|
||||
|
||||

|
||||

|
||||
|
||||
LLVM 由三部分构成:
|
||||
|
||||
@@ -16,7 +16,7 @@ LLVM 由三部分构成:
|
||||
|
||||
- Backend(后端):生成目标程序(机器码)
|
||||
|
||||

|
||||

|
||||
|
||||
正是由于这样的设计,使得 LLVM 具备很多有点:
|
||||
|
||||
@@ -51,7 +51,7 @@ Clang 相较于 GCC,具备下面优点:
|
||||
|
||||
- 设计清晰简单,容易理解,易于扩展增强
|
||||
|
||||

|
||||

|
||||
|
||||
### 查看编译过程
|
||||
|
||||
@@ -61,7 +61,7 @@ clang -ccc-print-phases main.m
|
||||
|
||||
对 main.m 文件
|
||||
|
||||

|
||||

|
||||
|
||||
可以看到经历了:输入、预处理、编译、LLVM Backend、汇编、链接、绑定架构7个阶段。
|
||||
|
||||
@@ -79,10 +79,10 @@ int main(int argc, const char * argv[]) {
|
||||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
语法分析,生成语法树(AST,Abstract Syntax Tree):`clang -fmodules -fsyntax-only -Xclang -ast-dump main.m`
|
||||
|
||||

|
||||

|
||||
|
||||
### LLVM IR
|
||||
|
||||
@@ -59,7 +59,7 @@ typedef struct NCTbl {
|
||||
|
||||
该表用于存储添加观察者时传了 NotificationName 的情况。也就是 Named Table 中,NotificationName 作为 key。在使用系统 API 注册观察者的时候还可以传入 object 参数,表示只监听该对象发出的通知,所以还需要一张表存储 object 和 observer 的对应关系,object 为 key,observer 为 value。
|
||||
|
||||

|
||||

|
||||
|
||||
- 第一个 MapTable key 为 notificationName,value 为另一个 MapTable(子 Table)
|
||||
|
||||
@@ -71,7 +71,7 @@ typedef struct NCTbl {
|
||||
|
||||
nameless Table 结构较为简单,因为没有 notificationName,所以就一层 MapTable。key 为 object,value 为链表,存储所有的观察者。
|
||||
|
||||

|
||||

|
||||
|
||||
### wildcard
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
## 渲染机制
|
||||
|
||||

|
||||

|
||||
|
||||
iOS 渲染框架可以分为4层,顶层是 UIKit,包括图形界面的高级 API 和常用的各种 UI 控件。UIKit 下层是 Core Animation,不要被名字误解了,它不光是处理动画相关,也在做图形渲染相关的事情(比如 UIView 的 CALayer 就处于 Core Animation 中)。Core Animation 之下就是由 OpenGL ES 和 CoreGraphics 组成的图形渲染层,OpenGL ES 主要操作 GPU 进行图形渲染,CoreGraphics 主要操作 CPU 进行图形渲染。上面3层都属于渲染图形软件层,再下层就是图形显示硬件层。
|
||||
|
||||
iOS 图形界面的显示是一个复杂的流程,一部分数据通过 Core Graphics、Core Image 有 CPU 预处理,最终通过 OpenGL ES 将数据传输给 GPU,最终显示到屏幕上。
|
||||
|
||||

|
||||

|
||||
|
||||
- Core Animation 提交会话(事务),包括自己和子树(view hierarchy) 的布局状态
|
||||
|
||||
@@ -20,13 +20,13 @@ iOS 图形界面的显示是一个复杂的流程,一部分数据通过 Core G
|
||||
|
||||
## Core Animation
|
||||
|
||||

|
||||

|
||||
|
||||
可以看到 Core Animation pipeline 由4部分组成:Application 层的 Core Animation 部分、Render Server 中的 Core Animation 部分、GPU 渲染、显示器显示。
|
||||
|
||||
### Application 层 Core Animation 部分
|
||||
|
||||

|
||||

|
||||
|
||||
- 布局(Layout):`layoutSubviews`、`addSubview`,这里通常是 CPU、IO 繁忙
|
||||
|
||||
@@ -52,7 +52,7 @@ Render Server 是一个独立的渲染进程,当收到来自 Application 的 (
|
||||
|
||||
## UIView 绘制流程
|
||||
|
||||

|
||||

|
||||
|
||||
- 每个 UIView 都有一个 CALayer,layer 属性都有 contents,contents 其实是一块缓存,叫做 backing store
|
||||
|
||||
@@ -60,7 +60,7 @@ Render Server 是一个独立的渲染进程,当收到来自 Application 的 (
|
||||
|
||||
- 当 backing store 写完后,通过 Render Server 交给 GPU 去渲染,最后显示到屏幕上
|
||||
|
||||

|
||||

|
||||
|
||||
- 调用 `[UIView setNeedsDisplay]` 方法时,并没有立即执行绘制工作,而是马上调用 `[view.layer setNeedsDisplay]` 方法,给当前 layer 打上标记
|
||||
|
||||
@@ -76,7 +76,7 @@ Render Server 是一个独立的渲染进程,当收到来自 Application 的 (
|
||||
|
||||
### 系统绘制流程
|
||||
|
||||

|
||||

|
||||
|
||||
- 首先 CALayer 内部会创建一个 CGContextRef,在 drwaRect 方法中,可以通过上下文堆栈取出 context,拿到当前视图渲染上下文也就是 backing store
|
||||
|
||||
@@ -88,7 +88,7 @@ Render Server 是一个独立的渲染进程,当收到来自 Application 的 (
|
||||
|
||||
### 异步绘制流程
|
||||
|
||||

|
||||

|
||||
|
||||
- 如果 layer 有代理对象,且代理对象实现了代理方法,则可以进入异步绘制流程
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ iOS 上应用必须被沙盒化,各个不同 App 之间的 Defaults Domain
|
||||
|
||||
通过设置符号断点可以看出, NSUserDefaults 内部在读写时会通过 `os_unfair_lock` 加锁进行多线程安全保护。
|
||||
|
||||

|
||||

|
||||
|
||||
## 存储性能如何
|
||||
|
||||
@@ -178,7 +178,7 @@ iOS 上应用必须被沙盒化,各个不同 App 之间的 Defaults Domain
|
||||
|
||||
通过对代码添加符号断点 `xpc_connection_send_message_with_reply_sync` 可以看到下面的堆栈
|
||||
|
||||

|
||||

|
||||
|
||||
执行 `[NSUserDefaults standardUserDefaults];` 可以发现是调用了 XPC,创建名称为 “com.apple.cfprefsd.daemon” 的 XPC Connection,且会发送一个 `xpc_connection_send_message_with_reply_sync` 的消息。
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
和
|
||||
|
||||

|
||||

|
||||
|
||||
## RunLoop API
|
||||
|
||||
@@ -392,7 +392,7 @@ CFRelease(obersver);
|
||||
|
||||
但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈
|
||||
|
||||

|
||||

|
||||
|
||||
查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要
|
||||
|
||||
@@ -1501,7 +1501,7 @@ UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示
|
||||
|
||||
2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop,可以替换 API,比如设置一个变量,标记是否需要结束 RunLoop
|
||||
|
||||

|
||||

|
||||
|
||||
改进代码如下
|
||||
|
||||
|
||||
@@ -327,7 +327,7 @@ QA:优先级反转是什么?
|
||||
|
||||
线程本质上就是 CPU 高速切换,看上去是同时在做多个线程内的事情。操作系统会使用基于优先级抢占式调度算法。高优先级的线程始终在低优先级线程前执行。
|
||||
|
||||

|
||||

|
||||
|
||||
线程 A 在 T1 时刻拿到锁,并处理数据。
|
||||
|
||||
@@ -437,13 +437,13 @@ int cursorr = 1;
|
||||
|
||||
假如对存钱过程,忘记解锁怎么办?产生死锁,如下
|
||||
|
||||

|
||||

|
||||
|
||||
添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。
|
||||
|
||||
这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下
|
||||
|
||||

|
||||

|
||||
|
||||
### pthread_mutex
|
||||
|
||||
@@ -632,21 +632,21 @@ si:step instruction,简写为 stepi,si。当你在 Xcode 汇编面板看
|
||||
|
||||
第一步:当第二次调用 saveMoney 方法,开启汇编调试
|
||||
|
||||

|
||||

|
||||
|
||||
看到可疑方法 `OSSpinLockLock`,给它加断点,看到第10行高亮了。lldb 模式输入 c,敲回车。次数输入 si 即可进入 `OSSpinLockLock` 方法内部调试
|
||||
|
||||
第二步:继续输入 si,敲回车
|
||||
|
||||

|
||||

|
||||
|
||||
第三步:看到可疑方法 `_OSSpinLockLockSlow`,给它加断点,lldb 输入 C。此时断点到这一行了,继续输入 si。
|
||||
|
||||

|
||||

|
||||
|
||||
第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。
|
||||
|
||||

|
||||

|
||||
|
||||
发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,太浪费性能了。
|
||||
|
||||
@@ -974,7 +974,7 @@ void _dispatch_semaphore_dispose(dispatch_object_t dou,
|
||||
|
||||
为了探究下实现,开启汇编调试
|
||||
|
||||

|
||||

|
||||
|
||||
可以查看 objc4 的源码,查找 `objc_sync_enter`
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval
|
||||
|
||||
栈、堆、BSS、数据段、代码段
|
||||
|
||||

|
||||

|
||||
|
||||
栈(stack):又称作堆栈,用来存储程序的局部变量(但不包括static声明的变量,static修饰的数据存放于数据段中)。除此之外,在函数被调用时,栈用来传递参数和返回值。栈内存地址越来越少
|
||||
|
||||
@@ -147,7 +147,7 @@ Demo1
|
||||
|
||||
运行该代码会 Crash,报错信息如下
|
||||
|
||||

|
||||

|
||||
|
||||
说明:一开始的报错信息只说坏内存访问,但是并没有显示具体的方法调用堆。想知道具体 Crash 原因还是需要看看堆栈比较方便。输入 bt 查看最后是由于 `objc_release` 方法造成 crash。
|
||||
|
||||
@@ -257,7 +257,7 @@ static inline bool _objc_isTaggedPointer(const void * _Nullable ptr)
|
||||
|
||||
tips:某些对象虽然是 TaggedPointer 类型,但是打印 class 发现不是,猜测可能是系统用类簇隐藏了某些实现细节。比如下面
|
||||
|
||||

|
||||

|
||||
|
||||
针对 NSNumber 的 TaggedPoniter 的 case,查看 class 打印出 `__NSCFNumber`。但根据源码和内存高地址位分析确实是 TaggedPoniter。
|
||||
|
||||
@@ -826,7 +826,7 @@ void sel_init(size_t selrefCount){
|
||||
|
||||
在 gone 处加断点,利用 runtime 查看类中的方法信息
|
||||
|
||||

|
||||

|
||||
|
||||
发现存在 `.cxx_destruct` 方法。
|
||||
|
||||
@@ -861,7 +861,7 @@ void sel_init(size_t selrefCount){
|
||||
@end
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
Tips:@property 会自动生成成员变量,另外类后面加 `{}` 在内部也可以加成员变量,假如成员变量是对象类型,比如 NSString,则叫实例变量。
|
||||
|
||||
@@ -875,7 +875,7 @@ Tips:@property 会自动生成成员变量,另外类后面加 `{}` 在内部
|
||||
|
||||
在 gone 的地方加断点,输入 `watchpoint set variable p->_name`,则会将 `_name` 实例变量加入 watchpoint,当变量被修改时会触发断点,可以看出从某个值变为 0x0,也就是 nil。此时边上调用堆栈显示在 `objc_storestrong` 方法中,被设置为 nil.
|
||||
|
||||

|
||||

|
||||
|
||||
### 深入 .cxx_destruct
|
||||
|
||||
@@ -1123,7 +1123,7 @@ class AutoreleasePoolPage {
|
||||
- 每个 AutoreleasePoolPage 对象占用 4096 字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放 autorelease 对象的地址
|
||||
- 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个对象,parent 指向上一个对象
|
||||
|
||||

|
||||

|
||||
|
||||
```objectivec
|
||||
id * begin() {
|
||||
@@ -1181,7 +1181,7 @@ int main(int argc, const char * argv[]) {
|
||||
|
||||
main 方法内部3个 autoreleasepool 底层怎么样工作的?
|
||||
|
||||

|
||||

|
||||
|
||||
3个@auto releasepool, 系统遇到第一个的时候底层就是初始化一个结构体 `__AtAutoreleasePool`,结构体构造方法内部调用 `AutoreleasePoolPage::push` 方法,系统给 AutoreleasePoolPage 真正保存 autorelease 对象的地方存储进一个 `POOL_BOUNDARY` 对象,然后储存 P1、P2 对象地址,遇到第二个则继续初始化结构体,调用 push 方法,存储一个` POOL_BOUNDARY` 对象,继续保存 P3,遇到第三个则继续初始化结构体,调用 push 方法,存储一个 `POOL_BOUNDARY` 对象,继续保存 P4。
|
||||
|
||||
@@ -1981,7 +1981,7 @@ iOS 在主线程的 Runloop 中注册了2个 Observer
|
||||
|
||||
结合 RunLoop 运行图
|
||||
|
||||

|
||||

|
||||
|
||||
- 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush`
|
||||
|
||||
@@ -2070,7 +2070,7 @@ NSHashMap、NSMapTable 都可以描述 key、value 的内存修饰。
|
||||
|
||||
这段代码运行会 crash,信息如下
|
||||
|
||||

|
||||

|
||||
|
||||
原因是 NSError 构造方法内部会加 autorelease。源码如下
|
||||
|
||||
@@ -2133,7 +2133,7 @@ MRC 下的 `[(id)(object) autorelease]` 等价于 ARC 下的 `id __autoreleasing
|
||||
|
||||
我写了个僵尸对象检测工具,效果如下
|
||||
|
||||

|
||||

|
||||
|
||||
可以定位僵尸对象,并且打印出具体堆栈,并模拟系统行为调用 `abort` 。对监控原理和工具实现感兴趣的可以查看这里[带你打造一套 APM 监控系统-内存监控之野指针/内存泄漏监控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md#zombieSniffer)
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ dispatch_semaphore_t semaphore_;
|
||||
|
||||
说明:直接 `performSelector` 存在警告,可以告诉编译器忽略警告。可以在 Xcode 点开警告,查看详情,复制 `[]` 里面的字符串去忽略警告
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ KVC 之后会触发 KVO。为什么?探究下 `setValueForKey`
|
||||
|
||||
整个流程如下
|
||||
|
||||

|
||||

|
||||
|
||||
```
|
||||
@implementation Person
|
||||
@@ -315,6 +315,6 @@ valueForKey 原理
|
||||
|
||||
- 都没找到则抛出异常
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -957,7 +957,7 @@ c 数组指针 `array()->lists + addedCount` 可以代表其中的位置。
|
||||
|
||||
过程如下
|
||||
|
||||

|
||||

|
||||
|
||||
QA:
|
||||
|
||||
@@ -1716,7 +1716,7 @@ Category 是在运行阶段,才会将数据合并到类信息中。
|
||||
|
||||
查看分类在 Runtime 加载类信息时候的调用原理可以知道,分类中的类方法、对象方法都会被加载原始类的前面去(initialize 是类方法)如下图:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -257,12 +257,12 @@ sizeof 是运算符。
|
||||
|
||||
`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象
|
||||
|
||||

|
||||

|
||||
|
||||
instance 的 isa 指向 Class。当调用方法时,通过 instance 的 isa 找到 Class,最后找到对象方法的实现进行调用
|
||||
class 的 isa 指向 meta-class。当调用类方法的时,通过 class 的 isa 找到 meta-class,最后找到类方法进行调用。
|
||||
|
||||

|
||||

|
||||
|
||||
当 Student 实例对象调用 Person 的对象方法时,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,在类对象的对象方法列表中找到方法实现并调用。
|
||||
当 Stduent 实例对象调用 init 方法时候,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,找到 Person Claas 的 superClass 到 NSObject 类对象,在 NSObject 类对象的方法列表中找到 `init` 方法并调用。
|
||||
|
||||
@@ -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 代码见[此处](./../assets/CFRunLoop.c)。 Source1 是 RunLoop 用来处理 Mach port 传来的系统事件的,Source0 是用来处理用户事件的。收到 Source1 的系统事件后本质还是调用 Source0 事件的处理函数。
|
||||
完整且带有注释的 RunLoop 代码见[此处](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 指针指向僵尸类。
|
||||
|
||||
@@ -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。
|
||||
|
||||

|
||||

|
||||
|
||||
结论:
|
||||
|
||||
@@ -6428,7 +6428,7 @@ parseJSError(line, column);
|
||||
|
||||
5. 拿脚本解析好的行号、列号、文件信息去和源代码文件比较,结果很正确。
|
||||
|
||||

|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
上图展示了一个 ELK 的日志架构图。简单说明下:
|
||||
|
||||
@@ -7272,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 -> 产品侧(显示端)对数据进行分类、报表、报警等操作。
|
||||
|
||||
@@ -7290,7 +7290,7 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
|
||||
|
||||
现在很多架构设计都是微服务,至于为什么选微服务,不在本文范畴。所以 crash 日志的符号化被设计为一个微服务。架构图如下
|
||||
|
||||

|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
### 2. 问题及解决方案
|
||||
|
||||
@@ -7644,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)实现。
|
||||
|
||||
@@ -7658,7 +7658,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
|
||||
|
||||
好像用 APM 的卡顿没发现啥问题。仔细看看发现了问题,我们的卡顿只是监控主线程方法耗时。一个页面加载后可能会触发一些网络请求功能,子线程请求完网络,可能会做一些数组裁剪、组装,拼接为 UI 所需要的信息,这个时候很可能会发生卡顿,所以这种 case 下卡顿监控是无法覆盖的。用下图表示
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
说明:
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ isa 在 arm64 之后必须通过 `ISA_MASK` 去查询 class(类对象、元类
|
||||
|
||||
`0x0000000ffffffff8ULL` 用程序员模式打开计算器
|
||||
|
||||

|
||||

|
||||
|
||||
其中,结构体中的数据存放大体是下面的结构:
|
||||
|
||||
@@ -343,7 +343,7 @@ struct class_ro_t {
|
||||
|
||||
具体关系整理如下图
|
||||
|
||||

|
||||

|
||||
|
||||
说明:
|
||||
|
||||
@@ -351,7 +351,7 @@ struct class_ro_t {
|
||||
|
||||
为什么不是二维数组?因为Array 中的子 Array长度不一致,且不能补空
|
||||
|
||||

|
||||

|
||||
|
||||
```c
|
||||
static void remethodizeClass(Class cls)
|
||||
@@ -436,7 +436,7 @@ struct class_ro_t {
|
||||
|
||||
- `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容
|
||||
|
||||

|
||||

|
||||
|
||||
## Method_t
|
||||
|
||||
@@ -474,7 +474,7 @@ typedef struct objc_selector *SEL;
|
||||
|
||||
iOS 中提供了一个叫做 `@encode` 的指令,可以将具体的类型表示成字符串编码
|
||||
|
||||

|
||||

|
||||
|
||||
```objectivec
|
||||
- (int)calcuate:(int)age heigith:(float)height;
|
||||
@@ -826,7 +826,7 @@ NSLog(@"%s %p", bucket._key, bucket._imp);
|
||||
// personSay 0xbec8
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
原理就是根据类对象结构体找到 cache 结构体,cache 结构体内部的 `_buckets` 是一个方法散列表,查看源代码,根据散列表的哈希寻找策略 `(key & mask)` 找到哈希索引,然后找到方法对象 bucket,其中寻找方法索引的 key 就是 方法 selector。
|
||||
|
||||
@@ -1411,7 +1411,7 @@ for 循环不断查找,找当前类的父类,直到当前类为 nil。
|
||||
|
||||
上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示
|
||||
|
||||

|
||||

|
||||
|
||||
### 动态方法解析阶段
|
||||
|
||||
@@ -1546,7 +1546,7 @@ SEL_resolveClassMethod, sel);`
|
||||
|
||||
完整流程如下
|
||||
|
||||

|
||||

|
||||
|
||||
上 Demo
|
||||
|
||||
@@ -1686,7 +1686,7 @@ void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
|
||||
|
||||
为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二
|
||||
|
||||

|
||||

|
||||
|
||||
```c
|
||||
int __forwarding__(void *frameStackPointer, int isStret) {
|
||||
@@ -1730,7 +1730,7 @@ int __forwarding__(void *frameStackPointer, int isStret) {
|
||||
|
||||
完整流程如下
|
||||
|
||||

|
||||

|
||||
|
||||
上 Demo
|
||||
|
||||
@@ -1951,7 +1951,7 @@ objc_msgSendSuper(arg, sel_registerName("class"))
|
||||
|
||||
我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2`
|
||||
|
||||

|
||||

|
||||
|
||||
查看 objc4 源代码发现是一段汇编实现。
|
||||
|
||||
@@ -2185,7 +2185,7 @@ void test () {
|
||||
|
||||
方法内的变量存储在栈上,堆向上增长,栈向下增长。
|
||||
|
||||

|
||||

|
||||
|
||||
3.**实例对象的本质就是一个结构体,存储所有成员变量(isa 是一个特殊成员变量,其他的成员变量,这里就是 _name),`sayHi` 方法内部的 self 就是 obj,找成员变量的本质就是找内存地址的过程(此时就是偏移8个字节)**
|
||||
|
||||
@@ -2234,7 +2234,7 @@ objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
|
||||
|
||||
所以此时的“前一个局部变量” 也就是结构体 `objc_super` 类型的 arg。arg 是一个结构体,结构体第一个成员变量就是 self,所以“前一个局部变量” 也就是 self(ViewController)
|
||||
|
||||

|
||||

|
||||
|
||||
## 应用场景
|
||||
|
||||
@@ -2268,7 +2268,7 @@ Person *p = [Person new];
|
||||
object_setClass(p, [Student class]);
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
3.动态创建类
|
||||
|
||||
@@ -2295,7 +2295,7 @@ void createClass (void) {
|
||||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
runtime 中 copy、create 等出来的内存,不使用的时候需要手动释放`objc_disposeClassPair(newClass>)`
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ block 本质上就是一个 oc 对象,也有 isa 指针
|
||||
|
||||
block 是封装了函数调用和函数调用环境的 OC 对象
|
||||
|
||||

|
||||

|
||||
|
||||
```objectivec
|
||||
int age = 27;
|
||||
@@ -247,7 +247,7 @@ NSLog(@"%@", [[block class] superclass]); // NSBlock
|
||||
NSLog(@"%@", [[[block class] superclass] superclass]); // NSObjec
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
代码存放在 text 段,static 修饰的数据存放在 data 区,程序员手动申请的内存存放在堆,局部变量存放在栈区
|
||||
|
||||
@@ -598,7 +598,7 @@ static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
|
||||
|
||||
可以看到 `__block int age = 27;` 变为了 `__Block_byref_age_0 age` 结构体。block 内部的函数在修改 age 的时候其实就是通过 `__main_block_impl_0` 结构体的 age 找到 `__Block_byref_age_0`,然后访问 `__Block_byref_age_0` 中的成员变量 `__forwarding` 访问成员变量 age,并修改值。
|
||||
|
||||

|
||||

|
||||
|
||||
`__block` 修饰基本数据类型和对象,对于生成的结构体也不一样。
|
||||
|
||||
@@ -692,7 +692,7 @@ int main(int argc, const char * argv[]) {
|
||||
|
||||
我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。
|
||||
|
||||

|
||||

|
||||
|
||||
```c
|
||||
// 0x0000000105231f70
|
||||
@@ -736,7 +736,7 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
|
||||
|
||||
## `__forwarding` 的设计
|
||||
|
||||

|
||||

|
||||
|
||||
当block在栈中时,`__Block_byref_age_0`结构体内的`__forwarding`指针指向结构体自己。
|
||||
|
||||
@@ -750,7 +750,7 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
|
||||
|
||||
被 `__block ` 修饰符修饰的对象在内存中如下
|
||||
|
||||

|
||||

|
||||
|
||||
```c
|
||||
|
||||
@@ -871,7 +871,7 @@ p.block();
|
||||
|
||||
`__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存,所以不推荐。ARC 下最佳用 `__weak`
|
||||
|
||||

|
||||

|
||||
|
||||
### MRC 下
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ MH_DSYM:存储着二进制文件符号。`.dSYM/Contents/Resource/DWARF/xx`
|
||||
|
||||
Xcode 中也可以查看 Mach-O 文件类型
|
||||
|
||||

|
||||

|
||||
|
||||
### Universal Binary
|
||||
|
||||
@@ -137,7 +137,7 @@ Xcode 中也可以查看 Mach-O 文件类型
|
||||
|
||||
## Mach-O 结构
|
||||
|
||||

|
||||

|
||||
|
||||
一个 Mach-O 文件包含3块
|
||||
|
||||
@@ -149,13 +149,13 @@ Xcode 中也可以查看 Mach-O 文件类型
|
||||
|
||||
可以用 [GitHub - fangshufeng/MachOView: 分析Macho必备工具](https://github.com/fangshufeng/MachOView) 和系统自带的 atool 查看 Mach-O 信息
|
||||
|
||||

|
||||

|
||||
|
||||
用 MachOView 查看 DDD Mach-O 文件
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
可以看到在 Mach-O 文件上,`__PAGEZERO` 的 `VM Size` 有值,但是 File Size 为0,也就是说 `__PAGEZERO` 在 Mach-O 中不占据内存,但是程序运行起来之后,会占据虚拟内存。所以代码段在 Mach-O 中 File Offset 为0(如果前面的 `__PAGEZERO` 的 File size 有值,这里的 File Offset 就不为0)。
|
||||
|
||||
@@ -179,9 +179,9 @@ Xcode 中也可以查看 Mach-O 文件类型
|
||||
|
||||
也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
我们会发现根据 Mach-O 文件中的信息,File Size、File Offset、VM Address、VM Size 可以判断出 `__TEXT` 段内函数信息,这样子不够安全
|
||||
|
||||
@@ -189,7 +189,7 @@ Xcode 中也可以查看 Mach-O 文件类型
|
||||
|
||||
Address Space Layout Randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码的位置,达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。
|
||||
|
||||

|
||||

|
||||
|
||||
- LC_SEGMENT (__TEXT) 的 VM Address 0x10005000
|
||||
|
||||
|
||||
Reference in New Issue
Block a user