feat: refine

This commit is contained in:
LiuBinPeng
2022-05-30 15:02:31 +08:00
parent 538801e651
commit 6cd0cf5144
60 changed files with 135 additions and 1487 deletions

View File

@@ -289,8 +289,6 @@ Framework 只是打包方式,动态、静态库都可以支持。甚至可以
基于上述2个问题诞生了虚拟内存技术。
### 内存缺页异常
每个进程在创建加载时会被分配一个大小大概为12倍真实地内存的连续虚拟地址空间让当前软件认为自己拥有一块很大内存空间。实际上是把磁盘的一小部分作为假想内存来使用。
@@ -299,34 +297,28 @@ CPU 不直接和物理内存打交道,而是通过 MMUMemory Manage Unit
每个进程都会有自己的页表 `Page Table` 页表存储了进程中虚拟地址到物理地址的映射关系所以就相当于地图。MMU 收到 CPU 的虚拟地址之后就开始查询页表,确定是否存在映射以及读写权限是否正常。
iOS 程序在进行加载时,会根据一 page 大小16kb 将程序分割为多页启动时部分的页加载进真实内存部分页还在磁盘中中间的调度记录在一张表Page Table这个表用来调度磁盘和内存两者之间的数据交换。
iOS 程序在进行加载时,会根据一 page 大小16kb 将程序分割为多页,启动时部分的页加载进真实内存,部分页还在磁盘中,中间的调度记录在一张内存映射Page Table这个表用来调度磁盘和内存两者之间的数据交换。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/iOSPageInPageOut.png)
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/iOSPageInPageOut.png)
如上图App 运行时执行某个任务时会先访问虚拟页表如果页表的标记为1则说明该页面数据已经存在于内存中可以直接访问。如果页表为0则说明数据未在物理内存中这时候系统会阻塞进程叫做缺页中断page fault进程会从用户态切换到内核态并将缺页中断交给内核的 page Fault Handler 处理。等将对应的 page 从磁盘加载到内存之后再进行访问,这个过程叫做 page in。
因为磁盘访问速度较慢,所以 page in 比较还是,而且 iOS 不仅仅是将数据加载到内存中,还要这页做签名认证,所以 iOS 耗时更长
因为磁盘访问速度较慢,所以 page in 比较耗时,而且 iOS 不仅仅是将数据加载到内存中,还要这页做 Code Sign 签名认证,所以 iOS 耗时更长
TipsCode Sign 加密哈希并不少针对于整个文件,而是针对于每一个 Page 的,保证了在 dyld 进行加载的时候,可以对每一个 page 进行独立验证。
等到程序运行时用到了才去内存中寻找虚拟地址对应的页帧,找不到才进行分配,这就是内存的惰性(延时)分配机制。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/PageFault.png)
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/PageFault.png)
为了提高效率和方便管理对虚拟内存和物理内存进行分页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` 源码的时候就发现了身影
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/Objc-OrderFile.png)
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Objc-OrderFile.png)
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)