1. Main函数之前的性能检测

应用的启动时间,一般分为Main函数执行之前和之后,执行之前称之为pre-main

系统提供了环境变量,让开发者可以看到pre-main过程中的耗时

查看方式:在Xcode中,选择项目SchemesRunArguments,添加DYLD_PRINT_STATISTICS环境变量,设置为YES
image.png

运行项目,lldb中出现耗时相关打印

  1. Total pre-main time: 1.8 seconds (100.0%)
  2. dylib loading time: 526.41 milliseconds (28.1%)
  3. rebase/binding time: 165.85 milliseconds (8.8%)
  4. ObjC setup time: 324.80 milliseconds (17.3%)
  5. initializer time: 853.94 milliseconds (45.6%)
  6. slowest intializers :
  7. libSystem.B.dylib : 10.44 milliseconds (0.5%)
  8. libMainThreadChecker.dylib : 58.23 milliseconds (3.1%)
  9. libglInterpose.dylib : 318.94 milliseconds (17.0%)
  10. AFNetworking : 39.55 milliseconds (2.1%)
  11. NELivePlayerFramework : 62.94 milliseconds (3.3%)
  12. XXXXX : 369.68 milliseconds (19.7%)
  • dylib loading time:动态库的载入耗时

    • 动态库的载入肯定会存在耗时,并且动态库会存在依赖关系。系统动态库存在于共享缓存,但自定义动态库没有这个待遇,所以苹果官方建议不要超过6个自定义动态库,超过可进行多个动态库合并,以此来优化动态库加载的耗时

    • 动态库的合并,需要源码才能进行。所以我们只能合并自己开发的动态库,日常使用的三方SDK可能无法合并

  • rebase/binding time:重定位符号和符号绑定的耗时

    • rebase:系统采用ASLR技术,保证地址空间随机化。所以在运行时,需要通过rebase进行重定位符号,使用ASLR+偏移地址

    • binding:使用外部符号,编译时无法找到函数地址。所以在运行时,dyld加载共享缓存,加载链接动态库之后,进行binding操作,重新绑定外部符号

  • ObjC setup time:注册OC类的耗时

    • 注册OC类的过程,读取二进制的data段找到OC的相关信息,然后注册OC类。应用启动时,系统会生成类和分类的两张表,OC类和分类的注册,会插入到这两张表中,所以会造成一定的时间消耗

    • 这部分时间很难优化,除非减少项目中类和分类的定义

    • 减少类和所属分类load方法的使用,让类以懒加载的方式加载

  • initializer time:执行load以及C++构造函数的耗时

    • 尽可能使用initialize方法代替load方法
  • slowest intializers:列举出几个比较耗时的动态库

2. 虚拟内存

2.1 概述

2.1.1 早期的操作系统

早期的操作系统,并没有虚拟内存的概念。系统由进程直接访问内存中的物理地址,这种方式存在严重的安全隐患。内存中的不同进程,可以计算出它们的物理地址,可以跨进程访问,可以随意进行数据的篡改

早期的程序也比较小,在运行时,会将整个程序全部加载到内存中。但随着软件的发展,程序越来越大,而且还有大型游戏的诞生,导致内存越来越紧张。这就是早期系统中,为什么经常出现内存不足的提示

所以,直接使用物理内存的弊端:

  • 可以跨进程访问,数据不安全

  • 将整个程序加载到内存,导致内存浪费

2.1.2 虚拟内存系统

现代的操作系统都引入了虚拟内存,进程持有的虚拟地址(Virtual Address)会经过内存管理单元(Memory Mangament Unit)的转换变成物理地址,然后再通过物理地址访问内存

操作系统以页为单位管理内存,在iOS系统中,一页为16KB。所以虚拟地址和物理地址的映射表,也称之为页表。页表存储在内存中,有了页表,就可以将程序和物理内存完全阻隔开

一个进程中,只有部分功能是活跃的,所以只需要将进程中活跃的部分放入物理内存,避免物理内存的浪费

现代的操作系统进行了更合理的优化,例如iOS系统中,当进程被加载时,虚拟内存中会开辟4G的空间(假空间),用于存放MachO、堆区、栈区。但物理内存中,并未真的分配。当数据加载到页表中,系统会配合CPU进行地址翻译,然后载入到物理内存中。地址翻译的过程,由CPU上的内存管理单元(MMU)完成

页表中记录了内存页的状态、虚拟内存和物理内存的对应关系。其中状态分为:未分配(Unallocated)、未缓存(Uncached)和已缓存(Cached

  • 未分配的内存页,是没有被进程申请使用的,也就是空闲的虚拟内存,不占用虚拟内存磁盘的任何空间

  • 未缓存的内存页,仅在虚拟内存中,没有被物理内存缓存

  • 已缓存的内存页,同时存在于虚拟内存和物理内存中

使用虚拟内存的优势:

  • 程序以懒加载的方式加载到内存中,按需加载,避免内存浪费

  • 将程序和物理内存完全阻隔开,无法跨进程访问,数据更安全

进程通信由系统提供API,使用kernel发送信号。但不能直接跨进程访问,保证数据的安全

2.2 缺页中断

  • 当程序访问未被缓存的内存页时,就会触发缺页中断

  • 缺页中断会将当前进程阻塞掉,此时需要先将数据载入到物理内存,然后再寻址,进行读取

  • 部分情况下,被访问的页面已经加载到物理内存中,但页表中并不存在该对应关系,这时只需要在页表中建立虚拟内存到物理内存的关系即可

  • 其他情况下,操作系统需要将磁盘上未被缓存的虚拟页加载到物理内存中

2.3 页面置换

物理内存的空间是有限的,当内存中没有空间时,操作系统会从选择合适的物理内存页驱逐回磁盘,为新的内存页让出位置,选择待驱逐页的过程在操作系统中叫做页面置换

例如,同一台设备上,依次打开微信、微博、淘宝、京东、抖音,此时再回到微信,又会看到微信的启动界面。因为系统在内存紧张的时候,会按照活跃度将最不活跃的内存进行覆盖

对于微信来说,程序进程还存在于系统中,所以进行热启动

  • 冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动

  • 热启动:当启动应用时,后台已有该应用的进程(例:按home键回到桌面,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动

3. ASLR

程序的代码在不修改的情况下,每次加载到虚拟内存中的地址都是一样的,这样的方式并不安全。为了解决地址固定的问题,出现了ASLR技术

ASLRAddress space layout randomization):是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的

大部分主流的操作系统已经实现了ASLR

  • Linux:在内核版本2.6.12中添加ASLR

  • WindowsWindows Server 2008Windows 7Windows VistaWindows Server 2008 R2,默认情况下启用ASLR,但它仅适用于动态链接库和可执行文件

  • Mac OS XAppleMac OS X Leopard10.52007年十月发行)中某些库导入了随机地址偏移,但其实现并没有提供ASLR所定义的完整保护能力。而Mac OS X Lion10.7则对所有的应用程序均提供了ASLR支持。Apple宣称为应用程序改善了这项技术的支持,能让3264位的应用程序避开更多此类攻击。从OS X Mountain Lion10.8开始,核心及核心扩充(kext)与zones在系统启动时也会随机配置

  • iOSiPhoneiPod touchiPad):AppleiOS4.3内导入了ASLR

  • AndroidAndroid 4.0提供地址空间配置随机加载(ASLR),以帮助保护系统和第三方应用程序免受由于内存管理问题的攻击,在Android 4.1中加入地址无关代码(position-independent code)的支持

4. 二进制重排

4.1 缺页中断的消耗

当系统访问虚拟内存时,发现数据还未加载到物理内存中,会触发缺页中断(Page Fault),造成进程阻塞。此时系统会先将数据加载到物理内存中,进程才能继续运行。虽然每一页数据加载到内存的速度很快,毫秒级别,但在应用冷启动时,可能会出现大量的缺页中断,对启动速度带来一定的时间消耗

使用测试项目,查看应用在启动过程中,Page Fault所带来的消耗

Xcode菜单中,选择ProductProfile
image.png

打开Instruments
image.png

运行测试项目,当第一个界面出来后即可停止,搜索main thread
image.png

  • 一个小测试项目,启动时缺页中断564次,耗时200毫秒。如果是微信、抖音等大型项目,不进行优化可达到6000次以上,造成不小的时间消耗

4.2 二进制重排的原理

搭建测试项目,查看代码顺序

打开项目,在Build SettingsWrite Link Map File,设置为YES
image.png

编译项目,来到工程的Build目录下,找到LinkMap文件
image.png

LinkMap文件,保存了项目在编译链接时的符号顺序,以方法/函数为单位排列

  1. # Symbols:
  2. # Address Size File Name
  3. 0x100005F80 0x0000002C [ 1] +[ViewController load]
  4. 0x100005FAC 0x00000048 [ 1] -[ViewController viewDidLoad]
  5. 0x100005FF4 0x0000007C [ 2] -[AppDelegate application:didFinishLaunchingWithOptions:]
  6. 0x100006070 0x00000100 [ 2] -[AppDelegate application:configurationForConnectingSceneSession:options:]
  7. 0x100006170 0x00000074 [ 2] -[AppDelegate application:didDiscardSceneSessions:]
  8. 0x1000061E4 0x0000009C [ 3] _main
  9. 0x100006280 0x0000009C [ 4] -[SceneDelegate scene:willConnectToSession:options:]
  10. 0x10000631C 0x0000004C [ 4] -[SceneDelegate sceneDidDisconnect:]
  11. 0x100006368 0x0000004C [ 4] -[SceneDelegate sceneDidBecomeActive:]
  12. 0x1000063B4 0x0000004C [ 4] -[SceneDelegate sceneWillResignActive:]
  13. 0x100006400 0x0000004C [ 4] -[SceneDelegate sceneWillEnterForeground:]
  14. 0x10000644C 0x0000004C [ 4] -[SceneDelegate sceneDidEnterBackground:]
  15. 0x100006498 0x00000024 [ 4] -[SceneDelegate window]
  16. 0x1000064BC 0x0000003C [ 4] -[SceneDelegate setWindow:]
  17. 0x1000064F8 0x0000003C [ 4] -[SceneDelegate .cxx_destruct]
  18. 0x100006534 0x0000000C [ 5] _NSLog
  19. ...

文件编译顺序是XcodeBuild PhasesCompile Sources的文件排列顺序
image.png

文件中方法/函数的符号顺序,就是代码的书写顺序
image.png

  • ViewController.m为例,load方法在viewDidLoad方法之前,和LinkMap文件中的顺序一致

所以,按照默认配置,在应用启动时,会加载到大量与启动时无关的代码,导致Page Fault的次数增长,影响启动时间。如果可以将启动时需要的方法/函数排列在最前面,就能大大降低缺页中断的可能性,从而提升应用的启动速度,这就是二进制重排的核心原理

4.3 二进制重排的配置

二进制重排的配置非常简单,只需要在工程中创建.order文件,按固定格式,将启动时需要的方法/函数顺序排列,然后在Xcode中使用.order文件即可。通过LinkMap文件中的顺序,查看最终的排序是否符合预期

在工程根目录创建.order文件
image.png

打开hk.order文件,写入启动时需要的方法/函数

  1. +[ViewController load]
  2. +[AppDelegate load]
  3. _main

Xcode使用.order文件,在Build SettingOrder File中配置
image.png

编译项目,打开LinkMap文件

  1. # Symbols:
  2. # Address Size File Name
  3. 0x100005F54 0x0000002C [ 1] +[ViewController load]
  4. 0x100005F80 0x0000002C [ 2] +[AppDelegate load]
  5. 0x100005FAC 0x0000009C [ 3] _main
  6. 0x100006048 0x00000048 [ 1] -[ViewController viewDidLoad]
  7. 0x100006090 0x0000007C [ 2] -[AppDelegate application:didFinishLaunchingWithOptions:]
  8. ...
  • 最前面三个方法/函数,按照.order文件中的顺序排列

由此可见,如果我们将项目中,启动时需要调用的所有方法/函数都找到,把它们全部写入到.order文件中,就能大大降低缺页中断的可能性。但真正的难点是,如何能找到项目中启动时需要调用的所有方法和函数

5. Clang插庄

在项目中,对于OC方法,可以对objc_msgSend方法进行HOOK。这样仅适用于OC方法,对于C函数BlockSwift的方法/函数,都无法拦截

LLVM内置了一个简单的代码覆盖率检测工具(SanitizerCoverage)。它在函数级、基本块级和边缘级上插入对用户定义函数的调用,通过这种方式,可以顺利对OC方法、C函数BlockSwift的方法/函数进行全面HOOK

官方文档:https://clang.llvm.org/docs/SanitizerCoverage.html

5.1 配置

搭建测试项目,在Build SettingOther C Flags中,增加-fsanitize-coverage=trace-pc-guard的配置
image.png

按照文档,在项目中加入示例代码

  1. #import "ViewController.h"
  2. #include <stdint.h>
  3. #include <stdio.h>
  4. #include <sanitizer/coverage_interface.h>
  5. @implementation ViewController
  6. - (void)viewDidLoad {
  7. [super viewDidLoad];
  8. }
  9. void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
  10. static uint64_t N;
  11. if (start == stop || *start) return;
  12. printf("INIT: %p %p\n", start, stop);
  13. for (uint32_t *x = start; x < stop; x++)
  14. *x = ++N;
  15. }
  16. void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  17. if (!*guard) return;
  18. void *PC = __builtin_return_address(0);
  19. char PcDescr[1024];
  20. // printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
  21. }
  22. @end

5.2 __sanitizer_cov_trace_pc_guard_init

运行项目,打印以下内容:

  1. INIT: 0x100bbd4c0 0x100bbd4f8
  • 打印来自__sanitizer_cov_trace_pc_guard_init函数

  • 通过for代码中的循环,不难看出,从startstop的地址中,存储的是uint32_t类型的值

  • 循环中xuint32_t指针类型,x++表示指针运算,步长+1会增加数据类型的长度

  • uint32_t4字节,所以循环中的代码含义,每四字节记录一个++N的值

使用lldb验证

  1. //读取start
  2. (lldb) x 0x100bbd4c0
  3. 0x100bbd4c0: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ................
  4. 0x100bbd4d0: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 ................
  5. //读取stop
  6. (lldb) x 0x100bbd4f8-4
  7. 0x100bbd4f4: 0e 00 00 00 c0 d2 e0 00 01 00 00 00 00 00 00 00 ................
  8. 0x100bbd504: 00 00 00 00 66 73 bb 00 01 00 00 00 00 00 00 00 ....fs..........
  • 读取最后一个值,要在stop地址的基础上减去4字节

  • startstop,读出值为01~0e,这些值表示当前项目中方法/函数的符号个数

5.3 __sanitizer_cov_trace_pc_guard

__sanitizer_cov_trace_pc_guard函数中设置断点,运行项目

来到断点,查看函数调用栈
image.png

  • main函数调用

继续执行程序,又会进入该函数的断点
image.png

  • didFinishLaunchingWithOptions方法调用

我们会发现一个现象,项目中每一个方法和函数的调用,都会触发__sanitizer_cov_trace_pc_guard的断点,并且由当前执行的方法/函数调用

写入测试代码

  1. void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  2. NSLog(@"__sanitizer_cov_trace_pc_guard");
  3. }
  4. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  5. NSLog(@"touchesBegan方法执行");
  6. test();
  7. }
  8. void(^block)(void) = ^(void){
  9. NSLog(@"Block执行");
  10. };
  11. void test(){
  12. NSLog(@"test函数执行");
  13. block();
  14. }
  15. -------------------------
  16. //输出以下内容:
  17. __sanitizer_cov_trace_pc_guard
  18. touchesBegan方法执行
  19. __sanitizer_cov_trace_pc_guard
  20. test函数执行
  21. __sanitizer_cov_trace_pc_guard
  22. Block执行
  • 从运行结果来看,方法和函数全部被HOOK

  • 被拦截的方法和函数,仅限当前项目中的符号,例如:NSLog等外部符号不会被HOOK

  • 二进制重排的本意,就是将代码实现的二进制中方法/函数符号,在启动时刻按照顺序排列在前面。外部符号的方法/函数实现,并不在当前项目中,所以它们的符号也不在重排的范围之内

5.4 原理

查看汇编代码
image.png

  • 在每一个方法和函数的汇编代码中,都多了一句bl指令,调用的正是__sanitizer_cov_trace_pc_guard函数

  • Clang插庄的实现原理:只要添加Clang插庄的标记,编译器就会在当前项目中,在所有方法、函数、Block的代码实现的边缘,插入一句__sanitizer_cov_trace_pc_guard函数的调用代码,达到方法/函数/Block100%覆盖

  • 相当于编译器在编译时期,修改了当前的二进制文件

  • 修改时机,有可能是语法分析之后,生成IR中间代码时进行修改(未验证)

5.5 获取符号名称

示例代码中,使用了一个__builtin_return_address函数
image.png

  • 函数的作用,获取当前返回地址,也就是调用者的函数地址

得到调用者的函数地址,获取符号名称

  1. #include <dlfcn.h>
  2. void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  3. NSLog(@"__sanitizer_cov_trace_pc_guard");
  4. if (!*guard) return;
  5. void *PC = __builtin_return_address(0);
  6. Dl_info info;
  7. dladdr(PC, &info);
  8. NSLog(@"%s", info.dli_fname);
  9. NSLog(@"%p", info.dli_fbase);
  10. NSLog(@"%s", info.dli_sname);
  11. NSLog(@"%p", info.dli_saddr);
  12. }
  • 使用dladdr函数,将传入的函数地址,获取基本信息,存入Dl_info结构体

Dl_info结构体的定义:

  1. typedef struct dl_info {
  2. const char *dli_fname; /* Pathname of shared object */
  3. void *dli_fbase; /* Base address of shared object */
  4. const char *dli_sname; /* Name of nearest symbol */
  5. void *dli_saddr; /* Address of nearest symbol */
  6. } Dl_info;
  • dli_fname:当前MachO路径
  • dli_fbase:当前MachO基地址
  • dli_sname:函数名称
  • dli_saddr:函数地址

运行项目,测试打印结果

  1. __sanitizer_cov_trace_pc_guard
  2. dli_fname:/private/var/containers/Bundle/Application/E4DBCC4F-B132-4462-A148-03B398B476F5/SanitizerCoverage.app/SanitizerCoverage
  3. dli_fbase0x104cb0000
  4. dli_sname:-[ViewController touchesBegan:withEvent:]
  5. dli_saddr0x104cb5a64
  • 通过dli_sname可以得到函数名称

修改测试代码,运行项目:

  1. #import "ViewController.h"
  2. #include <stdint.h>
  3. #include <stdio.h>
  4. #include <sanitizer/coverage_interface.h>
  5. #include <dlfcn.h>
  6. @interface ViewController ()
  7. @end
  8. @implementation ViewController
  9. + (void)load {
  10. // NSLog(@"load函数");
  11. }
  12. - (void)viewDidLoad {
  13. [super viewDidLoad];
  14. test();
  15. }
  16. void(^block)(void) = ^(void){
  17. // NSLog(@"Block执行");
  18. };
  19. void test(){
  20. // NSLog(@"test函数执行");
  21. block();
  22. }
  23. void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
  24. static uint64_t N;
  25. if (start == stop || *start) return;
  26. for (uint32_t *x = start; x < stop; x++)
  27. *x = ++N;
  28. }
  29. void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  30. void *PC = __builtin_return_address(0);
  31. Dl_info info;
  32. dladdr(PC, &info);
  33. NSLog(@"%s", info.dli_sname);
  34. }
  35. @end
  36. -------------------------
  37. //输出以下内容:
  38. +[ViewController load]
  39. main
  40. -[AppDelegate application:didFinishLaunchingWithOptions:]
  41. -[SceneDelegate window]
  42. -[SceneDelegate setWindow:]
  43. -[SceneDelegate window]
  44. -[SceneDelegate window]
  45. -[SceneDelegate scene:willConnectToSession:options:]
  46. -[SceneDelegate window]
  47. -[SceneDelegate window]
  48. -[SceneDelegate window]
  49. -[ViewController viewDidLoad]
  50. test
  51. block_block_invoke
  52. -[SceneDelegate sceneWillEnterForeground:]
  53. -[SceneDelegate sceneDidBecomeActive:]
  • 获取到启动时刻,所有被调用的方法、函数、Block的函数名称。其中部分函数多次调用,出现了重复符号,还需要对其排重

5.6 实践

日常开发中,我们经常会使用多线程开发。如果函数处于子线程,那__sanitizer_cov_trace_pc_guard函数也会在子线程进行回调

所以,当我们通过回调收集函数名称时,也要保证线程安全

5.6.1 收集返回地址

以下案例,我们使用线程相对安全的原子队列进行返回地址的收集

  1. //定义原子队列
  2. static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
  3. //定义结构体
  4. typedef struct {
  5. void *pc;
  6. void *next;
  7. } SYNode;
  8. void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  9. void *PC = __builtin_return_address(0);
  10. //创建结构体
  11. SYNode *node = malloc(sizeof(SYNode));
  12. *node = (SYNode){PC, NULL};
  13. //结构体入栈
  14. //offsetof:参数1传入类型,将下一个节点的地址返回给参数2
  15. OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
  16. }
  17. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  18. while (YES) {
  19. SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
  20. //取空则停止循环
  21. if(node == NULL){
  22. break;
  23. }
  24. Dl_info info;
  25. dladdr(node->pc, &info);
  26. NSLog(@"%s", info.dli_sname);
  27. }
  28. }
  • 定义:

    • 定义原子队列

    • 定义结构体,pc存储当前返回地址,next存储下一个节点地址

  • 收集

    • 创建结构体,对pc赋值,next设置为NULL

    • 结构体入栈

    • offsetof:宏,参数1传入类型,将下一个节点的地址返回给参数2

  • 测试

    • 循环读取node,取空则停止循环

    • 将返回地址写入Dl_info结构体

    • 打印符号名称

5.6.2 循环引发的大坑

运行上述案例:
image.png

  • touchesBegan方法出现死递归

touchesBegan方法中设置断点,运行项目,查看汇编代码
image.png

  • 方法中被插入三次__sanitizer_cov_trace_pc_guard函数的调用

这就是循环引发的大坑,SanitizerCoverage不但拦截方法、函数、Block,还会对循环进行HOOK

案例中,while循环被HOOK,循环的执行会进入回调函数。回调函数中存入队列的还是touchesBegan的函数地址,这会导致队列中永远存在一个到两个touchesBegannext永远获取不完

解决办法:

Build SettingOther C Flags中,将配置修改为-fsanitize-coverage=func,trace-pc-guard,对其增加func参数
image.png

再次运行项目,点击屏幕,输出以下内容:

  1. -[ViewController touchesBegan:withEvent:]
  2. -[SceneDelegate sceneDidBecomeActive:]
  3. -[SceneDelegate sceneWillEnterForeground:]
  4. block_block_invoke
  5. test
  6. -[ViewController viewDidLoad]
  7. -[SceneDelegate window]
  8. -[SceneDelegate window]
  9. -[SceneDelegate window]
  10. -[SceneDelegate scene:willConnectToSession:options:]
  11. -[SceneDelegate window]
  12. -[SceneDelegate window]
  13. -[SceneDelegate setWindow:]
  14. -[SceneDelegate window]
  15. -[AppDelegate application:didFinishLaunchingWithOptions:]
  16. main
  17. +[ViewController load]
  • 修改配置项,仅拦截方法的调用,成功解决循环引发的大坑

5.6.3 获取函数符号并排重

案例还要解决几个问题:

  • 过滤掉自身touchesBegan的函数名称

  • 函数和Block的符号,需要在函数名称之前增加_

  • 相同的函数符号,需要进行排重

  • 队列原则,先进后出。所以我们需要的符号顺序需要反转

修改touchesBegan方法,解决遗留问题:

  1. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  2. NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
  3. while (YES) {
  4. SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
  5. if(node == NULL){
  6. break;
  7. }
  8. Dl_info info;
  9. dladdr(node->pc, &info);
  10. NSString *name = @(info.dli_sname);
  11. if([name isEqualToString:@(__func__)]){
  12. continue;
  13. }
  14. if(![name hasPrefix:@"+["] && ![name hasPrefix:@"-["]){
  15. name = [@"_" stringByAppendingString:name];
  16. }
  17. if([symbolNames containsObject:name]){
  18. continue;
  19. }
  20. [symbolNames addObject:name];
  21. }
  22. symbolNames = (NSMutableArray<NSString *> *)[[symbolNames reverseObjectEnumerator] allObjects];
  23. for (NSString *symbol in symbolNames) {
  24. NSLog(@"%@", symbol);
  25. }
  26. }
  27. -------------------------
  28. //输出以下内容:
  29. +[ViewController load]
  30. _main
  31. -[AppDelegate application:didFinishLaunchingWithOptions:]
  32. -[SceneDelegate setWindow:]
  33. -[SceneDelegate scene:willConnectToSession:options:]
  34. -[SceneDelegate window]
  35. -[ViewController viewDidLoad]
  36. _test
  37. _block_block_invoke
  38. -[SceneDelegate sceneWillEnterForeground:]
  39. -[SceneDelegate sceneDidBecomeActive:]
  • 过滤掉自身touchesBegan的函数名称

  • 获取符号名称,如果不是+[-[开头,视为函数或Block,前面加_

  • 如果符合名称在数组中存在,跳过。否则,添加到数组

  • 将数组反转,并循环打印

5.6.4 写入文件并配置

修改touchesBegan方法,将符号列表写入.order文件

  1. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  2. NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
  3. while (YES) {
  4. SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
  5. if(node == NULL){
  6. break;
  7. }
  8. Dl_info info;
  9. dladdr(node->pc, &info);
  10. NSString *name = @(info.dli_sname);
  11. if([name isEqualToString:@(__func__)]){
  12. continue;
  13. }
  14. if(![name hasPrefix:@"+["] && ![name hasPrefix:@"-["]){
  15. name = [@"_" stringByAppendingString:name];
  16. }
  17. if([symbolNames containsObject:name]){
  18. continue;
  19. }
  20. [symbolNames addObject:name];
  21. }
  22. symbolNames = (NSMutableArray<NSString *> *)[[symbolNames reverseObjectEnumerator] allObjects];
  23. NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hk.order"];
  24. NSString *symbolStr = [symbolNames componentsJoinedByString:@"\n"];
  25. NSData *symbolData = [symbolStr dataUsingEncoding:kCFStringEncodingUTF8];
  26. [[NSFileManager defaultManager] createFileAtPath:filePath contents:symbolData attributes:nil];
  27. NSLog(@"%@", symbolStr);
  28. }

拿到.order文件,选择Add Additional Simulators...
image.png

选中案例App,点击Downlad Container...
image.png

选择路径,下载.xcappdata文件。右键显示包内容,在AppData/tmp目录下,找到.order文件
image.png

.order文件拷贝到工程根目录,在Build SettingOrder File进行配置
image.png

Build SettingsWrite Link Map File,设置为YES
image.png

编译项目,打开LinkMap文件
image.png

  • 配置生效,二进制重排成功

5.6.5 swift的函数符号

Other C Flags中的配置,仅对Clang编译器生效。而Swift使用swiftc编译器,要想获得swift函数符号,需要对Other Swift Flags进行配置
image.png

  • Clang的配置参数略有出入
  • 添加-sanitize-coverage=func-sanitize=undefined两项

创建SwiftTest.swift文件,写入测试代码:

  1. import Foundation
  2. class SwiftTest: NSObject {
  3. @objc class func swiftTest1(){
  4. }
  5. @objc class func swiftTest2(){
  6. }
  7. }

ViewControllerload方法和Block中分别调用

  1. + (void)load {
  2. [SwiftTest swiftTest1];
  3. }
  4. - (void)viewDidLoad {
  5. [super viewDidLoad];
  6. test();
  7. }
  8. void(^block)(void) = ^(void){
  9. [SwiftTest swiftTest2];
  10. };
  11. void test(){
  12. block();
  13. }

运行项目,点击屏幕,输出以下内容:

  1. +[ViewController load]
  2. _$s17SanitizerCoverage9SwiftTestC10swiftTest1yyFZTo
  3. _$s17SanitizerCoverage9SwiftTestC10swiftTest1yyFZ
  4. _main
  5. -[AppDelegate application:didFinishLaunchingWithOptions:]
  6. -[SceneDelegate setWindow:]
  7. -[SceneDelegate scene:willConnectToSession:options:]
  8. -[SceneDelegate window]
  9. -[ViewController viewDidLoad]
  10. _test
  11. _block_block_invoke
  12. _$s17SanitizerCoverage9SwiftTestC10swiftTest2yyFZTo
  13. _$s17SanitizerCoverage9SwiftTestC10swiftTest2yyFZ
  14. -[SceneDelegate sceneWillEnterForeground:]
  15. -[SceneDelegate sceneDidBecomeActive:]
  • 使用OCSwift混编,成功得到Swift函数符号

总结

Main函数之前的性能检测:

  • dylib loading time:动态库的载入耗时

  • rebase/binding time:重定位符号和符号绑定的耗时

    • rebase:系统采用ASLR技术,保证地址空间随机化。所以在运行时,需要通过rebase进行重定位符号,使用ASLR+偏移地址

    • binding:使用外部符号,编译时无法找到函数地址。所以在运行时,dyld加载共享缓存,加载链接动态库之后,进行binding操作,重新绑定外部符号

  • ObjC setup time:注册OC类的耗时

  • initializer time:执行load以及C++构造函数的耗时

  • slowest intializers:列举出几个比较耗时的动态库

虚拟内存:

  • 概述

    • 程序以懒加载的方式加载到内存中,按需加载,避免内存浪费

    • 将程序和物理内存完全阻隔开,无法跨进程访问,数据更安全

  • 缺页中断

    • 当程序访问未被缓存的内存页时,就会触发缺页中断

    • 缺页中断会将当前进程阻塞掉,此时需要先将数据载入到物理内存,然后再寻址,进行读取

  • 页面置换:

    • 物理内存的空间是有限的,当内存中没有空间时,操作系统会从选择合适的物理内存页驱逐回磁盘,为新的内存页让出位置,选择待驱逐页的过程在操作系统中叫做页面置换
  • 冷启动 & 热启动

    • 冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动

    • 热启动:当启动应用时,后台已有该应用的进程(例:按home键回到桌面,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动

ASLR

  • 一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的

二进制重排:

  • 将启动时需要的方法/函数排列在最前面,降低缺页中断的可能性,从而提升应用的启动速度

  • 方法/函数的编译链接顺序,将Write Link Map File设置为YES,通过LinkMap文件查看

  • 启用二进制重排,在Order File中配置.order文件

Clang插庄 :

  • 配置

    • OC:对Other C Flags进行配置,增加-fsanitize-coverage=trace-pc-guard

    • Swift:对Other Swift Flags进行配置,增加-sanitize-coverage=func-sanitize=undefined

  • API函数:

    • __sanitizer_cov_trace_pc_guard_init:获取当前项目中方法/函数的符号个数

    • __sanitizer_cov_trace_pc_guard:拦截方法/函数/Block之后的回调函数

      • __builtin_return_address:获取当前返回地址,也就是调用者的函数地址
  • 实现原理

    • 只要添加Clang插庄的标记,编译器就会在当前项目中,在所有方法、函数、Block的代码实现的边缘,插入一句__sanitizer_cov_trace_pc_guard函数的调用代码,达到方法/函数/Block100%覆盖

    • 相当于编译器在编译时期,修改了当前的二进制文件

    • 
修改时机,有可能是语法分析之后,生成IR中间代码时进行修改(未验证)

  • 循环引发的大坑

    • SanitizerCoverage不但拦截方法、函数、Block,还会对循环进行HOOK

    • Other C Flags中,增加func参数,即可解决

    • 拦截方法/函数/Block之后的回调函数,也存在多线程调用的情况。所以收集函数名称时,也要保证线程安全