iOS 应用启动优化
1.启动
1.1 冷启动
- 冷启动指的是第一次打开应用,或者打开很多其他应用后再打开该应用也可以称之为冷启动
1.2 热启动
- 热启动指的是应用退到后台后又被唤醒
1.3 查看启动时间
Scheme -> Edit Scheme -> run -> Arguments
在Environment Variables 添加名称为 DYLD_PRINT_STATISTICS 的一项。
将自己的程序运行在真机设备上,查看启动时间(Main函数前)
Total pre-main time: 1.0 seconds (100.0%)
dylib loading time: 252.32 milliseconds (24.8%)
rebase/binding time: 13.32 milliseconds (1.3%)
ObjC setup time: 356.37 milliseconds (35.1%)
initializer time: 392.66 milliseconds (38.6%)
slowest intializers :
libSystem.B.dylib : 8.38 milliseconds (0.8%)
libMainThreadChecker.dylib : 174.17 milliseconds (17.1%)
SchemeName : 323.75 milliseconds (31.9%)
-
dylib loading:
加载lib库,动态库,库越多占用时间越长,所以尽量少用第三方库,苹果建议不要超过6个。如果实在太多建议合并。注: 系统库不在个数计算范围内,Apple已经做了相关的优化。
-
rebase/binding:
rebase 就是ASLR+偏移量的时间 rebinding就是dyld对符号进行重新绑定。所以这个步骤就是重新绑定符号,得到符号的真实地址。符号少耗时就少,几乎不能优化。
-
ObjC setup:
OC类的注册。2万个OC类大约800ms,几乎也不存在优化,Swift可优化性多一些,所以使用Swift尽量减少类的使用可能会节约一些时间。注:以上都是听说,没验证过。
-
initializer:
load方法的加载时间。所以尽量减少load方法的使用,可以放在initializer里面配合dispathOnce进行调用。
slowest intializers: 比较耗时的操作是:libSystem.B.dylib 系统库,libMainThreadChecker.dylib 调试库。加载的时候不需要我们考虑。剩下的比较耗时的就是我们的主程序的加载了。
2.main之后
2.1 打点计时框架
BLStopwatch 里面有中文注释。
2.2 main之后的优化建议
- 尽量使用懒加载
- 利用CPU的多核特性使用多线程初始化(看情况用,少写bug)
- 不要在启动的地方使用Xib或者storyboard,因为xib和storyboard也是解析成代码进行渲染的。
2.3 pre-main的优化(main函数前)
- 减少动态库的使用(最好不要超过6个)
- 减少load方法的使用
- Swift减少类的使用(少了类注册)
- 二进制重排
iOS 二进制重排
注:本文只介绍实现步骤。
详细的原理与背景详见 字节跳动技术团队分享的文章:
抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%
1. 获取Linkmap
Target -> BuildSettings -> Linking -> Write Link Map File
修改为YES 即会在上面的路径写入Linkmap文件
- Object files: 链接了哪些.O 文件
- Sections: MachO 一些段的数据
- Symbols: 符号(主要优化的地方)
几种查看符号的命令,还是以Link Map的顺序为主
cd ../DemoName.app
nm DemoName 查看此Demo的符号
nu -Up DemoName 查看此Demo的符号(自定义)
nu -up DemoName 查看此Demo的符号(系统)
2.核心原理
2.1 ld
Xcode 的连接器是ld, ld有一个不常用的参数-order_file
,通过man ld
可以看到详细文档:
Alters the order in which functions and data are laid out. For each section in the output file,
any symbol in that section that are specified in the order file file is moved to the start of its
section and laid out in the same order as in the order file file.
可以看到,order_file中的符号会按照顺序排列在对应section的开始,完美的满足了我们的需求。
配置符号在order_file
文件里,ld
会根据配置的顺序进行连接,最后达到符号加载到内存的顺序改变,满足优先调用的方法整体排在前面,减少Page Fault,从而达到启动优化。如果order_file
中的符号实际不存在, ld会忽略这些符号,如果提供了link选项-order_file_statistics
,会以warning
的形式把这些没找到的符号打印在日志里。
2.2 Xcode 配置order file:
3. 符号获取
原理很简单,但是如何获得符号的顺序才是最难的。
3.1 通过hook方式
我们可以hook所有方法的调用,因为OC 方法的调用本质是发送 Objc_msgSend()
函数,所以我们可以 hook Objc_msgSend()
函数,但是这个函数是可变参数的,要想获取内部参数,需要通过汇编代码,从寄存器获得参数,这就加大了难度。另外还有一些C++函数,Swift的获取也不能通过Objc_msgSend()
函数。另外block作为一个特殊的单元,也不走Objc_msgSend()
函数,所以需要单独 hook。总之有很多瓶颈,详细的hook和使用可参考字节跳动技术团队分享的那篇文章。
3.2 编译器插桩(Clang插桩)
3.2.1 Xcode 配置
-
hook 所有方法:
Other C Flags 配置如下项
-fsanitize-coverage=trace-pc-guard
- 只 hook 方法 除去 while 循环啥的:
-fsanitize-coverage=func,trace-pc-guard
- Swift hook: Other Swift Flags配置如下项
-sanitize-coverage=func
-sanitize=undefined
3.2.2 主要hook函数
来自Clang 文档的示例代码
// 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);
}
3.2.3. 自动生成order文件
NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
while (YES) {
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:@"-["];
NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
}
//取反
NSEnumerator * emt = [symbolNames reverseObjectEnumerator];
//去重
NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
while (name = [emt nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//干掉自己!
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//将数组变成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
特别提醒!
获取order文件后可以删除上述配置和文件