iOS操做系统-- App启动流程分析与优化

背景知识:

  • mach-o文件为基于Mach核心的操做系统的可执行文件、目标代码或动态库,是.out的代替,其提供了更强的扩展性并提高了符号表中信息的访问速度,
  • 符号表,用于标记源代码中包括标识符、声明信息、行号、函数名称等元素的具体信息,好比说数据类型、做用域以及内存地址,iOS符号表在dSYM文件中
  • 程序构建过程:编译分三步走,对 源文件进行预处理(processing),处理预编译指令,生成.i文件,下一步进行编译,进行词法分析(lex工具识别词法规则语义表)、语法分析和语义分析生成.s汇编文件,最后进行汇编,生成二进制目标文件.o。目标文件再进行连接器连接,造成可执行文件.a或mach-o文件。
  • 连接分为动态连接和静态连接,静态连接会将全部目标文件.o所有内容连接到执行文件中,若是另外的执行文件须要其中的功能,也必须所有收录。动态连接为了解决这样的空间浪费问题,只将函数信息连接加入执行文件
  • dyld是加载动态连接库的库,该库在加载可执行文件的时候,递归加载所须要的全部动态库。动态库包括iOS操做系统的系统framework,oc的runtime系统libobjc,系统级别的库libSystem,例如libdispatch(GCD)、libsystem_block(Block)

App启动大体流程

对于一个可执行文件来讲,它的加载过程是: 分为两大部分:缓存

  1. pre-main 指的是操做系统开始执行一个可执行文件,并完成进程建立、执行文件加载、动态连接、环境配置
  2. main 指的是从加载main函数入口之后,到app delegate完成加载回调的过程

操做系统加载App可执行文件

操做系统加载可执行文件,经过fork(建立一个进程)指令在新的空间内来执行可执行文件,加载依赖的可执行文件(mach-o)文件,定位其内部与外部指针引用,例如字符串与函数,执行声明为attribute((constructor))的C函数,加载扩展(Category)中的方法,C++静态对象加载,调用ObjC的+load函数bash

基本流程:多线程

App 开始启动后,系统首先加载可执行文件(自身 App 的全部 .o 文件的集合),而后加载动态连接器 dyld,dyld 是一个专门用来加载动态连接库的库。 执行从 dyld 开始,dyld 从可执行文件的依赖开始,递归加载全部的依赖动态连接库。 动态连接库包括:iOS 中用到的全部系统 framework,加载 OC runtime 方法的 libobjc,系统级别的 libSystem,例如 libdispatch(GCD) 和 libsystem_blocks (Block)。app

dyld加载动态库

动态连接库的加载过程主要由dyld来完成,dyld是苹果的动态连接器。框架

  1. 系统先读取App的可执行文件(Mach-O文件)里的mach-o headers
  2. dyld去初始化运行环境,从里面得到动态依赖,开启缓存策略,加载程序相关依赖库(其中也包含咱们的可执行文件),并对这些库进行连接,最后调用每一个依赖库的初始化方法,在这一步,runtime被初始化。当全部依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化。
  3. 检查和确认符号表的是否存在和正确
  4. Map全部mach-o文件,用来总体统计变量声明、函数调用等信息
  5. 进行bind操做,对从其余库的引用的符号、函数等,进行其内存地址进行修正绑定
  6. 进行rebase操做,对自身库内部的引用进行修正
  7. 进行runtime系统初始化,会对项目中全部类进行类结构初始化,而后调用全部的load方法。
  8. 最后dyld返回main函数地址,main函数被调用,咱们便来到了熟悉的程序入口。 当加载一个 Mach-O 文件 (一个可执行文件或者一个库) 时,动态连接器首先会检查共享缓存看看是否存在其中,若是存在,那么就直接从共享缓存中拿出来使用。每个进程都把这个共享缓存映射到了本身的地址空间中。这个方法大大优化了 OS X 和 iOS 上程序的启动时间。

Mach-O 镜像文件

官方文档: developer.apple.com/library/arc…异步

Mach-O是OS X中二进制文件的本机可执行格式,是传送代码的首选格式。可执行格式肯定二进制文件中的代码和数据被读入内存的顺序。代码和数据的排序会影响内存使用和分页活动,从而直接影响程序的性能。段的大小经过其包含的全部段中的字节数来度量,并向上舍入到下一个虚拟内存页边界。 Mach-O二进制文件被组织成segements。每一个segement包含一个或多个部分。每一个部分都有不一样类型的代码或数据。segement始终从页面边界开始,但section不必定是页面对齐的。所以,segement终是4096字节或4千字节的倍数,其中4096字节是最小大小。 Mach-O可执行文件的segement和section根据其预期用途命名。segement名称的约定是使用以双下划线开头的全大写字母(例如,TEXT); section名称的约定是使用以双下划线开头的全小写字母(例如, text)。 Mach-O可执行文件中有几个可能的segements,但只有两个与性能有关:__TEXT段和__DATA段。函数

The __TEXT Segment: Read Only __TEXT segment是包含可执行代码和常量数据的只读区域。按照惯例,编译器工具建立具备至少一个只读__TEXT segment的每一个可执行文件。因为该段是只读的,所以内核能够将__TEXT segment直接从可执行文件映射到内存中一次。当segment被映射到内存时,它能够在全部进程之间共享其内容。 (这主要是框架和其余共享库的状况。)只读属性还意味着构成__TEXT segment的页面永远没必要保存到后备存储。若是内核须要释放物理内存,它能够丢弃一个或多个__TEXT页面,并在须要时从磁盘从新读取它们。 __TEXT segment的主要部分,sections分布工具

  • __text 已编译的可执行文件的机器代码
  • __const 通常的常量数据
  • __cstring 文字字符串常量(源代码中的引用字符串)
  • __picsymbol_stub 动态连接器(dyld)使用的与位置无关的代码存根例程

The __DATA Segment: Read/Write __DATA segment 包含可执行文件的很是量变量。该 segement 是可读写的,由于它是可写的,因此对于与库连接的每一个进程,逻辑上复制静态库或其余动态共享库的__DATA段。当内存页面可读写时,内核会使其变为copy-on-write。此技术能够作到,动态库是在内存中共享的,能够被其余各个进程访问,但由于__DATA Segment是可读可写的,就会经过某一进程对共享的_DATA Segment有写操做的时候,再进行单独的_DATA内存空间复制。 __DATA segment 有许多部分,其中一些仅由动态连接器使用。下面 列出了能够出如今__DATA segment 中的一些更重要的部分。有关段的完整列表,请参阅Mach-O运行时体系结构。布局

  • __data 初始化的全局变量(例如int a = 1;或static int a = 1;)。
  • __const 须要重定位的常量数据(例如,char * const p =“foo”;)
  • __bss 未初始化的静态变量(例如,static int a;)。
  • __common 未初始化的外部全局变量(例如,int a;外部功能块)。
  • __dyld 占位符部分,由动态连接器使用。
  • __la_symbol_ptr lazy符号指针。可执行文件调用的每一个未定义函数的符号指针。
  • __nl_symbol_ptr 非lazy符号指针。可执行文件引用的每一个未定义数据符号的符号指针。

Mach-O 性能影响 Mach-O可执行文件的__TEXT segment和__DATA segment的组成与性能有直接关系。优化这些sections的技术和目的是不一样的。可是,它们的共同目标是:提升内存使用效率。性能

最典型的Mach-O的文件由可执行代码组成,在__TEXT,__text当中。如__TEXT segment,该__TEXT是只读的,并直接映射到可执行文件,因此若是内核须要回收某些__text页面占用的物理内存,就没必要将页面保存到back store再将其分页。它只须要释放内存,并在后面代码引用的时候从磁盘从新读回。虽然这比交换内存分页的成本低,由于这只是一个磁盘访问,而不是两个内存分页的交换 , 但这仍然很损耗性能,特别是若是必须从磁盘从新建立许多页面。

对于这种状况的改进,是经过程序从新排序来改进代码的引用位置,如改进参考位置中所述。该技术将方法和功能组合在一块儿,具体取决于它们的执行顺序,调用频率以及它们相互调用的频率。若是__text部分组中的页面以这种方式逻辑上起做用,则它们不太可能被屡次释放和读回。例如,若是将全部启动时初始化函数放在一个或两个页面上,则在发生初始化后没必要从新建立页面。

与__TEXT段不一样,__DATA能够写入段,所以段中的页面__DATA不可共享。框架中的很是量全局变量可能会对性能产生影响,由于与框架连接的每一个进程都会得到这些变量的副本。解决这个问题的主要解决办法是尽量多的非恒定的全局变量尽量转移到__TEXT,__const经过宣布他们部分const。减小共享内存页面描述了此技术和相关技术。这一般不是应用程序的问题,由于应用程序中的__DATA部分不与其余应用程序共享。

编译器将不一样类型的很是量全局数据存储在段的不一样部分中__DATA。这些类型的数据是未初始化的静态数据和符号与未声明的“暂定定义”的ANSI C概念一致extern。未初始化的静态数据位于__bss段的__DATA部分中。暂定的符号在__common 该__DATA部分。

该 ANSI C和 C ++标准指定系统必须将未初始化的静态变量设置为零。(未初始化的其余类型的未初始化数据。)因为未初始化的静态变量和临时定义符号存储在单独的部分中,所以系统须要以不一样方式对待它们。可是当变量位于不一样的部分时,它们更有可能最终出如今不一样的内存页面上,所以能够单独进行交换,从而使代码运行速度变慢。如减小共享内存页面中所述,这些问题的解决方案是在段的一个部分中合并不是常量全局数据__DATA。

ObjC Runtime

dyld的加载过程会初始化Runtime系统,在此阶段,有至关多的优化工做能够作

这过程包括:

  1. 全部类型的定义和注册,Objective-C的类不是编译器决定的,是运行时动态载入到全局表中的
  2. 非脆弱的ivars变量抵消更新,修改实例变量的内存地址偏移问题
  3. 分类替换并添加到方法列表中,将分类中的方法加载到方法列表中
  4. 确认选择器全局惟一

Initializers 阶段

在Runtime系统加载之后,开始进行初始化

  1. Objc的+load()函数
  2. C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
  3. 非基本类型的C++静态全局变量的建立(一般是类或结构体)(non-trivial initializer) 好比一个全局静态结构体的构建,若是在构造函数中有繁重的工做,那么会拖慢启动速度

pre-main阶段分析

从上面能够得出如下几个结论,影响该阶段启动时间的因素以下:

  1. Mach-O可执行文件的加载和内存从新分配规划,对于其segment和section进行虚拟内存的分页管理的调度
  2. dyld动态连接内存中的公共镜像,在运行时进行检查共享数据和连接调用
  3. Runtime的初始化,包括class注册、category加载、变量对齐等
  4. C++静态对象和全局变量的加载
  5. ObjeC全部load函数的调用加载

优化措施:

  1. 减小ObjC的类膨胀问题,清理没有使用的类,合并松散无用的类
  2. 减小静态变量的声明和初始化的分离
static int x;
static short conv_table [128];
//更换为
static int x = 0;
static short conv_table [128] = {0};
复制代码

减小静态变量的使用 3. 减小符号表的导出 经过设置-exported_symbols_list或-unexported_symbols_lis来限制符号表的导出,从而减小dyld的工做量 4. 去除没有使用的动态库依赖,明确所依赖的frameworks是require仍是optional,optional会动态进行额外检查 5. 删除没有用的方法 6. 减小+load函数的实现,并减小在其中操做的逻辑 7. 对某些常常调用的代码进行二进制化,生成静态库,多使用静态库代替动态库,将多个静态库框架,集中制做成静态framework,从而可以减小dyld的连接工做 关于冷启动和热启动的不一样以下:

main阶段

从上图能够获得,影响main阶段的启动时间因素是:

  1. AppDelegate代理的加载生命周期回调
  2. Application Window的布局、绘制和加载
  3. RootViewController的加载 优化点:
  4. 压缩和减少启动图片
  5. 尽可能不使用storyboard或者是nib来布局rootViewController
  6. 在didFinishLaunchingWithOptions阶段,尽量减小阻塞代码的执行,能够利用多线程进行加载逻辑的处理,注意多线程对主线程同步阻塞可能形成的黑屏问题
  7. 将非同步需求的初始化逻辑进行异步加载
相关文章
相关标签/搜索