iOS 启动优化原理

1、虚拟内存 & ASLR

在早期计算机中数据是直接通过物理地址访问的,这就造成了下面两个问题

  • 1、内存不够用
  • 2、数据安全问题

内存不够 --- > 虚拟内存

虚拟内存就是通过创建一张物理地址和虚拟地址的映射表来管理内存,提高了CPU利用率,使多个进程可以同时/按需加载

  • 在iOS中,每个进程都有独立的虚拟内存,存放物理内存中,其地址是从0开始的,大小固定4G,每个虚拟内存又会按页划分,每页16K以页为单位加载,每个进程是相互独立的,保证进程间的数据安全
  • 当一个进程只有部分功能使用时,系统会自动将使用部分加载到物理内存中
  • CPU在进行数据访问时,会先访问虚拟内存,通过虚拟内存和物理内存的映射关系,去对应的物理内存中查找,在CPU上有专门处理映射的硬件
  • 当CPU访问数据时,如果虚拟内存中的数据没有物理内存绑定,会发生缺页异常(pagefault),会阻塞当前进程,直到数据加载到物理内存中,虚拟内存和物理内存绑定
  • 当内存条中的内存全部用完后,系统会将新的数据覆盖到很久没使用的内存上
    虚拟内存和物理内存

数据安全 --- > ASLR

因为虚拟内存的起始地址(0x000000)和大小(4G)是固定的,这就意味着我们的数据地址也是固定的,所以在iOS4.3引进了ASLR
ASLR:英文全称Address Space Layout Randomization,也叫地址空间配置随机加载,是一种针对缓冲区溢出安全保护技术。通过对堆、栈、共享库映射等线性区布局的随机化,增强了攻击者找到目标物理内存的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。
由于ASLR的存在,导致可执行文件和动态链接库在虚拟内存中的地址每次都是不固定的,所以需要在编译时来修复镜像中的资源指针,来指向正确的地址。即正确的内存地址 = ASLR地址 + 偏移值

2、Mach-O

Mach-O文件是Mach Object文件格式的缩写,它是用于可执行文件、动态库、目标代码的文件格式。作为a.out格式的替代,Mach-O格式提供了更强的扩展性,以及更快的符号表信息访问速度
我们可以通过工具MachOView来查看Mach-O文件具体信息

对于OS X 和iOS来说,Mach-O是其可执行文件的格式,主要包括以下几种文件类型

  • Executable:可执行文件
  • Dylib:动态链接库
  • Bundle:无法被链接的动态库,只能在运行时使用dlopen加载
  • Image:指的是Executable、Dylib和Bundle的一种
  • Framework:包含Dylib、资源文件和头文件的集合

Mach-O文件格式

Mach-O镜像文件

一个完整的Mach-O文件主要分为三大部分:

  • Header:Mach-O的cpu架构,文件类型以及加载命令等信息
  • Load Comands:文件中数据的具体组织结构,不同数据使用不同的加载命令
  • Data:数据中的每个段(Segment)的数据保存在这里,每个段有一个或多个部分,存放具体的数据、代码、符号表、动态符号表等
Header

Header中包含了整个Mach-O文件中的关键信息,可以使CPU快速知道整个Mach-O的基本信息,决定了一些基础架构、系统类型、指令条数等信息。针对32位和64位架构的cpu,分别使用了mach_headermach_header_64结构体来描述Mach-O头部,mach_header_64结构体相比于mach_header只是多了一个reserved保留字段

/*
 - magic:0xfeedface(32位) 0xfeedfacf(64位),系统内核用来判断是否是mach-o格式
 - cputype:CPU类型,比如ARM
 - cpusubtype:CPU的具体类型,例如arm64、armv7
 - filetype:由于可执行文件、目标文件、静态库和动态库等都是mach-o格式,所以需要filetype来说明mach-o文件是属于哪种文件
 - ncmds:sizeofcmds:LoadCommands加载命令的条数(加载命令紧跟header之后)
 - sizeofcmds:LoadCommands加载命令的大小
 - flags:标志位标识二进制文件支持的功能,主要是和系统加载、链接有关
 - reserved:保留字段
 */
struct mach_header_64 {
    uint32_t    magic;      /* mach magic number identifier */
    cpu_type_t  cputype;    /* cpu specifier */
    cpu_subtype_t   cpusubtype; /* machine specifier */
    uint32_t    filetype;   /* type of file */
    uint32_t    ncmds;      /* number of load commands */
    uint32_t    sizeofcmds; /* the size of all the load commands */
    uint32_t    flags;      /* flags */
    uint32_t    reserved;   /* reserved */
};

filetype主要记录Mach-O的文件类型,常用的有以下几种

#define MH_OBJECT   0x1     /* 目标文件*/
#define MH_EXECUTE  0x2     /* 可执行文件*/
#define MH_DYLIB    0x6     /* 动态库*/
#define MH_DYLINKER 0x7     /* 动态链接器*/
#define MH_DSYM     0xa     /* 存储二进制文件符号信息,用于debug分析*/
MachOView中的Header
Load Commands

Load Commands主要用于加载指令,例如动态链接器的位置、程序的入口、依赖库的信息、代码的位置、符号表的位置等等。其大小和数目在Header中已确定,在Mach.h中的定义如下

/*
 load_command用于加载指令
 - cmd 加载命令的类型
 - cmdsize 加载命令的大小
 */
struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};
Mach-O文件中Load Commands

其中LC_SEGMENT_64的类型segment_command_64定义如下

/*
 segment_command 段加载命令
 - cmd:表示加载命令类型,
 - cmdsize:表示加载命令大小(还包括了紧跟其后的nsects个section的大小)
 - segname:16个字节的段名字
 - vmaddr:段的虚拟内存起始地址
 - vmsize:段的虚拟内存大小
 - fileoff:段在文件中的偏移量
 - filesize:段在文件中的大小
 - maxprot:段页面所需要的最高内存保护(4 = r,2 = w,1 = x)
 - initprot:段页面初始的内存保护
 - nsects:段中section数量
 - flags:其他杂项标志位
 
 - 从fileoff(偏移)处,取filesize字节的二进制数据,放到内存的vmaddr处的vmsize字节。(fileoff处到filesize字节的二进制数据,就是“段”)
 - 每一个段的权限相同(或者说,编译时候,编译器把相同权限的数据放在一起,成为段),其权限根据initprot初始化。initprot指定了如何通过读/写/执行位初始化页面的保护级别
 - 段的保护设置可以动态改变,但是不能超过maxprot中指定的值(在iOS中,+x和+w是互斥的)
 */
struct segment_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT_64 */
    uint32_t    cmdsize;    /* includes sizeof section_64 structs */
    char        segname[16];    /* segment name */
    uint64_t    vmaddr;     /* memory address of this segment */
    uint64_t    vmsize;     /* memory size of this segment */
    uint64_t    fileoff;    /* file offset of this segment */
    uint64_t    filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t    nsects;     /* number of sections in segment */
    uint32_t    flags;      /* flags */
};
Data

Data主要用于存储具体的只读、可读写代码,例如方法、符号表、字符表、代码数据、重定向/符号绑定等。其中大多数的Mach-O文件均包含以下三个段:

  • __TEXT代码段:只读,包括函数和只读字符串
  • __DATA数据段:读写,可读写的全局变量等
  • __LINKEDIT:包含方法和变量的元数据(位置、偏移量),代码签名等信息

Data区中,section占很大比例,Section在Mach.h中是以结构体section_64(在arm64架构下)表示,其定义如下

/*
 Section节在MachO中集中体现在TEXT和DATA两段里.
 - sectname:当前section的名称
 - segname:section所在的segment名称
 - addr:内存中起始位置
 - size:section大小
 - offset:section的文件偏移
 - align:字节大小对齐
 - reloff:重定位入口的文件偏移
 - nreloc:重定位入口数量
 - flags:标志,section的类型和属性
 - reserved1:保留(用于偏移量或索引)
 - reserved2:保留(用于count或sizeof)
 - reserved3:保留
 */

struct section_64 { /* for 64-bit architectures */
    char        sectname[16];   /* name of this section */
    char        segname[16];    /* segment this section goes in */
    uint64_t    addr;       /* memory address of this section */
    uint64_t    size;       /* size in bytes of this section */
    uint32_t    offset;     /* file offset of this section */
    uint32_t    align;      /* section alignment (power of 2) */
    uint32_t    reloff;     /* file offset of relocation entries */
    uint32_t    nreloc;     /* number of relocation entries */
    uint32_t    flags;      /* flags (section type and attributes)*/
    uint32_t    reserved1;  /* reserved (for offset or index) */
    uint32_t    reserved2;  /* reserved (for count or sizeof) */
    uint32_t    reserved3;  /* reserved */
};
MachOView中TEXT和DATA
常见section

3、优化建议

启动的过程一般是指点击App图标开始到AppDelegate 的didFinishLaunching

  • 冷启动:内存中不包含App相关数据,一般可以通过重启手机来实现冷启动
  • 热启动:杀掉App进程后,数据依然存在内存中时启动

启动优化一般指在冷启动情况下主要分为两部分:

  • main函数开始之前 (pre-main): 操作系统加载App可执行文件到内存,执行一系列的加载和链接操作,也就是dyld加载过程
  • main函数开始:从main函数开始到AppDelegate 的didFinishLaunching方法执行为止,加载渲染第一个界面

main函数之前

在之前的文章已经了解过dyld加载流程
我们可以通过Edit Scheme -> Run -> Arguments ->Environment Variables->点击+添加环境变量DYLD_PRINT_STATISTICS == 1

检测启动时间

pre-main启动时间
上图中pre-main阶段总共用时1.7s
  • dylib loading:动态库加载耗时

  • rebase/binding:偏移修正和符号绑定

    • rebase:每个app的二进制文件内部的方法、函数调用,都有一个偏移地址,每次app启动,系统会分配一个ALSR,例如,二进制文件中有一个 test方法,偏移值是0x0001,而随机分配的ASLR是0x1f00,如果想访问test方法,其内存地址(即真实地址)变为 ASLR+偏移值 = 运行时确定的内存地址(即0x1f00+0x0001 = 0x1f01)
    • binding绑定就是给符号赋值的过程。方法、函数在编译时期会创建一个对应的符号指向一个随机地址,在运行时(磁盘加载到内存中,是一个镜像文件),会将真正的内存地址和符号进行绑定(即dyld将内存地址和符号绑定,也称动态库符号绑定)
  • Objc setup:oc类注册的耗时,类越多,耗时越长,swift没有这个

  • initializer:执行load和构造函数的耗时

优化
  • 少用外部动态库,苹果官方建议自定义动态库数量不要超过6个,如果超过需要进行动态库合并

  • 减少OC类使用

  • 将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数

  • 如果是swift,尽量使用struct

main函数开始后

  • 减少启动初始化的流程:能懒加载的懒加载,能延迟的延迟,能放后台初始化的放后台,尽量不要占用主线程的启动时间

  • 优化代码逻辑,去除非必要的代码逻辑

  • 启动阶段能使用多线程来初始化的,就使用多线程

  • 主UI框架尽量使用纯代码构建

  • 删除废弃的类和方法

你可能感兴趣的:(iOS 启动优化原理)