启动优化

启动优化

通过添加环境变量可以打印出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分三个阶段

  1. dyld loading:apple的动态链接器,用来装载Mach-O文件(可执行文件、动态库等)
    • 尽量减少自定义动态库数量,官方建议最大不超过6个
  2. rebase/binding:偏移修正
    • rebase:每次启动使用ASLR(地址空间布局随机化)地址值插入到二进制文件的开头,目的是为了安全
    • binding:NSLog(@"123")在编译时生成Mach O文件中创建一个符号NSLog,随机指向一个地址,binding是将符号所在的地址指向真正的地址做关联。dyld_styub_binder技术找到NSLog指针地址进行调用
  3. objc setup
    • runtime在此处初始化,对class和category进⾏注册,selector唯⼀性判断
  4. 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

image.png

Clang插桩

LLVM内置了代码覆盖仪表,它在函数级、基本块级和边缘级插入对用户定义函数的调用,并提供了这些回调的默认实现,在认为启动结束的位置添加代码,就能够拿到启动到指定位置调用到的所有函数符号。文档:https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs-with-guards

  1. 在目标工程 Target -> Build Settings -> Other C Flags 添加 -fsanitize-coverage=func,trace-pc-guard。(不写func会把所以跳转都hook,比如while循环)
  2. 如果有swfit代码,也要在 Other Swift Flags 添加 -sanitize-coverage=func 和 -sanitize=undefined
  3. 在代码文件中插入一下代码,要不然编译不通过
#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)); 
}
image.png
  • __builtin_return_address(0)返回上一个函数的调用地址
  1. 在启动结束的位置添加代码
//定义数组
    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);

获取调用函数名字

  1. 在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是大小

  1. 计算order文件中Symbol所在的虚拟内存页数

    • 上面提到iOS16K为一页
    • 遍历order文件每行,在link map文件中找到对应的符号地址(SymbolLineAddr)和大小(SymbolLineSize)
    • SymbolLineAddr减去开始位置的地址,然后除以16K,得到当前行符号对应的页数,添加到数组
    • 获取数组count,这里就是所有符号所在的内存页数总和
  2. 计算Order文件中Symbol理论上所占用内存需要多少内存页数

    • 上一步骤获得了每行符号对应的内存大小,值相加
    • Sum除以16K,可以获得理论上所有符号占用的的内存页数
    • 和上一步获得的结果比较,看是否一致,如果一致说明不存在缺页中断

    打点

image.png
  1. 获取进程时间戳
#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; 
    } 
}
  1. 获取最早的+load

通过 AAA 为前缀命名 Pod,让 +load 第一个被执行

  1. main函数
  2. finishLaunch begin
  3. finishLaunch end
  4. flow finish
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容