APP启动流程

众所周知,我们的项目打包出来的是.ipa文件,IPA文件包括:
1、App binary (Mach-o)文件
2、Bundle resource(Images,HTML,etc.)
3、.mobileprovision 描述文件
4、_CodeSignature 签名文件

Mach-O文件

Mach-o文件主要包括以下几种文件类型

  • Executable:应用的主要二进制
  • Dylib:动态链接库
  • Bundle:不能被链接,只能在运行时使用dlopen加载
  • Image:包含Executable、Dylib和Bundle
  • Framework:包含Dylib,资源文件和头文件的文件夹
Mach-O镜像文件(image)格式
1608564545299.jpg
  • Mach64 Header
  • Load Commands
  • Section64

几乎所有的Mach-O文件都包含3个段:_TEXT、_DATA和_LINKEDIT

  • __TEXT 包含 Mach header,被执行的代码和只读常量(如C 字符串)。只读可执行(r-x)。
  • __DATA 包含全局变量,静态变量等。可读写(rw-)。
  • __LINKEDIT 包含了加载程序的『元数据』,比如函数的名称和地址。只读(r–)

App启动

一般而言,App 的启动主要包括三个阶段:
1、main() 函数执行前pre-main。
自身App可执行文件的加载和系统dylib(动态链接库)
2、main() 函数执行后。
AppDelegate类中的didFinishLaunchingWithOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示
3、首屏渲染完成后。

04.png

05.png

动态链接库dyld

动态链接库的加载过程主要由dyld来完成。
1、系统读取App的程序可执行文件(Mach-O文件),加载dyld动态连接器
2、dyld初始化环境,加载程序的依赖库(动态库)并链接,rebase、bing
3、runtime初始化,依赖库初始化-程序可执行文件初始化-类初始化-load
4、dyld返回main函数地址,main函数被调用

dyld加载过程

  • Load dylibs image
    加载应用所依赖的动态库的Mach-o文件(减少非系统库的依赖、合并非系统库)
  • Rebase/Bind image
    1、ASLR(地址空间布局随机化)技术下,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定。
    2、rebase修复的是指向当前镜像内部的资源指针; 而bind指向的是镜像外部的资源指针。lazyBinding当第一次调用这个方法的时候再实施binding,通过dyld_stub_binder这个符号来做。
    (减少objc类的数量,减少selecor数量)


    6666.png
7777.png
  • Objc setup


    888.png

    dyld在binding操作结束之后,读取二进制文件的 DATA 段内容,找到与 objc 相关的信息
    注册 Objc 类
    确保 selector 的唯一性
    读取 protocol 以及 category 的信息

  • initializers
    以上三步属于静态调整,都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和栈中写入内容。
    1、 Objc的+load()函数
    2、 C++的构造函数属性函数
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_2_images, load_images, unmap_image);
}

总结dyld流程:
1、 dyld开始将程序二进制文件初始化
2、 交由ImageLoader读取image,其中包含了我们的类、方法等各种符号
3、 由于runtime向dyld绑定了回调,当image加载到内存后,dyld会通知runtime进行处理
4、 runtime接手后调用mapimages做解析和处理,接下来loadimages中调用 callloadmethods方法,遍历所有加载进来的Class,按继承层级依次调用Class的+load方法和其 Category的+load方法
整个事件由dyld主导,完成运行环境的初始化后,配合ImageLoader 将二进制文件按格式加载到内存,动态链接依赖库,并由runtime负责加载成objc 定义的结构,所有初始化工作结束后,dyld调用真正的main函数。

启动优化方案

pre-main阶段优化

  • 减少非系统库的依赖
  • 合并非系统库
  • 减少Objc类数量, 减少selector数量
  • 减少C++虚函数数量
  • 删除未使用的本地变量;
  • 删除未使用的参数;
  • 删除未使用的值。

main阶段优化

main() 函数开始执行后到首屏渲染完成前只处理首屏相关的业务,其他非首屏业务的初始化、监听注册、配置文件读取等都放到首屏渲染完成后去做。
1、在版本迭代过程中,如果业务发生变化,导致相应的代码也发生变化,一般情况下我们需要把对应的旧代码和旧资源删除掉(旧资源会增加App体积,旧代码会增加执行文件的大小,进而增加Objc类数量或者selector数量);
2、尽量抽象重复,时时重构代码,一方面减少执行文件大小,另一方面方便维护。尽可能地复用UI,在添加某个功能时,先去查查我们的代码中是否已经实现了该功能,减少重复。多重构代码,使用继承或者组合等技术减少代码量;
3、在写与启动相关的业务模块时尤其要注意,看哪些逻辑可以延迟加载或者懒加载;
4、类和方法名不要太长:iOS每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的,原因还是object-c的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,object-c对象模型会把类/方法名字符串都保存下来

另外一种启动优化方案:Clang插桩实现二进制重排

你可能感兴趣的:(APP启动流程)