美文网首页
APP启动过程及优化

APP启动过程及优化

作者: 我是本人yyp | 来源:发表于2018-10-18 15:11 被阅读0次

    以下内容仅仅作为向大佬们学习中的总结和记录

    一、名词解释

    1.什么是image
    无论对于系统的动态链接库还是对于App本身的可执行文件而言,他们都算是image(镜像),而每个App都是以image(镜像)为单位进行加载的,image包括以下文件:
    <1>executable可执行文件 比如.o文件
    <2>dylib 动态链接库
    <3>bundle 资源文件

    2.什么是ImageLoader
    image 表示一个二进制文件(可执行文件或 so 文件),里面是被编译过的符号、代码等,而 ImageLoader 作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载。

    3.Mach-O指哪些文件
    Mach是一个操作系统内核,在Mach上,一种可执行文件的格式是Mach-O。苹果的大部分文件格式都是Mach-O格式。在iOS中,以下文件指的是Mach-O:
    <1>Executable 可执行文件
    <2>Dylib 动态库
    <3>Bundle
    <4>Image
    <5>Framework

    Mach-o结构如图:


    image.png

    可以通过命令行otool -l XXX来看看Mach-o内部信息


    image.png
    Header头部 ,包含可以执行的CPU架构, 比如Mac的 PPC, PPC64, IA-32, x86-64,iOS的arm系列。
    magic,是mach-o文件的魔数,0xfeedface代表的是32位,0xfeedfacf代表64位

    cputype和cupsubtype代表的是cpu的类型和其子类型
    filetype,文件类型
    ncmds 指的是加载命令(load commands)的数量
    sizeofcmds 表示load commands的总字节大小

    Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式。
    cmd 是load command的类型,LC_SEGMENT的含义是(将文件中的段映射到进程地址空间)
    cmdsize 代表load command的大小
    segname 16字节的段名字,当前是__PAGEZERO
    vmaddr 段的虚拟内存起始地址
    vmsize 段的虚拟内存大小
    .fileoff 段在文件中的偏移量
    filesize 段在文件中的大小
    maxprot,initprot 最高内存保护和初始内存保护
    nsects段中包含section的数量


    image.png

    Data,可以拥有多个段(segment),每个段可以拥有零个或多个区域(section)。每一个段(segment)都拥有一段虚拟地址映射到进程的地址空间。大部分包含以下三个段:__TEXT 代码段,只读,包括函数,和只读的字符串
    __DATA 数据段,读写,包括可读写的全局变量等
    __LINKEDIT 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。
    sectname 第一个是__text ,就是主程序代码
    segname 该section所属的 segment名
    addr 该section在内存的启始位置
    size 该section的大小
    offset 该section的文件偏移
    align 字节大小对齐
    reloff 重定位入口的文件偏移
    nreloc 需要重定位的入口数量

    二、启动过程上

    APP启动分为热启动和冷启动
    启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。

    t(App总启动时间) = t1(main()之前的加载时间) + t2(main()之后的加载时间)

    t1 =自身App可执行文件(.o文件的集合)的加载和系统dylib(动态链接库)。动态链接库包括:iOS 中用到的所有系统 framework,加载OC runtime方法的libobjc,系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。

    具体加载顺序是先加载可执行文件,然后加载dyld,是个加载动态链接库的库。dyld从可执行文件的依赖库开始加载,递归加载所有的依赖库

    如何加载动态库?

    在Xcode中,可以通过设置环境变量来查看App的启动时间,DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS

    <1>load dylibs

    dyld会首先读取mach-o文件的Header和load commands。 �接着就知道了这个可执行文件依赖的动态库。例如加载动态库A到内存,接着检查A所依赖的动态库,就这样的递归加载,直到所有的动态库加载完毕。通常一个App所依赖的动态库在100-400个左右,其中大多数都是系统的动态库,它们会被缓存到dyld shared cache,这样读取的效率会很高。

    针对这一步骤的优化有:
    减少非系统库的依赖
    合并非系统库
    使用静态资源,比如把代码加入主程序

    <2>Rebase&Bind

    为什么要rebase?

    有两种主要的技术来保证应用的安全:ASLR和Code Sign。

    ASLR的全称是Address space layout randomization,翻译过来就是“地址空间布局随机化”。App被启动的时候,程序会被映射到逻辑的地址空间(CPU的虚拟地址,从物理地址映射到虚拟地址),这个逻辑的地址空间有一个起始地址,而ASLR技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函数的地址。

    在进行Code sign的时候,加密不是针对于整个文件,而是针对于每一个Page的。dyld进行加载的时候,是对每一个page进行独立的验证。

    之所以要rebase是因为刚刚提到的ASLR使得地址随机化,导致起始地址不固定,另外由于Code Sign,导致不能直接修改Image。Rebase的时候只需要增加对应的偏移量即可。待Rebase的数据都存放在__LINKEDIT中。

    bind指向的是镜像外部的资源指针。
    rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO。bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算。

    优化该阶段的关键在于减少__DATA segment中的指针数量。我们可以优化的点有:
    减少Objc类数量, 减少selector数量
    减少C++虚函数数量
    转而使用swift stuct(其实本质上就是为了减少符号的数量)

    Objc setup

    Objective C是动态语言,所以在执行main函数之前,需要把类的信息注册到一个全局的Table中。同时,Objective C支持Category,在初始化的时候,也会把Category中的方法注册到对应的类中,同时会唯一Selector。

    initializers

    以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和栈中写入内容。 在这里的工作有:
    Objc的+load()函数
    C/C++静态初始化对象和标记为attribute(constructor)的方法

    总结一下:对于main()调用之前的耗时我们可以优化的点有:
    减少不必要的framework,因为动态链接比较耗时
    check framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查
    合并或者删减一些OC类,关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类如下:
    删减一些无用的静态变量
    删减没有被调用到或者已经废弃的方法
    将不必须在+load方法中做的事情延迟到+initialize中
    尽量不要用C++虚函数(创建虚函数表有开销)

    三、启动过程下

    t2 = main方法执行之后到AppDelegate类中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法执行结束前这段时间。
    主要是构建第一个界面,并完成渲染展示。
    大部分项目会在该方法里初始化第一个页面,那么第一个页面的viewdidload时间就会算t2的时间中

    大部分情况下我们都会把界面的初始化过程放在viewDidLoad,但是这个过程会影响消耗启动的时间。特别是在类似TabBarController这种会嵌套childViewController的ViewController的情况,它也会把部分children也初始化,因此各种viewDidLoad会递归的进行。

    主要优化方法
    尽量减少初始页面viewdidload方法中的业务

    四、优化总结

    1.移除不需要的动态库
    2.移除不需要的类
    一个叫做fui(Find Unused Imports)的开源项目能很好的分析出不再使用的类.https://github.com/dblock/fui
    3.合并功能类似的类和扩展
    由于Category和ObjC的动态绑定有很强的关系,所以实际上分类是比较占用启动时间的。尽量合并一些分类,会对启动有一定的优化作用。
    4.压缩图片
    5.优化applicationdidfinishlaunching
    6.优化rootviewcontroller加载

    参考文章
    https://blog.csdn.net/Tencent_Bugly/article/details/77363817?locationNum=1&fps=1
    https://blog.csdn.net/u011452278/article/details/54966682

    相关文章

      网友评论

          本文标题:APP启动过程及优化

          本文链接:https://www.haomeiwen.com/subject/jgojzftx.html