iOS程序猿iOS学习笔记专注iOS开发的小渣渣

OC底层探索26-App启动时间优化

2021-07-11  本文已影响0人  Henry________

本文中所说的启动都指:冷启动。
冷启动:内存中不包含APP的数据,所有数据都需要从Mach-o载入到内存中,提供给应用使用。
热启动:内存中仍然存在APP的数据,数据不需要重新载入内存。

1、启动耗时

1.1 冷启动4个阶段

  1. dyld:动态库链接、初始化;
  2. runtime中:所有类加载+load方法执行C++相关函数
  3. main函数:call main()
  4. main函数之后:AppDelegate中的方法直到第一个页面显示完成;

1.2 启动耗时查看

想要优化启动时间,就需要要知道启动时app都做了什么?通过添加环境变量可以打印出APP的启动时间分析(Edit Scheme -> Run -> Arguments)

真机测试结果:


  1. dylib loading、
  2. rebase/binding、
  3. ObjC setup、
  4. initializers

那么这四步分别做了什么呢?这里让我先盗个图...


1.3 提高main()函数之前的加载时间

1.动态库加载越多,启动越慢。

2.ObjC类,方法越多,启动越慢。
3.ObjC的+load越多,启动越慢。

4.C的constructor函数越多,启动越慢。
5.C++静态对象越多,启动越慢。

2、耗时优化策略

2.1 删除无用代码,合并一些同样功能的类

OC类的注册耗时 (OC类越多,越耗时),swift的类不会存在这个问题。

检测iOS项目中未使用的方法文中有详细的介绍,工具和使用方式。

2.2 减少+load方法

方法交换等好多操作多多少少的会使用+load方法来执行一些操作,但是并不是每个方法都需要在+load那么早。建议部分操作可以延迟到+initialize中.

2.3 合并动态库

减少dyly动态库的使用,苹果建议动态库不超过6个

2.4 rebase/binding

减少重定向绑定操作的耗时;

ASLR(Address space layout randomization)地址空间配置随机加载,每次载入虚拟内存后,需要将原地址加上ASLR随机偏移值来进行内存读取.

3、虚拟内存与物理内存

恰巧看到一个很贴切的比喻:
比如你1T空间的百度网盘,你用了200M,网盘给你200M的空间资源,然后将这个资源地址和你的网盘账号关联起来。而你的网盘账号只是记录了每个资料和资料存放地址的映射关系列表,并不会占用你电脑空间。

百度网盘:物理内存
网盘账号:虚拟内存、虚拟页表

4、二进制重排:

目的:二进制重排就是为了把启动用到的这些数据,按调用顺序整合到一起。这样启动用到的数据()都在前面。就可以减少很多次pageFault,提高启动速度。
思路:获取启动时的符号调用顺序查看Mach-O中符号加载到虚拟页表的顺序(link map)进行排列。

4.1 查看pageFault

image.png

4.2 查看Mach-O中符号加载到虚拟页表的顺序(link map)

Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局.

查看包内容:


4.3 oreder.file-调整符号加载顺序

使用oreder.file,把启动时的方法调用顺序进行排列。

4.4-获取符号调用顺序

5、获取调用顺序-Clang插桩获取调用顺序

注:也可以使用fishHook:系统函数 -- objc_msgSend,但是swift方法和c
函数无法hook;

llvm内置了一个简单的代码覆盖率检测(SanitizerCoverage)。它在编译期对函数级、基本块级和边缘级插入对用户定义函数的调用。 clang官方文档

5.1 开启SanitizerCoverage

在汇编阶段只要有b,bl都会被hook,包含for、while循环(很坑)。
所以需要在命令里加上:coverage=func;

编译之后会报2个错误:


5.2 __sanitizer_cov_trace_pc_guard调用时机

查看调用时机,就需要借助汇编,在ViewController中的touchesBegand打下一个端点并且开启汇编;

5.3 获取所有符号地址

// clang依赖库
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
// dl_info
#import <dlfcn.h>
// 原子队列
#import <libkern/OSAtomic.h>

@implementation ClangTools
//定义原子队列
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;
}
// 核心方法!!!!
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    if (!*guard) return; 系统方法哨兵,这里不需要
    //__builtin_return_address(0); 0表示当前函数的栈返回地址,也就是调用该函数的方法地址;
    void *PC = __builtin_return_address(0);
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    
    //加入队列
    // offsetof两个作用:1. 获取SYNode内存大小 2. 移动SYNode大小后的地址赋值给next
    // offsetof方便链表使用
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
@end

5.4 将符号名称写成order.file

+(void)clangDataForWriteFile {
    //定义数组
    NSMutableArray<NSString *> * symbolNameList = [NSMutableArray array];
    
    while (YES) {
        // 从队列中取出SYNode
        SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
        
        if (node == NULL) {
            break;
        }
        
        Dl_info info = {};
        // 根据符号地址获取符号信息
        dladdr(node->pc, &info);
        NSString * tempName = @(info.dli_sname);
        free(node);
        // 除OC方法,其他方法头需要加上_
        BOOL isObjc = [tempName hasPrefix:@"+["]||[tempName hasPrefix:@"-["];
        NSString * symbolName = isObjc ? tempName : [@"_" stringByAppendingString:tempName];
        [symbolNameList addObject:symbolName];
    }
    // 数组取反
    NSEnumerator * enumerator = [symbolNameList reverseObjectEnumerator];
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNameList.count];
    //去重
    NSString * ttempName;
    while (ttempName = [enumerator nextObject]) {
        if (![funcs containsObject:ttempName]) {
            [funcs addObject:ttempName];
        }
    }
    // 当前函数并非属于启动函数
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    //写文件
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"HRTest.order"];
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    NSData * fileData = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
    
    NSLog(@"successful-clangTotal : %i",funcs.count);
}

demo下载

用在我自己的项目中,冷启动平均减少了50毫秒的启动时间。其实还是不错~

参考链接:
AppOrderFiles
iOS优化篇之App启动时间优化

上一篇下一篇

猜你喜欢

热点阅读