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,8 +2133,28 @@ MRC 下的 `[(id)(object) autorelease]` 等价于 ARC 下的 `id __autoreleasing
|
||||
|
||||
我写了个僵尸对象检测工具,效果如下
|
||||
|
||||

|
||||

|
||||
|
||||
可以定位僵尸对象,并且打印出具体堆栈,并模拟系统行为调用 `abort` 。对监控原理和工具实现感兴趣的可以查看这里[带你打造一套 APM 监控系统-内存监控之野指针/内存泄漏监控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md#zombieSniffer)
|
||||
|
||||
Demo [👇这里](https://github.com/FantasticLBP/BlogDemos/tree/master/僵尸对象探针)
|
||||
Demo [👇这里](https://github.com/FantasticLBP/BlogDemos/tree/master/僵尸对象探针)
|
||||
|
||||
### 内存是连续的吗?
|
||||
|
||||
应用启动后,Mach-O 文件是分段载入内存的。我们使用的内存都是虚拟内存,通过内存映射表来做。
|
||||
|
||||
每个进程在创建加载时,会被分配一个大小大概为1~2倍真实地内存的连续虚拟地址空间,让当前软件认为自己拥有一块很大内存空间。实际上是把磁盘的一小部分作为假想内存来使用。
|
||||
|
||||
CPU 不直接和物理内存打交道,而是通过 MMU(Memory Manage Unit,内存管理单元),MMU 是一种硬件电路,速度很快,主要工作是内存管理,地址转换是功能之一。
|
||||
|
||||
每个进程都会有自己的页表 `Page Table` ,页表存储了进程中虚拟地址到物理地址的映射关系,所以就相当于地图。MMU 收到 CPU 的虚拟地址之后就开始查询页表,确定是否存在映射以及读写权限是否正常。
|
||||
|
||||
iOS 程序在进行加载时,会根据一 page 大小16kb 将程序分割为多页,启动时部分的页加载进真实内存,部分页还在磁盘中,中间的调度记录在一张内存映射表(Page Table),这个表用来调度磁盘和内存两者之间的数据交换。
|
||||
|
||||
如上图,App 运行时执行某个任务时,会先访问虚拟页表,如果页表的标记为1,则说明该页面数据已经存在于内存中,可以直接访问。如果页表为0,则说明数据未在物理内存中,这时候系统会阻塞进程,叫做缺页中断(page fault),进程会从用户态切换到内核态,并将缺页中断交给内核的 page Fault Handler 处理。等将对应的 page 从磁盘加载到内存之后再进行访问,这个过程叫做 page in。
|
||||
|
||||
因为磁盘访问速度较慢,所以 page in 比较耗时,而且 iOS 不仅仅是将数据加载到内存中,还要对这页做 Code Sign 签名认证,所以 iOS 耗时更长
|
||||
|
||||
Tips:Code Sign 加密哈希并不少针对于整个文件,而是针对于每一个 Page 的,保证了在 dyld 进行加载的时候,可以对每一个 page 进行独立验证。
|
||||
|
||||
等到程序运行时用到了才去内存中寻找虚拟地址对应的页帧,找不到才进行分配,这就是内存的惰性(延时)分配机制。
|
||||
@@ -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 是类方法)如下图:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -289,8 +289,6 @@ Framework 只是打包方式,动态、静态库都可以支持。甚至可以
|
||||
|
||||
基于上述2个问题,诞生了虚拟内存技术。
|
||||
|
||||
|
||||
|
||||
### 内存缺页异常
|
||||
|
||||
每个进程在创建加载时,会被分配一个大小大概为1~2倍真实地内存的连续虚拟地址空间,让当前软件认为自己拥有一块很大内存空间。实际上是把磁盘的一小部分作为假想内存来使用。
|
||||
@@ -299,34 +297,28 @@ CPU 不直接和物理内存打交道,而是通过 MMU(Memory Manage Unit,
|
||||
|
||||
每个进程都会有自己的页表 `Page Table` ,页表存储了进程中虚拟地址到物理地址的映射关系,所以就相当于地图。MMU 收到 CPU 的虚拟地址之后就开始查询页表,确定是否存在映射以及读写权限是否正常。
|
||||
|
||||
iOS 程序在进行加载时,会根据一 page 大小16kb 将程序分割为多页,启动时部分的页加载进真实内存,部分页还在磁盘中,中间的调度记录在一张表(Page Table),这个表用来调度磁盘和内存两者之间的数据交换。
|
||||
iOS 程序在进行加载时,会根据一 page 大小16kb 将程序分割为多页,启动时部分的页加载进真实内存,部分页还在磁盘中,中间的调度记录在一张内存映射表(Page Table),这个表用来调度磁盘和内存两者之间的数据交换。
|
||||
|
||||

|
||||

|
||||
|
||||
如上图,App 运行时执行某个任务时,会先访问虚拟页表,如果页表的标记为1,则说明该页面数据已经存在于内存中,可以直接访问。如果页表为0,则说明数据未在物理内存中,这时候系统会阻塞进程,叫做缺页中断(page fault),进程会从用户态切换到内核态,并将缺页中断交给内核的 page Fault Handler 处理。等将对应的 page 从磁盘加载到内存之后再进行访问,这个过程叫做 page in。
|
||||
|
||||
因为磁盘访问速度较慢,所以 page in 比较还是,而且 iOS 不仅仅是将数据加载到内存中,还要多这页做签名认证,所以 iOS 耗时更长
|
||||
因为磁盘访问速度较慢,所以 page in 比较耗时,而且 iOS 不仅仅是将数据加载到内存中,还要对这页做 Code Sign 签名认证,所以 iOS 耗时更长
|
||||
|
||||
Tips:Code Sign 加密哈希并不少针对于整个文件,而是针对于每一个 Page 的,保证了在 dyld 进行加载的时候,可以对每一个 page 进行独立验证。
|
||||
|
||||
等到程序运行时用到了才去内存中寻找虚拟地址对应的页帧,找不到才进行分配,这就是内存的惰性(延时)分配机制。
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
为了提高效率和方便管理,对虚拟内存和物理内存进行分页(Page)管。进程在访问虚拟内存的一个 page 而对应的物理内存却不存在(没有被加载到物理内存中),则会触发一次缺页异常(缺页中断),然后分配物理内存,有需要的话会从磁盘 mmap 读入数据。
|
||||
|
||||
|
||||
|
||||
启动时所需要的代码分布在 VM 的第一页、第二页、第三页...,这样的情况下启动时间会影响较大,所以解决思路就是将应用程序启动刻所需要的代码(二进制优化一下),统一放到某几页,这样就可以避免内存缺页异常,则优化了 App 启动时间。
|
||||
|
||||
二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。
|
||||
|
||||
一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理,放到某一页或者某几页」(虚拟内存中的页)。Xcode 工程允许开发者指定 「Order File」,可以「按照文件中的方法顺序去加载」,可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。
|
||||
|
||||
|
||||
|
||||
### 如何获取启动阶段的 Page Fault 次数
|
||||
|
||||
Instrucments 中的 System Trace 可以查看详细信息。
|
||||
@@ -341,7 +333,7 @@ Instrucments 中的 System Trace 可以查看详细信息。
|
||||
|
||||
其实二进制重排 Apple 自己本身就在用,查看 `objc4` 源码的时候就发现了身影
|
||||
|
||||

|
||||

|
||||
|
||||
Xcode 使用的链接器为 `ld`。 ld 有个参数 `-order_file` 。order_file 中的符号会按照顺序排列在对应 section 的开始。
|
||||
|
||||
@@ -361,24 +353,41 @@ clang 插桩,才可以 hook OC、C、block、Swift 全部。LLVM 官方文档
|
||||
|
||||
其实难点是如何拿到启动时刻所调用的所用方法?代码可能是 Swift、block、c、OC,所以hook 肯定不行、fishhook 也不行,用 clang 插桩可行
|
||||
|
||||
- https://mp.weixin.qq.com/s/SUHaGD1T2Vce4Ag-qgxtgg
|
||||
步骤:
|
||||
|
||||
在看 App 启动时间优化之前先看2个方法: **load** 和 **initialize**。
|
||||
- 在 Xcode Build Setting 下搜索 “Other c Flags”,在后面添加 `-fsanitize-coverage=trace-pc-guard`
|
||||
|
||||
load
|
||||
- 在工程入口文件添加2个方法来解决编译报错问题 `__sanitizer_cov_trace_pc_guard_init`、`__sanitizer_cov_trace_pc_guard`
|
||||
|
||||
> Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.
|
||||
> 当一个类或者它的分类被加载到 Objective-c 的 runtime 中的时候会出发 load 方法。可以实现这个方法去执行一些类特定的行为。
|
||||
- clang 插桩原理就是给每个(oc、c)方法、block 等方法内部第一行添加 hook 代码,来实现 AOP 效果。所以在 `__sanitizer_cov_trace_pc_guard` 内部将函数的名称打印出来,最后可以统一写入 order 文件
|
||||
|
||||
```objectivec
|
||||
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
|
||||
uint32_t *stop) {
|
||||
static uint64_t N; // Counter for the guards.
|
||||
if (start == stop || *start) return; // Initialize only once.
|
||||
printf("INIT: %p %p\n", start, stop);
|
||||
for (uint32_t *x = start; x < stop; x++)
|
||||
*x = ++N; // Guards should start from 1.
|
||||
}
|
||||
|
||||
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
|
||||
void *PC = __builtin_return_address(0);
|
||||
Dl_info info;
|
||||
dladdr(PC, &info);
|
||||
printf("name: %s\n", info.dli_sname);
|
||||
}
|
||||
```
|
||||
|
||||
initialize
|
||||
- 可能会存在 while、for 循环 case,所以为了避免死循环,需修改 "Other c Flags" 配置为 `-fsanitize-coverage=func,trace-pc-guard`。func 表示仅 hook 函数时调用
|
||||
|
||||
> Initializes the class before it receives its first message.
|
||||
> 当一个类接收到第一条消息的时候会初始化。
|
||||
- 最后修改 Build Setting 中的 "Order File" 配置项
|
||||
|
||||
load 方法会在类被加载到 runtime 的时候调用。且父类的 load 方法比子类先执行。load 方法只会执行1次。
|
||||
initialize 方法会在第一次收到消息的时候调用。父类的 initialize 方法比子类先执行。假如有 Person 类,还有一个子类 children。子类第一次收到消息的时候会先调用父类的 initialize,然后调用子类的 initialize,如果子类没有实现 initialize 那么父类的 initialize 会执行多次。
|
||||
|
||||
总结:
|
||||
|
||||
|
||||
|
||||
## 总结
|
||||
|
||||
启动优化思路主要是先监控发现具体的启动时间和启动阶段对应的各个任务,有了具体数据,才可以谈优化。
|
||||
|
||||
@@ -390,8 +399,6 @@ initialize 方法会在第一次收到消息的时候调用。父类的 initiali
|
||||
|
||||
- 启动阶段必须的任务,如果不适合并发,则利用技术手段将代码加速
|
||||
|
||||
|
||||
|
||||
## 参考
|
||||
|
||||
- [# 抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%](https://mp.weixin.qq.com/s/Drmmx5JtjG3UtTFksL6Q8Q)
|
||||
|
||||
@@ -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` 方法并调用。
|
||||
|
||||
@@ -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