18、iOS底层探索-启动优化

1、pre-main

iOS应用的启动时间分为main函数之前与main函数之后,main之前的部分叫做 pre-main

pre-main耗时情况

在iOS15之前,我们可以通过设置 Edit Scheme 中的设置 Environment Variables 增加 DYLD_PRINT_STATISTICS 来在控制台打印pre-main过程的耗时情况 image.png

Total pre-main time: 1.8 seconds (100.0%)
         dylib loading time: 526.41 milliseconds (28.1%)
        rebase/binding time: 165.85 milliseconds (8.8%)
            ObjC setup time: 324.80 milliseconds (17.3%)
           initializer time: 853.94 milliseconds (45.6%)
           slowest intializers :
             libSystem.B.dylib :  10.44 milliseconds (0.5%)
    libMainThreadChecker.dylib :  58.23 milliseconds (3.1%)
          libglInterpose.dylib : 318.94 milliseconds (17.0%)
                  AFNetworking :  39.55 milliseconds (2.1%)
         NELivePlayerFramework :  62.94 milliseconds (3.3%)
               XXXXX : 369.68 milliseconds (19.7%)
复制代码
名称 作用 说明
dylib loading 动态库的载入 动态库的载入存在耗时;动态库会存在依赖关系;系统动态库存在于共享缓存(自定义动态库不是)
rebase/binding 重定位符号和符号绑定 rebase:ASLR+偏移地址;binding:将 外部符号 与其 来源库中的实现地址 进行绑定的过程
ObjC setup 注册 OC 应用启动时,系统会生成 分类 的两张表,它们会注册并插入到这两张表中,产生耗时
initializer 执行load以及C++构造函数
slowest intializers 列举出几个比较耗时的动态库 libSystem、AFNetWorking等库

优化建议

  • 苹果官方建议 自定义动态库 不超过6个,超过可进行多个动态库合并(需要源码支持,所以不能合并三方SDK)

  • 尽可能使用initialize方法代替+load方法

  • 集成fui控件可以帮助查找工程中未使用的类

  • 未使用图片检测工具,拿掉未使用的图片能减小APP打包体积

2、虚拟内存

2.1、物理内存的弊端

  • 可以跨进程访问,数据不安全
  • 将整个程序加载到内存,导致内存浪费

2.2、虚拟内存的特点

  • iOS 系统中,将程序分页处理,一页为16KB(假设不设置分页,有100M空间,程序A占30M,程序B占40M,程序C占50M,那么A、B运行后再运行C,剩余30M不够C使用,就需要将A或B整个干掉一个腾出空间)

  • 虚拟地址和物理地址的映射表,也称之为页表,页表存储在内存中

  • 一个进程中,只有部分功能是活跃的,所以只需要将进程中活跃的部分放入物理内存,避免物理内存的浪费

  • iOS系统中,当进程被加载时,虚拟内存中会开辟4G的空间(假空间),用于存放MachO、堆区、栈区。但物理内存中,并未真的分配;当数据加载到页表中,系统会配合CPU进行地址翻译,然后载入到物理内存中;地址翻译的过程,由CPU上的内存管理单元(MMU)完成

2.2.1、缺页中断(Page Fault)
  • 当程序 访问未被缓存的内存页时(功能未使用而未被载入物理内存),就会触发缺页中断

  • 缺页中断会将当前进程阻塞掉,此时需要先将未被缓存的虚拟页载入到物理内存,然后再寻址,进行读取

2.2.2、页面置换
  • 物理内存的空间是有限的,当内存中没有空间时,操作系统会从选择合适的物理内存页驱逐回 磁盘,为新的内存页让出位置,选择待驱逐页的过程在操作系统中叫做页面置换
2.2.3、ASLR
  • 防止虚拟内存从0开始读取数据不安全而加的随机偏移量

3、二进制重排

  • 在冷启动过程中,数据都还没有加载进内存,因此会产生大量的缺页中断(Page Fault
  • Xcode 菜单中,选择Product --> Profile --> Instruments -->

image-2.png

运行测试项目,当第一个界面出来后即可停止,搜索main thread image.png 小测试项目,启动时缺页中断 564 次,耗时 200 毫秒,如果是大型项目,会消耗更多的时间

3.1、查看代码执行顺序

  • 首先将 Build Setting 中的Write Link Map File改为 YES image.png

  • 编译后在工程的Build目录下,找到LinkMap文件 image.png image.png

  • LinkMap 文件,保存了项目在编译链接时的符号顺序,以方法/函数为单位排列 image.png

  • 文件编译顺序是 XcodeBuild Phases --> Compile Sources 的文件排列顺序 image.png

  • 文件中方法/函数的符号顺序,就是代码的书写顺序

3.2、二进制重排原理

  • 默认情况,在应用启动时,会加载大量与启动时无关的代码,导致Page Fault的次数增长,影响启动时间;若我们 将启动时需要的方法/函数排列在最前面,就能大大降低 Page Fault 次数,从而提升应用的启动速度

3.3、二进制重排操作

  1. 在项目根目录创建.order文件(这里随便起了个名叫pagefault.order) image.png

  2. 按自己想要的顺序写入启动是需要的方法/函数 image.png

  3. .order 文件配置进工程:Build Setting --> Order File --> 填入./pagefault.order image.png

  4. 再编译后打开 LinkMap 文件查看 image.png

    可以看到按 .order 文件书写的 方法/函数 顺序,对应的 方法/函数 被提到最前边加载进内存,因此只要找的启动时需要的 方法/函数 越准确越全面,缺页中断 Page Fault 的机率就越低

总结

  • 启动优化方法

    • 减少自定义动态库个数
    • 减少项目中未使用的类和方法
    • 使用二进制重排将启动时需要的符号方法提前加载到内存
  • rebase / binding

    • rebase:将虚拟内存因ASLR而产生的误差修复,将指针指向正确的物理地址借助 MMU 翻译读取物理内存中的数据
    • binding:将 外部符号 与它所在的库中具体实现该符号功能的地址进行关联绑定
  • 确定程序启动时需要调用哪些符号需要借助Clang插桩

猜你喜欢

转载自juejin.im/post/7109861095405256735