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

本文中所说的启动都指:冷启动。
冷启动:内存中不包含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)

真机测试结果:


  • main函数之前可以看到大概需要四个步骤:
  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个

  • 可执行文件Mach-O->显示包内容->Frameworks中可以查看项目中使用到的动态库。
  • 因为项目是swift项目,所以有一些swift的系统库。不过61个库还是吓自己一跳。

2.4 rebase/binding

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

  • rebase:通过aslr加密技术所有使用到的符号重定向
  • binding绑定:将aslr加密后的地址绑定给对应的符号

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

3、虚拟内存与物理内存

  • 物理内存:真实内存条。物理内存的地址叫做物理地址(真实存在的);

  • 虚拟内存:(一张表 保存虚拟地址和物理地址对照表,也称为页表) 用来管理应用虚拟地址和物理内存地址的映射关系,存在物理内存的操作系统模块中需要硬件的支持;5大分区都是存在虚拟内存地址;

  • 内存分页管理:所有的内存数据都被分割成 一页为单位的页,应用的虚拟内存被分为一页一页,首地址都为0。
  • 内存页大小: MacOS 4k iOS 16k。
  • 虚拟空间大小:每个应用(进程)默认可以分配4G大小。但它实际只是一张页表,记录映射关系就可以
  • 安全性提高:通过页表也解决的安全问题,当前进程只能访问系统分配的页表地址,无法访问真实的物理地址、以及其他页面的内容;

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

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

4、二进制重排:

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

4.1 查看pageFault

  • 缺页异常(pageFault):读取到没有加载到物理内存中一页时触发;多次的pageFault也会造成启动时间的加长;
  • iOS中每一页是16K大小,但是16K中,可能真正在启动时刻需要用到的,可能不到1K。 但是启动需要访问到这1K数据,不得不把整页都加载。
image.png
  • 可以看到pageFault出现了3040次,看起来还是挺少的。注:这是热启动的结果.

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

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

查看包内容:


  • 函数顺序:(书写顺序)


  • 如果这个符号加载顺序和符号调用顺序一致就解决了这个问题;

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

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

  • 使用oreder.file可以调整方法的加载顺序;

4.4-获取符号调用顺序

  • 有了oreder.file这个手段,只要再知道符号调用顺序就完美了,继续往下看。

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

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

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

5.1 开启SanitizerCoverage

  • 开启OC项目: Build Settings-> Other C Flags 中添加 -fsanitize-coverage=func,trace-pc
  • 开启Swift项目: Build Settings-> Other Swift Flags 中添加 --sanitize-coverage=func-sanitize=undefined

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

编译之后会报2个错误:


  • 说是__sanitizer_cov_trace_pc_guard_init,__sanitizer_cov_trace_pc_guard两个符号没有找到,那我们就自己写一个。这就是Clang的核心方法。

5.2 __sanitizer_cov_trace_pc_guard调用时机

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

  • 每一个方法、block、函数在调用前,都会被clang在编译阶段将__sanitizer_cov_trace_pc_guard符号插入方法的函数调用栈

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);
}
  • 在第一个页面出现的位置调用,获取到启动符号的执行顺序,将HRTest.order文件导出。
  • 根据本文中4.3,修改项目oreder.file配置

demo下载

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

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

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

推荐阅读更多精彩内容