iOS底层-30:启动优化

冷启动和热启动
当用户按下home键时,iOSAPP不会立马被kill掉,还会存活一段时间,在这个时间之内,用户再进入APPAPP几乎不需要做什么,就可以还原到退出时的状态。这个称之为热启动
与之对应的冷启动,就是内存中不包含APP的相关数据,必须从磁盘中载入到内存。重启手机后,就是绝对的冷启动。

APP的启动优化主要分为两个阶段:

  • pre-main(main()函数之前)
  • main()函数之后

pre-main

苹果提供了一个方法,可以监测main()函数之前的耗时。
Xcode中,Project->Scheme->Edit Scheme -> Run -> Arguments -> Environment Variables,添加DYLD_PRINT_STATISTICS

command+R运行,控制台就会多出这样一份信息。

这里用的是我之前一个没有优化过的项目,选用的是release环境,我们可以看到pre-main time非常久,总共达到了933.64ms

  • dylib loading time:动态库的载入
  • rebase/binding time:偏移修正和符号绑定
  • ObjC setup time:OC类注册耗时,swif类要比OC
  • initializer time:执行+load和构造函数的耗时
  • slowest intializers:最慢的,举了一些例子
  • libglInterpose.dylib:系统库,和调试相关
  • 最后一个是咱们的主程序耗时

偏移修正:在二进制文件中,方法、函数调用都有一个地址,这个地址是在这个二进制文件当中的偏移地址。但是我们的程序在运行中,系统会给我们分配一个ASLR随机地址值,插入到二进制文件的开头,函数方法的地址就发生了变化。
符号绑定:比如我们的NSLog,是foundation框架中的方法,编译的时候并不知道它的真实地址,真实地址是在运行时绑定的,这就是符号绑定。

与之对应的优化方法有这些:

1. 减少动态库的使用
官方建议自定义动态库不要多于6个,但实际使用6个可能远远不够,这时候就要考虑动态库合并了。

2. 减少不必要的OC类,尽量不使用+load方法
减少加载后可能不会使用的类或者方法,尽量不要写+load()方法,可以用+initialize()dispatch_once()替换+load()

3. 减少C++构造函数和静态对象
尽量不要写 __attribute__((constructor))的C函数,也尽量不要用到C++的静态对象

main函数之后

main函数之后到第一个页面渲染完成,这之中尽量不要做耗时的操作,如果一定要做,那么请使用多线程去完成。

启动时需要加载展示的页面,最好不要用storyboardxib,因为storyboard要经过一层代码解析和转换,再渲染。

二进制重排

由于现在系统采用的都是虚拟地址加物理地址,启动时大量的page fault也会耗时。通过二进制重排,把启动时需要访问的内存,集中在一个page里面,减少page fault的次数,这就是二进制重排的核心原理。

检测工具

page fault的次数可以通过Instruments -> System Trace查看

  • 选择我们要监测的应用运行


  • 首屏渲染完成停止
    搜索main thread,选择Summary:Virtual Memory查看虚拟内存情况

我们可以看到page fault的次数是1022次,总共耗时278.38ms,最长的一次是4.83ms

二进制重排实现

二进制文件的顺序是根据Build Phases -> Compile Sources里的顺序来的,每个文件里面按照从上到下的顺序。

  • link map
    通过link map可以查看方法写入二进制的顺序。


    搜索link map,将Write Link Map File修改为YES,编译一次就可以在上面的path路径中拿到link map文件。

  • 通过配置.order文件,可以改变文件的链接顺序,可以在Xcode中配置


    下面是objc源码中的.order文件,可以参考这个来写。

如果orde_file中的符号实际不存在会怎么样?
Id会忽略这些符号,如果提供了link选项-order_file_statistics,会以warning的形式把这些没找到的符号打印在日志里。

前面的准备工作做好了,下面最重要的是如何获取启动时加载的方法?

要获取APP启动时加载的方法,首先想到的是hookobjc_msgSend,使用fishhook,但由于objc_msgSend是可变参数,需要使用汇编,而且不能覆盖所有符号。下面提供一个clang插桩方式,hook一切符号的方式。

clang插桩

clang插桩官方文档

  • 在Xcode中添加-fsanitize-coverage=trace-pc-guard

  • 编译会报两个错误


    说明只要我们添加了-fsanitize-coverage=trace-pc-guard,他就会自动调用___sanitizer_cov_trace_pc_guard_init___sanitizer_cov_trace_pc_guard这两个函数

官方文档给出了这两个函数的例子:


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

这里面的start为0stop为启动时函数调用结束的数目。通过stop - start可以得出方法函数的调用次数。

extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check. 屏蔽了load方法
  void *PC = __builtin_return_address(0);//当前函数返回到上一个调用的地址 0 表示当前函数会回到哪里  1 表示上一个函数会回到哪里
  char PcDescr[1024];
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

这个函数,在每一次调用方法时都会进来,guard里包含了一些方法地址和信息。


只要通过trace_pc_guard就可以拿到APP启动时调用的所有方法,现在只要我们通过guard信息拿到方法的reciverSEL,就可以进行order_file的编写。

  • 打上图示断点

    这时候进来的是main函数,查看左边堆栈可知

    打印发现PC的地址和main函数的地址一样

    PC函数返回地址,当调用完trace_pc_guard,会继续执行原函数,PC只是返回原函数地址,并不是原函数的起始地址

这里我们需要借助dlopen动态链接库

#import <dlfcn.h>

其中有一个Dl_info结构体

typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;

trace_pc_guard加上以下几行代码


断点运行

可以看到main函数的名字输出了。

  • fname:文件地址
  • fbase:文件地址
  • sname:函数名称
  • saddr:函数起始地址

这样我们通过dladdr()函数就拿到了我们想要的内容。

符号的存储

  • 自定义一个原子队列,将数据存入队列
//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
  • 自定义一个结构体SYNode用来存储PC数据
//定义符号结构体
typedef struct {
    void *pc;
    void *next;
}SYNode;

队列是一个单链表,next用来指向下一个元素位置

  • 自定义一个获取队列中数据的函数readSymbolList()
void readSymbolList() {
    
    while (YES) {
        SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
        if (node == NULL) {
            break;
        }
        Dl_info info;
        dladdr(node->pc, &info);
        printf("%s \n",info.dli_sname);
    }
}

我们在App首屏界面中的touchesBegan调用它,读取队列中的数据。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{   
    readSymbolList();
}
  • 运行程序,查看readSymbolList()打印结果


    发现程序进入可死循环,究其原因是因为while循环也会被hook掉,每进行一次循环就会调用一次trace_pc_guard

  • 进入循环后打上断点


  • 查看汇编


    此时的地址是0x104a24808

  • 进入trace_pc_guard打上断点

  • 断点进来后,查看PC


返回地址为0x0000000104a24870

  • 回到readSymbolList()的汇编


    找到0x104a24870这个地址看到是一个跳转,跳转到0x104a24838这个地址

  • 查看0x104a24838


    这正是第一个断点的位置所在,如此就陷入了死循环,程序在readSymbolList()trace_pc_guard中一直循环跳转。

死循环解决方法

clang插桩的时候,设置让循环不会调用trace_pc_guard方法


在上面的value中加入func,这样就只会hook方法。

  • 再次运行



    程序启动时调用的方法成功打印。注意这里是一个倒序的,因为是单链表。

上述的打印还存在一下几个问题:
1. 顺序是颠倒的
2. 没有+load方法
3. 方法重复
4. 如果是函数,需要在前面加_

  • 解决load方法


    去除这一行代码

  • 处理并写入order


void writeOrderFile() {
    
    //定义数组
    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);
        //如果是OC方法 直接存
        if ([name hasPrefix:@"+["] || [name hasPrefix:@"-["]) {
            [symbolNames addObject:name];
            continue;
        }
        //如果不是
        [symbolNames addObject:[@"_" stringByAppendingString:name]];
        
    }
    
    //取反 去重
    NSEnumerator *enumerator = [symbolNames reverseObjectEnumerator];
    NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    
    NSString *name;
    while (name = [enumerator nextObject]) {
        if (![funcs containsObject:name]) {
            [funcs addObject:name];
        }
    }
    //去除本身这个函数
    [funcs removeObject:[NSString stringWithFormat:@"_%s",__func__]];
    
    //写入order_file
    //组装成字符串
    NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
    //文件路劲
    NSString *filePath = [NSString stringWithFormat:@"%@%@",NSTemporaryDirectory(),@"example.order"];
    //文件内容
    NSData *fileData = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    //写入
    BOOL isSuccess = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
    if (isSuccess) {
        NSLog(@"order_file 写入成功");
    }else{
        NSLog(@"order_file 写入失败");
    }
}
  • 拿到order文件,配置到Xcodeorder file路径

  • 再次运行App,查看启动耗时


    优化后

    优化前

可以看到差不多pre-main时间减少了200ms

优化后

优化前

page fault数目从1000多减到了54

  • 完整代码
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>


//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;

//定义符号结构体
typedef struct {
    void *pc;
    void *next;
}SYNode;

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);
    
    //创建结构体
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    //加入结构体
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
    
}


void writeOrderFile() {
    
    //定义数组
    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);
        free(node);
        //如果是OC方法 直接存
        if ([name hasPrefix:@"+["] || [name hasPrefix:@"-["] || [name hasPrefix:@"_"]) {
            [symbolNames addObject:name];
            continue;
        }
        //如果不是
        [symbolNames addObject:[@"_" stringByAppendingString:name]];
        
    }
    
    //取反 去重
    NSEnumerator *enumerator = [symbolNames reverseObjectEnumerator];
    NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    
    NSString *name;
    while (name = [enumerator nextObject]) {
        if (![funcs containsObject:name]) {
            [funcs addObject:name];
        }
    }
    //去除本身这个函数
    [funcs removeObject:[NSString stringWithFormat:@"_%s",__func__]];
    
    
    //写入order_file
    //组装成字符串
    NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
    //文件路劲
    NSString *filePath = [NSString stringWithFormat:@"%@%@",NSTemporaryDirectory(),@"example.order"];
    //文件内容
    NSData *fileData = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    //写入
    BOOL isSuccess = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
    if (isSuccess) {
        NSLog(@"order_file 写入成功");
    }else{
        NSLog(@"order_file 写入失败");
    }
}

Swift

需要在Other Swift Flags配置一下参数

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容