启动优化
通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)
- DYLD_PRINT_STATISTICS设置为1
- 如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1
目前DYLD_PRINT_STATISTICS在iOS15中失效了,可以通过Time Profiler诊断工具看
DYLD_FRAMEWORK_PATH
DYLD_FALLBACK_FRAMEWORK_PATH
DYLD_VERSIONED_FRAMEWORK_PATH
DYLD_LIBRARY_PATH
DYLD_FALLBACK_LIBRARY_PATH
DYLD_VERSIONED_LIBRARY_PATH
DYLD_IMAGE_SUFFIX
DYLD_INSERT_LIBRARIES
DYLD_PRINT_TO_FILE
DYLD_PRINT_LIBRARIES
DYLD_PRINT_LOADERS
DYLD_PRINT_SEARCHING
DYLD_PRINT_APIS
DYLD_PRINT_BINDINGS
DYLD_PRINT_INITIALIZERS
DYLD_PRINT_SEGMENTS
DYLD_PRINT_ENV
DYLD_SHARED_REGION
DYLD_SHARED_CACHE_DIR
冷启动premain分三个阶段
- dyld loading:apple的动态链接器,用来装载Mach-O文件(可执行文件、动态库等)
- 尽量减少自定义动态库数量,官方建议最大不超过6个
- rebase/binding:偏移修正
- rebase:每次启动使用ASLR(地址空间布局随机化)地址值插入到二进制文件的开头,目的是为了安全
- binding:
NSLog(@"123")
在编译时生成Mach O文件中创建一个符号NSLog,随机指向一个地址,binding是将符号所在的地址指向真正的地址做关联。dyld_styub_binder技术找到NSLog指针地址进行调用
- objc setup
- runtime在此处初始化,对class和category进⾏注册,selector唯⼀性判断
- load、constructor、initializer
- 调⽤所有类的load的⽅法,初始化C&C++的静态化变量,然后调⽤ constructor函数
虚拟内存和物理内存
早期计算机是物理内存
CPU通过虚拟地址找到真实物理内存
操作系统为了解决安全问题和效率问题,抽象出了虚拟内存页(实际上是一个表)的概念。内存都是分页访问的。这里的page指的就是内存页。(就像磁盘存储的最小单位 磁盘簇,大小是4k一样)
MacOS 、linux (4K为一页)
iOS(16K为一页)
缺页中断
PageFault就是缺页中断:当app调用一个方法,发现该方法没有在内存中,此时操作系统就会立刻阻塞整个app进程,触发一个缺页中断。操作系统会从磁盘中读取这页数据到物理内存上 , 然后再将其映射到虚拟内存上 ( 如果当前内存已满 , 操作系统会通过置换页算法 找一页数据进行覆盖,这也是为什么开再多的应用也不会崩掉 , 但是之前开的应用再打开时 , 就重新启动了的根本原因 )。
假如,app启动时期需要调用 method1、method5和method6,这三个方法分布在page1、page2和page3上。每装载一个内存页page都会发生一次PageFault(缺页终端)。通常一个PageFault的处理时间是0.1ms~1ms,取0.5ms计算。这三次处理PageFault时间是 3 * 0.5ms = 1.5ms。
二进制重排
将所有启动时刻需要调用的方法排列在一起
怎么查看PageFault次数?
用Xcode的System Trace查看,搜索MainThread,Summary切换到Virtual Memory
Clang插桩
LLVM内置了代码覆盖仪表,它在函数级、基本块级和边缘级插入对用户定义函数的调用,并提供了这些回调的默认实现,在认为启动结束的位置添加代码,就能够拿到启动到指定位置调用到的所有函数符号。文档:https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs-with-guards
- 在目标工程 Target -> Build Settings -> Other C Flags 添加 -fsanitize-coverage=func,trace-pc-guard。(不写func会把所以跳转都hook,比如while循环)
- 如果有swfit代码,也要在 Other Swift Flags 添加 -sanitize-coverage=func 和 -sanitize=undefined
- 在代码文件中插入一下代码,要不然编译不通过
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
//所以方法、函数、block、property的符号
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.
}
//原子队列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
void * pc;
void * next;
}SymbolNode;
//hook了方法,函数,block的调用,只要有跳转就会hook,比如while。 Other C Flags 添加 -fsanitize-coverage=func,trace-pc-guard可以解决这个问题
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(&symboList, node, offsetof(SymbolNode, next));
}
-
__builtin_return_address(0)
返回上一个函数的调用地址
- 在启动结束的位置添加代码
//定义数组
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (YES) {//一次循环!也会被HOOK一次!!
//offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if (node == NULL) {
break;
}
Dl_info info = {0};
dladdr(node->pc, &info);
NSString * name = @(info.dli_sname);
free(node);
BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
//是否去重??
[symbolNames addObject:symbolName];
}
//反向数组
NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
//创建一个新数组
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
//去重!
while (name = [enumerator nextObject]) {
if (![funcs containsObject:name]) {//数组中不包含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];
Dl_info info = {0}; dladdr(node->pc, &info); NSString * name = @(info.dli_sname);
获取调用函数名字
- 在Xcode->Target->Build Settings->Linking->Order File中设置导出的文件order路径(可以将order文件复制到工程根目录)
如何验证?
导出的order文件格式如下
+[SDWebImageDownloader(CollectingMetrics) load]
+[SDWebImageDownloaderOperation(Metrics) load]
___39+[UIScrollView(MJExtension) initialize]_block_invoke
+[MMSDWebImageURLProtocol load]
+[FFFastImageViewManager load]
+[LXDBacktraceLogger load]
+[MMLikePopupManager load]
+[MMV5PopupMenuRNManager load]
……
Xcode Build产生的LinkMap文件格式如下
# Path: /Users/xxxx/Library/Developer/Xcode/DerivedData/xxxxx-hfblvrkmkyihscafanwsvjxsftmr/Build/Products/Debug-iphonesimulator/xxx.app/xxx
# Arch: x86_64
# Object files:
[ 0] linker synthesized
[ 1] dtrace
[ 2] /Users/xxxx/Library/Developer/Xcode/DerivedData/xxxx-hfblvrkmkyihscafanwsvjxsftmr/Build/Intermediates.noindex/X x x x.build/Debug-iphonesimulator/xxx.build/Objects-normal/x86_64/main.o
[ 3] /Users/xxxx/Library/Developer/Xcode/DerivedData/Xxxx-hfblvrkmkyihscafanwsvjxsftmr/Build/Intermediates.noindex/Xxxx.build/Debug-iphonesimulator/xxxx.build/Objects-normal/x86_64/NTAppDelegate.o
…………
# Sections:
# Address Size Segment Section
0x100002FD0 0x02022AA2 __TEXT __text //主程序
0x102025A72 0x00006D2C __TEXT __stubs //用于 Stub 的占位代码,很多地方称之为桩代码。
0x10202C7A0 0x0000478C __TEXT __stub_helper //当 Stub 无法找到真正的符号地址后的最终指向
0x102030F30 0x000BF5A8 __TEXT __cstring //C 语言字符串
0x1020F04D8 0x0014C990 __TEXT __objc_methname //Objective-C 方法名称
0x10223CE68 0x0006F010 __TEXT __gcc_except_tab
…………
# Symbols:
# Address Size File Name
0x100002FD0 0x00000120 [ 2] _main
0x1000030F0 0x00000016 [ 2] _sancov.module_ctor_trace_pc_guard
0x100003110 0x00000440 [ 3] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100003550 0x00000120 [ 3] -[AppDelegate wbSDKServiceDidFinishedNotification:]
0x100003670 0x00000190 [ 3] -[AppDelegate dealUserUnloginOrUnUploaded]
0x100003800 0x00000070 [ 3] -[AppDelegate configRootTabViewController:]
0x100003870 0x000009E0 [ 3] +[AppDelegate setRootTabControllerBelow:]
…………
Sections:
Address Size Segment Section
0x100002FD0 0x02022AA2 __TEXT __text
这里就是主程序代码段的虚拟内存地址,0x100002FD0是地址开始位置,0x02022AA2是大小
-
计算order文件中Symbol所在的虚拟内存页数
- 上面提到iOS16K为一页
- 遍历order文件每行,在link map文件中找到对应的符号地址(SymbolLineAddr)和大小(SymbolLineSize)
- SymbolLineAddr减去开始位置的地址,然后除以16K,得到当前行符号对应的页数,添加到数组
- 获取数组count,这里就是所有符号所在的内存页数总和
-
计算Order文件中Symbol理论上所占用内存需要多少内存页数
- 上一步骤获得了每行符号对应的内存大小,值相加
- Sum除以16K,可以获得理论上所有符号占用的的内存页数
- 和上一步获得的结果比较,看是否一致,如果一致说明不存在缺页中断
打点
- 获取进程时间戳
#import <sys/sysctl.h>
#import <mach/mach.h>
+ (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc*)procInfo
{
int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
size_t size = sizeof(*procInfo);
return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;
}
+ (NSTimeInterval)processStartTime
{
struct kinfo_proc kProcInfo;
if ([self processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo])
{
return kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0;
}
else
{
return 0;
}
}
- 获取最早的+load
通过 AAA 为前缀命名 Pod,让 +load 第一个被执行
- main函数
- finishLaunch begin
- finishLaunch end
- flow finish