iOS启动优化

虚拟内存&物理内存

在计算机早期,数据的访问都是通过物理地址访问的,即进程直接对应到具体的物理内存;

这种方式有两个问题

一、内存数据的安全问题(可以通过已知地址+偏移量来获取到内存中数据)

二、内存不够用

针对问题,分别有不同的解决方案

内存不够用:虚拟内存

在进程和物理内存之间增加一个中间层,这个中间层就是所谓的虚拟内存,主要用于解决当多个进程同时存在时,对物理内存的管理。提高了CPU的利用率,使多个进程可以同时、按需加载。所以虚拟内存其本质就是一张虚拟地址和物理地址对应关系的映射表

1、每个进程都有一个独立的虚拟内存,其地址都是从0开始,大小是4G固定的,每个虚拟内存又会划分为一个一个的页表(页表的大小在iOS中是16K,其他的是4K),每次加载都是以页表为单位加载的,进程间是无法互相访问的,保证了进程间数据的安全性;页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)

2、一个进程中,只有部分功能是活跃的,所以只需要将进程中活跃的部分放入物理内存,避免物理内存的浪费(优化空间)

3、当CPU需要访问数据时,首先是访问虚拟内存,然后通过虚拟内存去寻址,即把地址翻译为实际物理内存地址,然后对相应的物理地址进行访问

4、如果在访问时,虚拟地址的内容未加载到物理内存,会发生缺页异常(PageFault),缺页异常的处理过程,操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪,如果内存已经满了,没有空地方了,那就找一个页覆盖,至于具体覆盖的哪个页,就需要看操作系统的页面置换算法是怎么设计的了

虚拟内存与物理内存联系(图片来自网络)
页表工作原理(图片来走网络)

内存数据的安全问题:ASLR

虚拟内存的起始地址与大小都是固定的,这意味着,当访问时,其数据的地址也是固定的,这会导致内存的数据非常容易被破解,为了解决这个问题,所以苹果为了解决这个问题,在iOS4.3开始引入了ASLR技术

ASLR的概念:(Address Space Layout Randomization )地址空间配置随机加载,是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术

其目的的通过利用随机方式配置数据地址空间,使某些敏感数据(例如APP登录注册、支付相关代码)配置到一个恶意程序无法事先获知的地址,令攻击者难以进行攻击

由于ASLR的存在,导致可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要在编译时来修复镜像中的资源指针,来指向正确的地址。即正确的内存地址 = ASLR地址 + 偏移值

优化方案

优化方案可以根据pre-main以及main函数阶段的优化(本章暂时先不讨论)

接下来着重介绍pre-main阶段的一种优化方案:二进制重排

二进制重排原理:

在虚拟内存部分(上面第4点),已知,当进程访问一个虚拟内存,而对应的物理内存不存在时,会触发缺页中断(Page Fault),因此阻塞进程。此时就需要先加载数据到物理内存,然后再继续访问。这个对性能是有一定影响的

基于Page Fault,App在冷启动过程中,会有大量的类、分类、三方等需要加载和执行,此时的产生的Page Fault所带来的的耗时是很大的。看下图

1、打开Instruments-->System Trace

System Trace

2、选择真机,工程,启动,首个页面加载出来点击停止(冷启动)

第一个页面出来后点击停止

3、查看Main Thread 下的Summary: Virtual Memory

未重排二进制时,参查看具体Page Fault与Duration

注意:此处1958就是冷启动情况下Page Fault次数,367.57就是耗时

4、可以通过设置Write Link Map File来输出加载顺序

Write Link Map File设置
加载顺序

从上面的Page Fault的次数以及加载顺序,可以发现其实导致Page Fault次数过多的根本原因是启动时刻需要调用的方法,处于不同的Page导致的。因此,我们的优化思路就是:将所有启动时刻需要调用的方法,排列在一起,即放在一个页中,来减少Page Fault。这就是二进制重排的核心原理

重排二进制时,参查看具体Page Fault与Duration

注意:此处3135是Page fault次数,21.65就是对应的耗时


二进制重排具体步骤:

在进行重排前,需要了解几个名次

Link Map

Link Map是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File,Link Map主要包含三部分

Link Map主要包含三部分

1、object Files 生成二进制用到的link单元的路径和文件编号

2、Sections 记录Mach-O每个Segment/section的地址范围

3、Symbols 按顺序记录每个符号的地址范围(如上面黑色图)

ld

Xcode 是用的链接器叫做ld,ld有一个参数叫Order File, 我们可以通过这个参数配置一个order文件的路径(如下图),在这个order文件中,将所需要的符号按照顺序写在里面,在项目编译时,会按照这个文件的顺序进行加载,以此来达到我们的优化

order file添加order文件

1、在.order文件中 ,需要将从启动到首页展示出来的符号按顺序写在里面

2、当工程 build 的时候 , Xcode 会读取这个文件 , 打的二进制包就会按照这个文件中的符号顺序进行生成对应的mach-O.

可以通过输出Link Map File来对比重排前后文件的顺序

Link Map File查找路径:

/Users/用户名/Library/Developer/Xcode/DerivedData/App名称/Build/Intermediates.noindex/App名称.build/Debug-iphoneos/App名称.build/App名称-LinkMap-normal-arm64.txt

注意:替换其中的中文为实际的地址

优化后加载顺序

通过对比两次加载顺序(上面两幅黑色背景图),可知打的二进制包对应的mach-O是按照.order中的顺序进行加载的

如果项目小,可以很轻易的找到从启动到首页加载出现之间所调用的所有方法,如果项目很大,那么这些文件的查找将是一个十分费力的事情;那么该如何查找呢?

Clang插桩

具体步骤:

1、配置

在TARGETS-->Build Settings --> Other C Flags 添加:-fsanitize-coverage=func,trace-pc-guard

other c flags

如果有Swift,那么还需要在TARGETS-->Build Settings --> Other Swift Flags 添加:

-sanitize-coverage=func

-sanitize=undefined

other Swift Flags配置

2、在首页添加两个方法

添加方法之前需要先定义两个结构用来方便存储和读取

引入相关库:

#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)

void __sanitizer_cov_trace_pc_guard(uint32_t*guard) 

因为添加了Other C Flags后,会自动找这两

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);

        //start:起始位置

        //stop:并不是最后一个符号的地址,而是整个符号表的最后一个地址,最后一个符号的地址=stop-4(因为是从高地址往低地址读取的,且stop是一个无符号int类型,占4个字节)。stop存储的值是符号的

        //函数、方法、block都能拿到

      for(uint32_t*x = start; x < stop; x++)

        *x = ++N;

}

/// 全面hook方法、函数、以及block调用,用于捕捉符号,是在多线程进行的,这个方法中只存储pc,以链表的形式

/// @param guard  是一个哨兵,告诉我们是第几个被调用的

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {

//  if (!*guard) return; //这行代码会把load 方法return掉

    /*

     char PcDescr[1024];

     printf("guard: %p %x PC %s \n"guard,*guard,PcDescr)

     */

    //PC是当前函数返回到上一个调用的地址!!  参数:0代表当前函数返回到哪里 1代表上层函数返回到哪里去

    void *PC = __builtin_return_address(0);

    //创建结构体!

    SYNode* node =malloc(sizeof(SYNode));

    *node = (SYNode){PC,NULL};

    //加入队列

    //符号的访问不是通过下标访问,是通过链表的next指针,所以需要借用offsetof(结构体类型,下一个的地址即next)

    OSAtomicEnqueue(&symbolList, node,offsetof(SYNode,next));//链表数据结构

}

3、获取所有符号并写入文件

while循环从队列中取出符号,处理非OC方法的前缀,存到数组中

3.1数组取反,因为入队存储的顺序是反序的

3.2数组去重,并移除本身方法的符号

3.3将数组中的符号转成字符串并写入到lvjianxiong.order文件中

- (void)getSymbolFile{

    //定义数组

    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];


    while (YES) {//一次循环!也会被HOOK一次!!

            //解决循环办法:Other C Flags 添加 func,只有func才被hook

            //取出

           SYNode* node =OSAtomicDequeue(&symbolList,offsetof(SYNode, next));

            if(node ==NULL) {

                break;

            }

            Dl_info info = {0};

            dladdr(node->pc, &info);//将pc赋值给info

            printf("%s \n",info.dli_sname);

            //重复的原因是while(YES),即:循环一次会被hook一次

            NSString* name =@(info.dli_sname);

            free(node);


        //

        BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];

        NSString* symbolName = isObjc ? name : [@"_"stringByAppendingString:name];

        //是否去重??

        [symbolNames addObject:symbolName];

    }

    //取出来是反的,所以需要反转数组

    //反向数组

    //    symbolNames = (NSMutableArray*)[[symbolNames reverseObjectEnumerator] allObjects];

    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:@"lvjianxiong.order"];

    //文件内容

    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];

    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];

}

4、在首页的touchesBegan方法中调用获取符号文件

-(void)touchesBegan:(NSSet *)toucheswithEvent:(UIEvent*)event{

    [self getSymbolFile];

}

此处也可以放入到其他地方,方便的地方即可,只是为了方便获取符号文件,一般来说,是第一个渲染的界面

5、拷贝文件,放入指定位置,并配置路径

一般将该文件放入主项目路径下,并在Build Settings --> Order File中配置./lvjianxiong.order

配置Order File


经过二进制重排,启动速度可提升15%左右

另外:Clang插桩只需要使用一次,所以获取到.order后,直接删除上面代码即可

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

推荐阅读更多精彩内容