美文网首页iOS程序员iOS Developer
WWDC关于APP启动的建议

WWDC关于APP启动的建议

作者: ChinaChong | 来源:发表于2018-12-21 23:01 被阅读34次

    一些概念

    Mach-O是运行时产生的可执行文件的文件类型
    • Image:

      • Executable:程序的主二进制文件
      • Dylib:动态库
      • Bundle:是一种特殊的Dylib,是不能进行链接的,只能在运行时用dlopen()函数打开。
    • Framework:Dylib+储存该Dylib需要的资源、头文件的目录结构

    Mach-O Image File被分割成几个段
    引用自WWDC2016
    • _TEXT:头文件,代码,只读常量
    • _DATA:所有可读写内容(全局变量、静态变量等等)
    • _LINKEDIT:储存关于如何加载程序的“元数据”

    每个段都是page size的倍数,图中_TEXT占有三个page,arm64一个page size是16KB,其它的是4KB

    虚拟内存把每一个进程地址映射到物理内存RAM中:
    • page错误
    • 多个进程中出现的相同的RAM page
    • 文件回溯page:mmap()、延迟读取(lazy reading)
    • Copy-On-Write(COW)
    • 脏page和干净page
    • Permissions:rwx
    安全:

    ASLR(Address space layout randomization)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。据研究表明ASLR可以有效的降低缓冲区溢出攻击的成功率,如今Linux、FreeBSD、Windows等主流操作系统都已采用了该技术。

    ASLR:
    • 地址空间随机布局
    • 镜像(Images)加载在随机的地址

    代码签名

    • 每一个page拥有的内容
    • page内的哈希验证

    exec()到main(),内核让你的App在随机的地址开始运行
    什么是Dyld?
    • Dyld是动态加载器,内核加载的辅助程序
    • 程序从Dyld开始执行
    • Dyld运行在进程中
    • Dyld负责加载动态依赖库
    • Dyld拥有与App相同的权限

    Main()函数之前的五个阶段

    Dyld主导的五个阶段:
    引用自WWDC2016
    一、Load dylibs:

    动态映射所有的动态依赖库。

    1. 解析动态依赖库(dylib)的列表
    2. 找到所有需要的mach-o(dylib)文件
    3. 打开并读取每一个找到的文件
    4. 验证这些文件是不是mach-o文件
    5. 找到它的编码签名,在内核里对它进行注册
    6. 给每一个分段调用映射

    现在,所有App指向的动态依赖库都被递归加载,App通常需要加载100-400个动态库,大多数是OS(操作系统)的动态库

    二、Rebase

    遍历所有内部数据指针为他们添加一个滑动值。这些指针的位置都被编码在__LINKEDIT段里。

    1. 调整所有镜像内的指针,添加一个slide偏差值。
    Slide = actual_address - preferred_address
    
    1. 出于安全考虑,引入了 ASLR,全称 Address Space Layout Randomization

    大概意思就是镜像(dylib)会加载在随机的地址上,和actual_address会有一个偏差(slide),dyld需要修正这个偏差,来指向正确的地址。

    1. Rebase+Bind+ObjC大多数时间在做修复:
      • 代码签名意味着命令不能被修改
      • 代码不能被加载到任何地址上,而且永远不能被修改
      • 所有的修复都发生在__DATA数据段
    三、Bind

    遍历查询符号表,设置指向镜像外部的指针。

    1. 所有在其它动态库引用的东西都会符号化
    2. Dyld需要找到所有符号名
    3. 会比Rebasing进行更多的计算
    四、ObjC

    通知 runtime 准备镜像、OC类的注册、偏移ivar的地址、加载Category

    1. runtime要维护一张表,包含所有类名(Class Name)及其映射的类(Class)
    2. 完成所有OC类的定义注册
    3. 运行时改变所有ivar的偏移量
    4. 接下来在ObjC阶段可以定义分类(Category)
    5. 最后让Selector都是唯一的
    五、Initializers
    1. dyld开始调用C++静态构造函数,初始化器用来初始化那些抽象DATA

    2. 调用所有类的 +load 方法,顺序:父类->子类->Category

    3. 每个Initializers按照从下向上的顺序执行。

      为什么从下向上?

      因为当Initializers运行时,可能会调用一些dylib,我们需要确保那些dylib已经准备好被调用,所以从下开始运行Initializers,一直往上到应用类,可以很安全的调用依赖的内容。

    4. 最后Dyld调用main()函数

    小结:Dyld是一个辅助程序
    1. 加载所有的依赖库
    2. 修复所有DATA page中的指针
    3. 运行所有的初始化器(Initializers)
    4. 跳转到主函数

    优化启动时间的实践部分

    • 启动时间如果在400ms(0.4s)以内会让用户觉得启动快
    • 启动时间千万不要超过20s,否则OS将会认为你的APP进入死循环,杀死APP
    App启动都做了什么?

    在main函数之前的五个阶段之后,还要调用:

    1. main()

    2. UIApplicationMain() : 加载framework的初始化器,加载nibs

    3. applicationWillFinishLaunching

    以上8个步骤都算在这400ms内。

    热启动和冷启动
    • 热启动:App及其数据已经在内存(磁盘)中
    • 冷启动:App不在内核的缓存中

    优化方案:

    一、Load Dylib阶段
    1. 合并已有的动态库(包括framework),限制动态库(包括framework)个数效果非常明显
    2. 使用静态库代替

    3. 可以使用延迟加载,也就是使用dlopen()函数,但是dlopen()会带来细微的性能和正确性的问题。


      优化前的链接库个数
      优化前的启动时间

    原有26个framework合并成2个后,由240ms变为21ms。

    优化后的链接库个数
    优化后的启动时间
    二、Rebase和Bind阶段
    1. 减少OC元数据
      • 减少OC类的数量(不鼓励使用很多很小的类,只有一两个方法的那种)
      • 减少selector的数量
      • 减少Category的数量
    2. 减少C++虚拟函数,虚拟函数创建被称作 V表格,和OC元数据相同

    3. 避免让机器生成过多的代码,机器生成的指针非常耗内存
      • 使用偏移量代替指针
      • 标记为只读
    三、ObjC阶段

    这个阶段的优化工作已经在Rebase和Bind阶段做完了

    四、Initializers阶段

    有两种Initializers:显式和隐式

    显式:
    1. +load
    方案:使用 +initiailize 代替
    1. C/C++的 __attribute__((constructor))

    方案:使用site initializers

    • 使用dispatch_once()
    • 使用pthread_once()
    • 使用std::once()

    隐式:大部分是C++的全局变量带来的非默认初始化器
    1. 使用site initializers
    2. 只设置简单的值
    3. 不要在初始化器中调用dlopen()
    4. 不要在初始化器中创建线程

    参考

    WWDC2016-406

    相关文章

      网友评论

        本文标题:WWDC关于APP启动的建议

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