IOS优化:启动时间

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、检测启动时间
  • 二、为什么需要二进制重排
  • 三、二进制重排原理
  • 四、调试 Page Fault
  • 五、order文件
  • 六、静态插桩代码覆盖工具的机制和原理
  • 七、写入order文件
  • 八、验证插桩前后差距
  • 九、全文总结
  • 十、摒弃解释,直接使用

一、检测启动时间

冷启动:指APP被后台kill后重新启动APP,这种启动方式叫做冷启动。
热启动:APP的状态由running切换为suspend,APP 没有被kill仍然在后台运行。再次把APP切换到前台,这种启动方式叫热启动。

启动时间的划分可以把main()函数作为关键点分割成两块。t1阶段,main()之前的处理所需时间,称为pre-main。t2阶段,main()main()之后处理所需时间。t2阶段耗时的主要是业务代码推荐 BLStopwatch,这个工具可以打点统计业务耗时 本部分优化根据各自业务需求自行处理。

通过添加环境变量可以获取到pre-main阶段的时间。Xcode 中提供了测量 pre-main的时间 检测。Edit scheme -> Run -> Auguments 添加环境变量 DYLD_PRINT_STATISTICSvalue设为YES。

启动以后可以看到启动时长:

加载dylib

分析每个dylib(大部分是系统的),找到其Mach-O文件,打开并读取验证有效性;找到代码签名注册到内核,最后对dylib的每个segment调用mmap()

rebase/bind

dylib加载完成之后,它们处于相互独立的状态,需要绑定起来。Rebase将镜像读入内存,修正镜像内部的指针,性能消耗主要在IO。Bind是查询符号表,设置指向镜像外部的指针,性能消耗主要在CPU计算。

Objc setup

runtime会维护一张类名与类的方法列表的全局表。读取所有类,将类对象其注册到这个全局表中(class registration)。读取所有分类,把分类加载到类对象中(category registration)。检查selector的唯一性(selector uniquing)。

initalizer time

这部分其实就是load方法的耗时。

优化思路

移除不需要用到的动态库,尽量使用系统库,且苹果建议数量控制在 6 个以下
移除不需要用到的类;合并功能类似的类和扩展;经测试 20000 个类会增加约 800毫秒
尽量进行懒加载,尽量避免在load()方法里执行操作,把操作推迟到initialize()方法


二、为什么需要二进制重排

当我们向操作系统申请内存时,操作系统并不是直接分配给我们物理内存,而是只标记当前进程拥有该段内存,当真正使用这段内存时才会分配。这种延迟分配物理内存的方式就通过 page fault 机制来实现的。

当我们访问一个内存地址时,如果该地址非法,或者我们对其没有访问权限,或者该地址对应的物理内存还未分配, cpu 都会生成一个 page fault ,进而执行操作系统的 page fault handler。如果是因为还未分配物理内存,操作系统会立即分配物理内存给当前进程,然后重试产生这个page fault的内存访问指令。

程序加载时,不可能一下全部加载到内存,类似懒加载。这样就导致访问虚拟内存的某一页时,没有和真正的物理内存进行映射,导致page fault。每次page fault都会阻塞进程,耗费5ms左右的时间。

对用户而言,使用App时第一个直接体验就是启动 App 时间,而启动时期会有大量的类、分类、三方等等需要加载和执行,此时多个Page Fault所产生的的耗时往往是不能小觑的,下面我们就通过二进制重排来优化启动耗时。


三、二进制重排原理

程序启动时,会加载必要的page,为了减少page fault的数量,加快启动速度,可以把必要的代码尽量合并到一个page中。iOS是ld链接器,可以通过order文件进行二进制重排达到这个目的。

假设在启动时期我们需要调用两个函数 method1method4,函数编译在 mach-O 中的位置是根据 ld ( Xcode 的链接器) 的编译顺序并非调用顺序来的,因此很可能这两个函数分布在不同的内存页上。如下图,那么启动时,page1page2 都需要从无到有加载到物理内存中,从而触发两次Page Fault。二进制重排的做法就是将 method1method4 放到一个内存页中,那么启动时则只需要加载一次 page 即可,也就是只触发一次 Page Fault。在实际项目中,我们可以将启动时需要调用的函数放到一起 ( 比如 前10页中 ) 以尽可能减少 Page Fault,进而减少启动耗时。


四、调试 Page Fault

最好是卸载App,重新安装,调试第一次启动的效果。如果多次启动调试,你会发现count的波动范围很大。所以如果想获取准确的数据,最好重新安装App或者打开多个App之后,再来调试。这是因为内存管理机制,杀掉进程时,他所占用的物理内存空间,如果没有被覆盖使用,那么这部分内存有很大可能一直存在。重新打开,内存就不需要全部初始化。所以 冷热启动的界定不能以是否后台杀死来简单判断。

首先Xcode跑对应项目到手机上,然后每次杀后台,重启,打开Xcode选中 Open Developer ToolInstruments面板,选择System Trace,选择真机设备,点击运行按钮,等待首页出现点击⏹停止,等待分析完成。删除过滤条件,直接输入main,找到项目的target箭头展开,点击Main thread,根据图中选择Virtual Memory 查看 File Backed Page In次数。

  1. 打开Instruments,选择System Trace
  1. 选择真机,选择工程,选择启动,当页面加载出来的时候,停止。

这里面File Backed Page In就是page fault的次数。当我们把APP杀死后里面再启动,结果发现File Backed Page In这个值变得很小,说明APP就算杀死后,在启动不是冷启动,还是有一部数据在系统的缓存中。如何才是真正的冷启动呢,我们可以把APP杀掉后启动多个手机里面的APP,然后再启动APP,发现File Backed Page In又变得很大。二进制重排是在链接阶段生成的,重排之后生成可执行文件,所以我们只能在编译阶段来优化,而无法对已生成的ipa进行优化。


五、order文件

前面说了这么多,那么具体该怎么操作呢?苹果其实已经给我们提供了这个机制。实际上 二进制重排就是对即将生成的可执行文件重新排列,即它发生在链接阶段。首先,Xcode 用的链接器叫做 ldld 有一个参数叫 Order File,我们可以通过这个参数配置一个后缀名为order的文件路径。在这个 order 文件中,将你需要的符号按顺序写在里面。当工程 build 的时候,Xcode 会读取这个文件,打的二进制包就会按照这个文件中的符号顺序进行生成对应的 mach-O

我们可以在XCode配置二进制重排,首先我们要确定符号的顺序,才能知道怎么重排,XCode使用的链接器叫做ldld有个参数叫order_file,我们可以将文件的路径告诉XCode,在order_file文件中把符号的顺序写进去,XCode编译的时候就会按照文件中的符号顺序打包成二进制可执行文件。

我们可以在苹果的objc4-750源码中找到这种文件。

可以参考一下libObjc 项目,它已经使用了二进制重排进行优化。这些都是ios应用启动加载过程中熟悉的方法。order 文件里符号写错了或不存在会不会有问题?ld 会忽略这些符号,如果提供了link 选项-order_file_statistics,他们会以warning的形式把这些没找到的符号打印在日志里。会不会影响上架?不会,order文件只是重新排列了所生成的mach-O(可执行文件) 中函数表与符号表的顺序。打开后是下面这种格式:

里面全是函数符号,我们打开项目,在build setting 里面搜索order file,发现这里面指定了order的文件路径,因为一旦在这里指定了order file的路径,XCode就会在编译的时候按照文件里面写进去的顺序。

我们现在写一个Demo,AppDelegate添加如下方法。

+ (void)test111 {
    NSLog(@"test111");
}

+ (void)test222 {
    NSLog(@"test222");
}

+ (void)test333 {
    NSLog(@"test333");
}

然后编译,如何查看整个项目的符号顺序呢,我们到Build Settings搜索Link MapLink Map就是我们链接的符号表,我们把它改成YES,这样编译的时候就会把链接的符号表给我们写出来。

command + R我们运行下,然后在Products里面的.app文件,在我们Intermediates.noindex-->项目名.build--->Debug-iphoneos-->项目名.build--->项目名-LinkMap-normal-x86_64.txt,这个文件里面就有链接的符号顺序表。

我们在项目中用touch创建test.order文件,修改方法顺序。

然后在Build setting里面搜下order file在后面将该文件地址添加进去。

这样Xcode在编译时候就会按照order文件中的符号顺序链接代码了,我们编译一下,再看一下LinkMap-normal-x86_64.txt文件。

我们发现是按照order的符号顺序来的,而且如果order里面写了项目中不存在的方法符号,XCode会自动过滤掉,不存在影响。我们二进制重排并非只是修改符号地址 , 而是利用符号顺序 , 重新排列整个代码在文件的偏移地址 , 将启动需要加载的方法地址放到前面内存页中 , 以此达到减少 page fault 的次数从而实现时间上的优化 , 一定要清楚这一点。

项目实战

1、新建一个项目,添加方法

2、修改配置,编译,找到xxx.txt文件

3、新建一个order文件:touch binary.order,加入几个方法

-[ViewController test3]
-[ViewController test2]
-[ViewController test1]

4、修改Order File配置为:$(SRCROOT)/Binary/binary.order 或 ./Binary/binary.order

5、clean,编译,再次查看xxx.txt文件。

可以看到,我们所写的这三个方法已经被放到最前面了,也就是说,这三个方法被放到了距离 mach-O 中首地址偏移量最小位置。假设这三个方法原本在不同的三页,那么意味着我们已经优化掉了两个 Page Fault。到这里,离启动优化就只差一步了,如何获取启动运行的函数?我们采用clang 插桩的技术方案,这样完全拿到 swift、oc、c、block 全部函数。


六、静态插桩代码覆盖工具的机制和原理

简单来说 SanitizerCoverage 是 Clang 内置的一个代码覆盖工具。它把一系列以 __sanitizer_cov_trace_pc_ 为前缀的函数调用插入到用户定义的函数里,借此实现了全局 AOP 的大杀器。其覆盖之广,包含 Swift/Objective-C/C/C++ 等语言,Method/Function/Block 全支持。

开启SanitizerCoverage 的方法是:在build settings 里的 Other C Flags 中添加 -fsanitize-coverage=func,trace-pc-guard。如果含有 Swift 代码的话,还需要在 Other Swift Flags 中加入 -sanitize-coverage=func-sanitize=undefined。所有链接到 App 中的二进制都需要开启 SanitizerCoverage,这样才能完全覆盖到所有调用。通过llvm 插桩的确定 order_file 的方案,需要使用源码重新打包。如果项目全是已经编译好的二进制模块,使用该方案效果不佳。

腾讯大神写了个工具 AppOrderFiles。CocoaPods 接入,程序启动完成函数一行调用,生成 Order File。全在 GitHub 里了:github.com/yulingtianx…

AppOrderFiles(^(NSString *orderFilePath) {
    NSLog(@"OrderFilePath:%@", orderFilePath);
});

添加编译设置。直接搜索 Other C Flags 来到 Apple Clang - Custom Compiler Flags 中添加配置:-fsanitize-coverage=trace-pc-guard。通过这种方式适合纯 OC 工程获取符号。由于 swift 的编译器前端是自己的 swift 编译前端程序,因此配置稍有不同。搜索 Other Swift Flags,添加两条配置即可:-sanitize-coverage=func、 -sanitize=undefined。swift类同样可以通过这个方式获取。

cocoapod 工程引入的库,会产生多 target,我们在主target添加的配置是不会生效的,我们需要针对需要的target做对应的设置。对于直接手动导入到工程里的 sdk,不管是静态库 .a 还是动态库,会默认使用主工程的设置,也就是可以拿到符号的。

按照上面配置完成以后,在代码任意地方实现如下两个方法。这样所有的方法调用后,都会调用一次__sanitizer_cov_trace_pc_guard方法。在每个函数调用的第一句实际代码,会被添加进去了一个 bl指令, 调用到__sanitizer_cov_trace_pc_guard 这个函数中来 。

bl是汇编跳转指令,即调用方法。静态插桩实际上是在编译期,在每一个函数内部第一行代码处,添加 hook 代码 ( 即我们添加的__sanitizer_cov_trace_pc_guard 函数 ) ,实现全局的方法 hook,即AOP效果。

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;  // Duplicate the guard check.

  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  //__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

七、写入order文件

写入文件时有许多需要注意的地方,即坑点。考虑到这个方法会来特别多次,使用锁会影响性能,这里使用苹果底层的原子队列 ( 底层实际上是个栈结构,利用队列结构 + 原子性来保证顺序 ) 来实现。上述这种clang插桩的方式,会在while循环中同样插入hook代码。通过汇编会查看到while循环,会被多次静态加入 __sanitizer_cov_trace_pc_guard调用,导致死循环。解决方式是将 Other C Flags 修改为如下:-fsanitize-coverage=func,trace-pc-guardfunc表示仅hook函数时调用。

引入头文件
#import 
#import 
pragma mark - 获取order文件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSMutableArray * symbolNames = [NSMutableArray array];
    while (YES) {
        //offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
        SymbolNode * node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));
        if (node == NULL) break;
        Dl_info info;
        dladdr(node->pc, &info);
        
        NSString * name = @(info.dli_sname);
        
        // 添加 _
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        
        //去重
        if (![symbolNames containsObject:symbolName]) {
            [symbolNames addObject:symbolName];
        }
    }

    //取反
    NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
    NSLog(@"%@",symbolAry);
    
    //将结果写入到文件
    NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"binary.order"];
    NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
    BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    if (result) {
        NSLog(@"%@",filePath);
    }else{
        NSLog(@"文件写入出错");
    }
    
}
//原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
    void * pc;
    void * next;
}SymbolNode;
pragma mark - 静态插桩代码
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;  // Duplicate the guard check.
    
    void *PC = __builtin_return_address(0);
    
    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    
    //入队
    // offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置
    OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
}

最后运行,下载.order文件到本地,就可以愉快的玩耍了。这样order文件就被写入沙盒中。我们可以把这个文件导出,加入到我们工程中。生成的order文件如下图所示:


八、验证插桩前后差距

重排前后LinkMap文件对比,如下图所示:

插桩前监控数据:

插桩后监控数据:

本次测试设备是iPhone xs max,优化结果:627.51-169.06=458.45ms。

使用方法

每次准备发版前,使用MedLinkerOptimize这个target,运行App,点击首页的"重排"按钮,然后点击"生成order文件”按钮(此时比较耗时、耐心等待!),用新生成的order文件替换项目中的旧order文件。


九、全文总结

我们可以看到图中项目的Page Fault数量并不多,这是因为当前项目是一个demo,代码和文件都极少。当代码多起来的话,Page Fault的 数量和加载耗时都会随着代码增加而增加。二进制重排 可以很好优化这个问题,其中心思想是重新排列 方法符号的顺序, 使启动的相关方法排在最前面从而减少启动Page Falut的数量。我们先来看看原来的符号顺序,这需要用到 链接映射文件 Link Map FileLink Map File 里可以看到方法符号的排序。知道了原来的符号排序,开发者怎么去设置自己想要的顺序呢?Xcode提供了排列符号的设置给开发者,设置 order_file 即可。苹果也一直身体力行,objc 源码就采用了二进制重排优化。虽然知道了可以通过设置 .order文件调整符号的位置,但是并不知道怎么编写 order_file 。下载objc-750源码(源码下载地址),查看其order_file。打开libobjc.order,原来只需要填写符号即可。全手写一定是不可取的,想实现自动化就要解决下列问题:保证不遗漏方法。保证方法符号正确。保证方法符号顺序正确。抖音团队使用的是 静态扫描+运行时trace的方案, 能够覆盖到80%~90%的符号。但是上述的方法也存在性能瓶颈。initialize hook不到。部分block hook不到。C++通过寄存器的间接函数调用静态扫描不出来。为了解决这个瓶颈,我打算尝试一下在文末提到的编译期插桩。顾名思义,编译插桩就是在代码编译期间修改已有的代码或者生成新代码。编译期时,在每一个函数内部二进制源数据添加 hook 代码来实现全局hook效果。

说白了我们要跟踪到 每个方法的执行,从而获取到启动时 方法执行的顺序,然后再按照这个顺序去编写order file。跟踪的具体实现会用到clang 的 SanitizerCoverage,这是什么东西??LLVM 具有内置的简单代码覆盖率检测工具(SanitizerCoverage)它可以在函数,块、边缘级别插入用户定义函数并提供回调。通过看守者跟踪 (Tracing PCs with guards

文档是个好东西~里面就有 example。


十、摒弃解释,直接使用

1、使用二进制重排

二进制重排原理

尝试这样一种场景,在应用启动过程中,调用到的方法处在不同的内存分页上,那么在启动过程中就会不停地触发缺页中断,导致进程阻塞,从而引起启动时间变长。而如果可以尝试将启动过程所需要的方法尽可能集中在较少的分页上,通过减少缺页中断的触发次数,就可以将启动时间缩短。这就二进制重排优化启动的基本原理。

如何判断缺页中断耗时

既然需要进行优化,那就需要有个衡量的标准,来进行优化前后的对比来查看优化是否达到预期效果。这里主要有两种方式。

使用Instruments工具

在Xcode中可以使用Instruments工具中的System Trace工具来查看在应用启动阶段中缺页中断的触发次数.为了能够更加真实的反应数据 , 最好是将应用杀掉重新安装 , 因为冷热启动的界定其实由于进程的原因并不一定后台杀掉应用重新打开就是冷启动。

  1. 打开 Instruments , 选择 System Trace
  1. 选择真机,选择工程,点击启动,当首个页面加载出来点击停止。
  1. 等待分析完成,查看缺页次数。这就是第一次安装时候的page fault次数与耗时。
设置Xcode调试参数

通过Xcode启动参数设置可以查看到启动过程的耗时,从侧面做一个验证。打开项目在Xcode中,通过Edit Scheme->Arguments(或者快捷键组合cmd+shift+,)设置打印加载数据分析参数来查看启动参数。这样就可以在启动之后查看到启动加载相关操作的耗时。

如何查看自己工程的符号顺序

不得不说,Xcode是开发神器,你需要的功能它几乎都有。Link Map 是编译期间产生的产物 ,(ld的读取二进制文件顺序默认是按照Compile Sources - GUI里的顺序 ) ,它记录了二进制文件的布局,这时候就可以通过设置Write Link Map File来查看。

然后运行项目,就会在Products的同级目录中找到关于项目的一个.txt文件,这里就保存了在编译期间的二进制分布信息。这个符号顺序明显是按照 Compile Sources 的文件顺序来排列的。

在这个.txt文件中可以看到符号的加载顺序:

如何改变二进制符号的加载顺序

在Xcode中可以通过设置Order File来人为干预编译期间的符号加载顺序。随便定义一个symbols.order文件:

-[ViewController clipView]
_CGRectMake
-[ViewController pan:]

在Xcode编译配置中设置symbols.order的路径:

清理项目编译,重新编译,然后查看编译生成的.txt文件,就会发现设置的order文件确实改变了文件的编译顺序。

获取启动期间加载的所有符号

基于以上的实践基础,可以使用clang静态插桩来获取启动期间调用到的函数符号。

编写order文件

获取到启动期间的所有符号之后对符号进行调整顺序去重之后,写入.order文件用以改变编译期间的二进制布局,达到减少触发缺页中断,缩短启动时间的目的。


2、clang静态插桩方式

静态插桩作用

通过静态插桩,可以查看项目中的代码执行情况,进而为项目优化提供依据。

  • 重排二进制文件:可以根据启动时调用的方法,存储在.order文件中,认为干预二进制文件的生成,优化启动速度;
  • 删除无用代码:可以根据项目中方法的执行情况,查看方法的覆盖率,将没有使用到的方法进行删除,减少二进制文件的大小;
  • 跟踪方法调用顺序:可以将调用的符号进行保存来查看应用中方法的调用顺序,跟踪异常。
步骤一:添加 Build Setting 设置
Target -> Build Setting -> Custom Complier Flas ->

Other C Flags 添加:
-fsanitize-coverage=func,trace-pc-guard


Other Swift Flags 添加:
-sanitize-coverage=func
-sanitize=undefined

代码插桩是指根据一定的策略在代码中插入桩点来统计代码覆盖的技术手段,一般可以分为三个粒度:

  • 函数(function):按照函数为单位进行插桩;
  • 基本块(basic block):按照代码执行单元进行分组的执行单元,单元内部的代码执行次数一定是相同的;
  • 边界(Edge):按照代码执行路径进行插桩。

针对iOS来说,clang支持以上粒度的插桩方式。这里先介绍一些函数粒度的插桩实现。Clang 是一个高度模块化开发的轻量级编译器。可以通过设置Clang的编译参数实现静态插桩。在Xcode->Build Settings中搜索Other C Flags然后在其中添加

// 基本块覆盖可以使用参数:  -fsanitize-coverage=bb,trace-pc-guard
// 边缘覆盖可以使用参数:  -fsanitize-coverage=edge,trace-pc-guard
-fsanitize-coverage=func,trace-pc-guard
步骤二:添加代码

添加以下两个函数到启动最早的那个 ViewController 即可。

#import "dlfcn.h"
#import 

//初始化原子队列
static OSQueueHead list = OS_ATOMIC_QUEUE_INIT;
//定义节点结构体
typedef struct {
    void *pc;   //存下获取到的PC
    void *next; //指向下一个节点
} Node;

哨兵初始化函数,其中[*start,*end)表示了哨兵的标志,这里可以理解为每个哨兵guard是一个指针,保存了一个uint32_t的整形数据来作为自己的标记。

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.
}

当每个函数开始调用时会被插入该回调,所以在方法调用开始就会执行该回调。函数中guard就是__sanitizer_cov_trace_pc_guard_init[start, end)区间中一个。

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
     // 在这里可以尝试获取到执行函数的信息
     void *PC = __builtin_return_address(0);
     Node *node = malloc(sizeof(Node));
     *node = (Node){PC, NULL};
     // offsetof() 计算出列尾,OSAtomicEnqueue() 把 node 加入 list 尾巴
     OSAtomicEnqueue(&list, node, offsetof(Node, next));
}

然后运行项目会发现,回调正常执行那么问题来了,如何在回调函数中获取到当前执行函数的信息呢?

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
     NSMutableArray *arr = [NSMutableArray array];
     while(1){
         //有进就有出,这个方法和 OSAtomicEnqueue() 类比使用
         Node *node = OSAtomicDequeue(&list, offsetof(Node, next));
         //退出机制
         if (node == NULL) {
             break;
         }
         //获取函数信息
         Dl_info info;
         dladdr(node->pc, &info);
         NSString *sname = [NSString stringWithCString:info.dli_sname encoding:NSUTF8StringEncoding];
         printf("%s \n", info.dli_sname);
         //处理c函数及block前缀
         BOOL isObjc = [sname hasPrefix:@"+["] || [sname hasPrefix:@"-["];
         //c函数及block需要在开头添加下划线
         sname = isObjc ? sname: [@"_" stringByAppendingString:sname];
         
         //去重
         if (![arr containsObject:sname]) {
             //因为入栈的时候是从上至下,取出的时候方向是从下至上,那么就需要倒序,直接插在数组头部即可
             [arr insertObject:sname atIndex:0];
         }
     }
       
     //去掉 touchesBegan 方法 启动的时候不会用到这个
     [arr removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
     //数组合成字符串
     NSString * funcStr = [arr  componentsJoinedByString:@"\n"];
     //写入文件
     NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"link.order"];
     NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
     NSLog(@"%@", filePath);
     [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
步骤三:取出 order file
  • 在步骤二的代码NSLog(@"%@", filePath);断点
  • 如果页面无法触发点击,viewDidLoad里面调用touchesBegan:withEvent:也可以
  • 运行代码后记录link.order 的路径
  • Finder 前往路径取出 order file

使用Xcode连接真机,启动应用直至第一个控制器界面加载完成,使用快捷键cmd+shift+2进入Devices and Simulators界面,选择对应应用并点击Download Containers选择保存路径下载文件。在下载的.xcappd中右键显示包内容,在AppData->Library->Caches路径下即可保存的.txt文件。由于队列的特性,这里的符号与实际调用顺序是相反的。这样就可查看到在应用首个控制器显示之前系统调用的所有符号,从而为应用启动优化奠定基础。

步骤四:设置 order file

link.order的路径放到工程根目录
Target -> Build Setting -> Linking -> Order File 设置路径

步骤五:编译代码

把步骤一 order file 的设置还原
把步骤二添加代码删除
clean 以后编译代码

你可能感兴趣的:(IOS优化:启动时间)