前言

一、冷启动和热启动

二、启动耗时分析

三、引出二进制重排

四、自定义order文件

五、二进制重排方案

六、Clang插庄分析

七、记录所有启动符号

八、二进制重排实战


掌握虚拟内存和物理内存的原理之后,继续分析IOS在启动过程中如何优化启动速度。


一、冷启动和热启动

App启动分为冷启动和热启动

1、冷启动

冷启动是指在内存中不包含相关数据,必须从磁盘中载入到内存,这个过程是冷启动。
当把app进程杀掉,重新打开app时,不一定进入冷启动的状态,此时内存中可能还有该应用程序的相关数据。
以下情况,是冷启动:

  • (1)当内存不足,APP被系统kill后,再启动。
  • (2)重启设备之后,第一次打开App的过程。

2、热启动

热启动是指打开app之前,数据仍然在内存中,打开app时,可以从内存中直接读取数据。

注意:冷启动和热启动是系统决定的。


二、启动耗时分析

分析启动耗时,可以以main函数为节点,分为2个阶段:

  • 程序启动开始到main函数结束 - main函数之前的阶段(靠系统反馈)
  • main函数开始到首屏渲染完成结束 - main函数之后的阶段(自行统计进入 main 函数到第一个界面显示的耗时)

1、main函数之后的优化

先简单的说下main函数之后的优化,大概总结下面4点:

  • 对一些用到时才需要加载的数据,进行懒加载,比如通过业务判断可能加载也可能不加载的数据。
  • 子线程异步处理耗时操作
  • 启动的相关页面,除了LaunchScreen.storyboard,尽量都用纯代码编写,因为xib和storyborad会有一次转化的过程。
  • 多余的业务代码,尽量删除,因为多余的代码也会编译进去,从而会耗时。

2、main函数之前的分析(pre-main)

之前探索过dyld的源码,也分析过dyld的动态链接过程,先回顾以下大致流程:

1、配置环境变量
2、加载共享缓存
3、实例化主程序
4、加载插入的动态库
5、链接主程序
6、链接动态库
7、weakBind弱引用绑定主程序
8、初始化主程序
9、通知所有监控启动进程的进程,将要进入main函数。

查看最后几步的代码可以找到一个环境变量:DYLD_PRINT_STATISTICS

    // dump info if requested
	if ( sEnv.DYLD_PRINT_STATISTICS )
		ImageLoader::printStatistics((unsigned int)allImagesCount()
        , initializerTimes[0]);
	if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
		ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount()
        , initializerTimes[0]);

DYLD_PRINT_STATISTICS 可以打印出main函数之前的耗时时间。

通过以下路径配置环境变量:

Xcode -> Edit Scheme -> Run -> Arguments -> Environment Variables
增加 DYLD_PRINT_STATISTICS :1

3、通过环境变量运行分析

运行之前一个项目,加入环境变量,运行(冷启动的数据):

Total pre-main time: 739.85 milliseconds (100.0%)
         dylib loading time: 135.79 milliseconds (18.3%)
        rebase/binding time: 168.59 milliseconds (22.7%)
            ObjC setup time:  82.63 milliseconds (11.1%)
           initializer time: 352.82 milliseconds (47.6%)
           slowest intializers :
             libSystem.B.dylib :   4.88 milliseconds (0.6%)
    libMainThreadChecker.dylib :  24.88 milliseconds (3.3%)
                  xxxChatClient(自己的动态库) : 223.48 milliseconds (30.2%)
                      xxxx(主程序) : 309.33 milliseconds (41.8%)
  • Total pre-main time: 总时长
  • Dylib loading time: 加载可执行文件(App 的.o 文件的集合)以及 加载动态链接库的时间
  • rebase/binding time: 对动态链接库进行 rebase 指针调整和 bind 符号绑定;
  • ObjC setup time: Objc 运行时的初始化处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等;
  • initializer timer:初始化,包括了执行 +load() 方法、attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量。
  • slowest initializer: 举出几个最慢的
    libSystem.B.dylib 和 libMainThreadChecker.dylib 是系统库
    xxxChatClient 是自定的动态库(官方建议自定义的动态库,最好是6个,所以合并动态库对于启动优化是很有效果的)
    xxxx 是主程序

4、rebase(偏移修正)解释:

  • app编译成二进制文件后,在其内部,每一个方法、函数都有一个地址,这个地址是相对于二进制文件的偏移地址。
  • 在启动时,为了安全,系统会引入一个安全机制,叫做ASLR(Address space layout randomization)。会在整个二进制文件中的最前面,随机加一个偏移值。
  • ASLR的出现就是为了解决,虚拟内存地址不变,带来的安全问题
比如一个方法 funcA,它在二进制文件中的偏移地址是 0x0001。
ASRL的随机值是 0x1000。
当运行到内存的时候,会通过ASLR进行修正。
那么在运行时,funcA的真实内存地址就是:
偏移地址+ASLR = 真正的内存地址,
也就是 0x0001 + 0x1000 = 0x1001。

再举个例子,通过bulgy报错堆栈结合DSYM工具来分析具体错误代码:
当我们使用DSYMTools工具时,会让我们输入以下内容:

  • UUID - 我们将.dSYM文件拖入之后,会自动读取
  • 默认 Silde Address 如下图所示,其实就是ASRL的随机值:
  • 错误信息内存地址:就是默认Silde Address + 偏移地址。
    可以做一个计算:将默认 Silde Address转成十进制,然后加上偏移地址,就是错误的内存地址
    原始数据如下:

    图上运行时的内存地址就是要填写的错误信息内存地址。

小结:偏移修正指的就是计算方法在虚拟内存中的地址的过程。


5、binding(符号绑定)解释:

动态库的方法绑定,是指将方法名字与方法的实现进行绑定的过程。

比如NSLog函数,是Fundation框架的,app是不知道它的地址的。
在MachO中,会创建一个符号NSLog。
在运行时,会将NSLog的真实地址,和NSlog的符号进行关联(是在内存中进行绑定)
这个关联的过程就是符号绑定。(dyld做的)

6、补充

升级Xcode13 iOS15时,DYLD_PRINT_STATISTICS环境变量就失效了,控制台打印就没了。
原因是它只适配iOS10 - iOS14,在IOS15用了新版本的dyld。
如需要查看可以使用相关系统内的手机、模拟机。(测试环境:Xcode15 模拟器iOS13.0 / iOS14.0.1 / iOS14.5。
同样也可以借助instrument工具中的App Launch来进行分析。

三、引出二进制重排

1、pre-main阶段的优化分析:

先说一组数据:大概每增加2万个OC类,启动会耗时800ms。
针对pre-main阶段的优化,大概总结为以下3点:

  • 减少动态库加载:每个库本身都有依赖关系,苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司建议最多使用 6 个非系统动态库。
  • 减少加载启动后不会去使用的类或者方法
  • +load()方法里的内容可以放到首屏渲染完成后再执行,或使用 +initialize()方法替换掉。在一个 +load() 方法里,进行运行时方法替换操作会带来 4 毫秒的消耗。

完成以上3点的优化之后,如果还想继续优化,就要回顾一下前面总结的虚拟内存和物理内存的原理了,其中一个核心问题就是pagefalut的次数。

2、 查看pagefalut的次数

通过instruments工具,选择System Trace

运行之前项目:

可以看到,File Back page In 对应的Count就是Page Falut的次数。
另外通过App Store渠道分发的App,Page Fault还会进行签名验证,所以一次Page Fault的耗时比想象的要多。

3、二进制重排:

二进制文件默认排列顺序规则是: 首先按照文件的编译顺序,然后根据每个文件的书写顺序来排列。
这就造成了app在启动的时候,启动相关的方法、函数被排列到了不同的文件里面,也就分配到了不同的页中(启动的方法并没有集中在一起)。
比如下面的情况:

IOS的一页大小是16k。
但是有可能一页16k数据里面只有一个函数(极端情况)。
也可能1000页里面,总共就有16k 大小的数据。
16k这么多的函数,全部被随机分配到1000页中了。

试想如果把启动函数和方法尽可能集中在一起,那么就可以减少Page Falut的次数,也就加快了启动时间。这种优化就是LLVM 为我们提供的优化方式:二进制重排。

小结:二进制重排的原理就是:将所有启动时刻需要调用的方法,排列在一起!


四、自定义order文件

1、LinkMap

LinkMap 是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File。

LinkMap 主要包括三大部分:

  • Object Files 生成二进制用到的link单元的路径和文件编号
  • Sections 记录Mach-O每个Segment/section的地址范围
  • Symbols 按顺序记录每个符号的地址范围


2、order文件

首先来一个测试demo:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

void test1() {
    printf("1\n");
}

void test2() {
    printf("2\n");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    test1();
}

+ (void)load {
    printf("load\n");
    test2();
}

@end

以上几个方法或函数的调用顺序是 load -> test2 -> viewDidLoad -> test1
但是编译后的排列顺序由默认规则可知,是根据文件内的书写顺序,应该为 test1 -> test2 -> viewDidLoad ->load
编译一下项目,找到对应的linkmap文件:PreMainTest-LinkMap-normal-x86_64.txt

打开文件如下:

可以看到排列顺序确实按照默认规则来的。
符号前面的地址,比如_test1对应的0x100001E50,也可以通过MachoView来查看,将可执行文件拖入:

以上红框部分就是_test1的实现。

注意: macho中,__TEXT 都是代码 ,__DATA 都是数据


3、自定义order文件

可以通过自定义order文件,来改变方法、函数的排列顺序。
之前的objc源码也有order文件(libobjc.order),818版本之后,貌似被删掉了。
在测试工程根目录创建一个mg.order文件,内容如下:

_main
-[ViewController viewDidLoad]
-[mg hello]
_test1
_test2

然后在Build setting->Order File设置此文件路径

clean一下项目,然后运行,最后打开linkmap文件

可以看到,顺序是按照order文件的排列顺序。
其中注意几个点:

  • 如果遇到没有的符号,会自动忽略
    如果在 Other Linker Flags: Debug 中添加-order_file_statistics,会以 warning 的形式把这些没找到的符号打印在日志里。
  • 没有写的符号,还是按照原来的顺序排列
  • nm -p xxx 也能查看符号,但是顺便不完全
  • 其原理就是,Ld(静态链接),会根据order文件进行重排。
    Xcode 使用的链接器件是ld,ld有一个不常用的参数 -order_file,通过man ld可以查看详细文档。

五、二进制重排方案

要实现二进制重排,就要获取启动时刻所有的调用函数或者方法,然后按照调用顺序写入order文件中。
那么问题来了,我们可以自定义一个方法作为启动结束,比如第一个页面的ViewDidLoad可以作为启动结束的方法,但是之前的方法如何获取呢?

1、使用Fishhook (fishhook原理后面具体总结)

FishHook可以HOOK系统函数,但是不能HOOK自定义的函数。
那么可以通过HOOK“objc_msgSend”吗?这里有2个坑点:

  • objc_msgSend 函数式可变参数!HOOK部分的代码需要写汇编
  • block、swift、自定义的c函数,无法通过fishHook来hook,虽然swift和自定义的c函数可以通过inlineHOOK来做,但是前提是需要原始函数的地址,所以无法批量hook。

2、使用Clang插庄来%100符号覆盖


六、Clang插庄分析

下面就使用Clang插庄的方法来进行二进制重排。

1、Clang编译选项设置

根据Clang官方文档: https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs,找到Tracing PCs with guards:
With -fsanitize-coverage=trace-pc-guard the compiler will insert the following code on every edge。
添加编译选项:-fsanitize-coverage=trace-pc-guard添加到Other C Flags中

此时编译器会插入以下2个函数:

__sanitizer_cov_trace_pc_guard(&guard_variable)
// The guards are [start, stop).
// This function will be called at least once per DSO and may be called
// more than once with the same values of start/stop.
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);

2、解决报错

设置完Clang的Flags之后,直接运行项目(之前的demo工程),此时会有2个报错:

报错找不到以下2个符号

___sanitizer_cov_trace_pc_guard
___sanitizer_cov_trace_pc_guard_init

这说明添加了 -fsanitize-coverage=trace-pc-guard 这个flag,就会去调用以上2个函数。
此时需要自己去实现这2个函数,直接复制官网的demo代码:

// trace-pc-guard-cb.cc
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

// This callback is inserted by the compiler as a module constructor
// into every DSO. 'start' and 'stop' correspond to the
// beginning and end of the section with the guards for the entire
// binary (executable or DSO). The callback will be called at least
// once per DSO and may be called multiple times with the same parameters.
extern "C" 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.
}

// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
//    if(*guard)
//      __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
//    __sanitizer_cov_trace_pc_guard(guard);
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
  // If you set *guard to 0 this code will not be called again for this edge.
  // Now you can get the PC and do whatever you want:
  //   store it somewhere or symbolize it and print right away.
  // The values of `*guard` are as you set them in
  // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
  // and use them to dereference an array or a bit vector.
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  // This function is a part of the sanitizer run-time.
  // To use it, link with AddressSanitizer or other sanitizer.
  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

整理如下:

#import "ViewController.h"
@interface ViewController ()

@end

@implementation ViewController

void test1() {
    printf("1\n");
}

void test2() {
    printf("2\n");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    test1();
}

+ (void)load {
    printf("load\n");
    test2();
}

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) {
    if (!*guard) return;
    void *PC = __builtin_return_address(0);     
    char PcDescr[1024];
    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
@end

再次运行,没有报错了。

3、__sanitizer_cov_trace_pc_guard_init 函数分析

__sanitizer_cov_trace_pc_guard_init函数是一个初始化的函数,通过注释:Initialize only once 和 if (start == stop || *start) return;判断条件可以知道:printf("INIT: %p %p\n", start, stop);只会走一次。
那么__sanitizer_cov_trace_pc_guard_init函数是否可以通过遍历start到stop的过程,来取到符号呢。通过打印分析:

INIT: 0x10fca4a34 0x10fca4abc
0x10fca4a34 是start的地址,0x10fca4abc是stop的地址

回顾一下lldb指令:

x就是memory read内存读取并打印的作用,x: 每一段以16进制打印
iOS内存为小端模式(低地址指向低地址,高地址指向高地址),x与x/4gx打印结果 每一段刚好相反

用x 0x10fca4a34 和 x 0x10fca4abc 分别打印:

(lldb) x 0x10fca4a34
0x10fca4a34: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x10fca4a44: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00  ................
(lldb) x 0x10fca4abc
0x10fca4abc: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x10fca4acc: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
(lldb) 

由于stop指向的是结束的位置,而stop是一个uint32_t *的指针,数据是4节对齐。所以结束的数据内容,要想读出来,应该通过stop的内存地址向左偏移4个字节(减去4个字节之后的地址才指向结束内容的首地址)。
x 0x10fca4abc-4打印出来内容如下:

(lldb) x 0x10fca4abc-4
0x10fca4ab8: 22 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  "...............
0x10fca4ac8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

内容是22,这个22是什么意思呢,经过测试发现,每增加一个方法、函数或者block。内容就会加1。
这个数量其实就是__sanitizer_cov_trace_pc_guard插庄的数量。虽然数量知道了,但是我们的目的是要拿到符号,显然这个函数没办法帮我们完成需求,最多做一个数量的统计,所以继续分析下一个函数。

4、__sanitizer_cov_trace_pc_guard 函数分析

在这个函数下一个断点,通过堆栈可以发现:参数guard相当于哨兵,它既不是上一个函数的调用地址,也不是当前函数的返回地址,所以无法从这里下手。但是可以猜测,这个函数是插入在了每一个函数和方法调用的时候。
开始验证:增一个点击方法,然后点击之后再调用test1函数

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    test1();
}

通过测试发现,点击屏幕之后,__sanitizer_cov_trace_pc_guard函数会走2次。堆栈如下:
第一次:

第二次:

分别在执行touchesBegan和test1的时候调用了__sanitizer_cov_trace_pc_guard。
继续在test1函数调用处下个断点,看汇编:

//汇编解释:
sp是栈寄存器,它时刻指向栈顶。
看到sp指令,代表每个函数或者方法调用之前,必须要处理环境的问题(开辟栈空间、释放栈空间)。
比如内存,参数等先做一次处理,处理完成之后才会调用源代码。
bl,就是跳转一个函数, 可以看到会跳转到:__sanitizer_cov_trace_pc_guard

test1函数中确实调用了__sanitizer_cov_trace_pc_guard。也就证明:

只要在Other C Flags 添加 -fsanitize-coverage=trace-pc-guard 标记,Clang会先收到。

Clang读完代码,最终会生成中间代码IR,在生成代码的时候,会在每一个函数的边缘插入一行__sanitizer_cov_trace_pc_guard代码。这样每一个函数方法调用的时候,都会调用__sanitizer_cov_trace_pc_guard。相当于做到了全局的AOP。

继续探索:

试想lldb可以查看堆栈,那应该有相应的api去获取函数的调用地址,所以在__sanitizer_cov_trace_pc_guard函数中是否可以获取调用者的地址。而__builtin_return_address是调用者的地址吗?

void *PC = __builtin_return_address(0); 

将断点来到main函数调用的时候,看下堆栈:

这个0x00000104cfd864 是main函数的调用地址吗?其实不然,继续看下汇编:

main函数的调用地址是0x1049f1844,而0x00000104cfd864是__sanitizer_cov_trace_pc_guard的返回地址。

小结一下:

  • PC的地址是返回地址,并不是上一个函数的调用地址
  • 当前函数返回到上一个调用的地址,就是当前__sanitizer_cov_trace_pc_guard调用完毕会回到哪个地方去。
  • __builtin_return_address 标记返回地址 (其中参数为0是指当前函数的返回地址,1是当前函数调用者的返回地址)
  • 在函数调用栈中,每一个函数的在bt堆栈中的返回地址是不一样的。也可以通过偏移值<+xxx> 获取到函数的起始位置。

    但是这种方案显然比较麻烦。

5、获取符号

可以通过一个api来获取函数相关信息:

typedef struct dl_info {
     const char      *dli_fname; 文件路径(哪个macho下)
     void            *dli_fbase; base (dli_fname的内存地址)
     const char      *dli_sname; 符号名称
     void            *dli_saddr; 函数地址(起始位置)
} Dl_info;

导入头文件 #include <dlfcn.h>

Dl_info info;
dladdr(PC, &info);
printf("fname:%s\nfbase:%p\n,dli_sname:%s\n,dli_saddr:%p\n\n",info.dli_fname,
           info.dli_fbase,info.dli_sname,info.dli_saddr);

运行可以看到,打印出了函数相关的信息。

七、记录所有启动符号

经过以上分析,方法和函数的符号可以获取到了,那么下面就开始编写代码,获取所有启动调用的方法了(以第一个页面渲染完作为启动结束)。

1、创建原子性队列,通过链表的方式,将SYNode(包含PC的结构体),加入队列

因为 __sanitizer_cov_trace_pc_guard函数是在子线程中调用的,所以需要用原子性队列,解决线程安全问题。

#include <libkern/OSAtomic.h> //原子性队列
//定义原子队列 (链表的结构)
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct {
    void *pc;
    void *next;
} SYNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    //if (!*guard) return; //为了避开load等前期的加载方法
    void *PC = __builtin_return_address(0); //0是指当前函数的返回地址,1是当前函数调用者的
    //创建结构体!    
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    //加入结构体!
    //offsetof 就是把结构体的下一个位置,直接指向结构体的next
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}


2、在touchBegin中去获取解析队列数据

//增加一个点击方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {  
     //定义数组
    NSMutableArray<NSString *>*symbolNames = [NSMutableArray array];
    
    while (true) { //一次循环,也会被HOOK一次
        SYNode *node =  OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
        if (node == NULL) {
            break;
        }
        Dl_info info;
        dladdr(node->pc, &info);
        NSString *name = @(info.dli_sname);
        //去重复!
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        if (!isObjc) {
            name = [NSString stringWithFormat:@"_%@",name];
        }
        if (![symbolNames containsObject:name]) {
            [symbolNames addObject:name];
        }
        
        free(node);
    }
   
    //反向数组
    symbolNames = (NSMutableArray *)[[symbolNames reverseObjectEnumerator] allObjects];
    [symbolNames removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    //写入orderfile文件
    //数组转成字符串
    NSString *funcStr = [symbolNames componentsJoinedByString:@"\n"];
    NSLog(@"%@",funcStr);
    //字符串写入文件
    //文件路径
    NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"mg2.order"];
    NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}

3、解决坑点

以上是完整的代码,但是有一个致命的坑点,会发现touchesBegan在无限递归了。
其原因是,此时hook的是每一次跳转,只要有b和bl的地方,都会被hook。
通过汇编分析,While 循环一次,会有一个b指令。
所以就会添加一次,添加的还是当前方法的名称。

解决方法就是在编译选项那里增加一个参数:func

-fsanitize-coverage=func,trace-pc-guard

执行完代码,点击一下屏幕(注意:如果点击2下屏幕,数据就没了,因为队列读取完就释放了,第二次点击的时候,队列为空的)
Command shift + 2,将手机沙盒文件取出,可以查看到mg2.file。

4、Swift如何插庄

很简单,在Other Swift Flags中添加:

-sanitize=undefined -sanitize-coverage=func

八、二进制重排实战

1、配置主程序的编译选项

直接在xcconfig文件中加入:

OTHER_CFLAGS = $(inherited) -fsanitize-coverage=func,trace-pc-guard
OTHER_SWIFT_FLAGS = $(inherited) -sanitize=undefined -sanitize-coverage=func

2、配置pod依赖库的编译选项

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      if config.name == 'Debug'
          config.build_settings['VALID_ARCHS'] = ['arm64 x86_64']
          config.build_settings['OTHER_CFLAGS'] ||= '$(inherited)'
          config.build_settings['OTHER_CFLAGS'] << ' '
          config.build_settings['OTHER_CFLAGS'] << '-fsanitize-coverage=func,trace-pc-guard'
          
          config.build_settings['OTHER_SWIFT_FLAGS`'] ||= '$(inherited)'
          config.build_settings['OTHER_SWIFT_FLAGS`'] << ' '
          config.build_settings['OTHER_SWIFT_FLAGS`'] << '-sanitize=undefined -sanitize-coverage=func'
      end
      if config.name == 'Release'
          config.build_settings['VALID_ARCHS'] = ['arm64']
      end
      
      if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"
        target.build_configurations.each do |config|
            config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
        end
      end
    end
  end
end

3、根据分析的代码记录符号,然后读取写入文件,就不在叙述了。


4、以第一个启动页面的- (void)viewDidAppear:(BOOL)animated方法作为启动结束

最后再次检测一下Page Fault的次数,经过多次测试,从原来的825-845次,降低到了800次左右