实现 iOS App 的冷启动优化

实现 iOS App 的冷启动优化当用户按下 home 键 iOS App 不会立刻被 kill 而是存活一段时间 这段时间里用户再打开 App App 基本上不需要做什么 就能还原到退到后台前的状态 我们把 App 进程还在系统中 无需开启新进程的启动过程称为热启动 而冷启动 则是指 App 不在系统进程中 比如设备重启后 或是手动杀死 App 进程 又或是 App 长时间未打开过 用户再点击启动 App 的过程

大家好,我是讯享网,很高兴认识大家。这里提供最前沿的Ai技术和互联网信息。



当用户按下 home 键,iOS App 不会立刻被 kill,而是存活一段时间,这段时间里用户再打开 App,App 基本上不需要做什么,就能还原到退到后台前的状态。我们把 App 进程还在系统中,无需开启新进程的启动过程称为热启动

冷启动则是指 App 不在系统进程中,比如设备重启后,或是手动杀死 App 进程,又或是 App 长时间未打开过,用户再点击启动 App 的过程,这时需要创建一个新进程分配给 App。我们可以将冷启动看作一次完整的 App 启动过程,本文讨论的就是冷启动的优化。

3047084-9cebc4207ea12fa2.jpg

这一步,指的是动态库加载。在此阶段,dyld 会:

  1. 分析 App 依赖的所有 dylib;
  2. 找到 dylib 对应的 Mach-O 文件;
  3. 打开、读取这些 Mach-O 文件,并验证其有效性;
  4. 在系统内核中注册代码签名;
  5. 对 dylib 的每一个 segment 调用 mmap()

一般情况下,iOS App 需要加载 100-400 个 dylibs。这些动态库包括系统的,也包括开发者手动引入的。其中大部分 dylib 都是系统库,系统已经做了优化,因此开发者更应关心自己手动集成的内嵌 dylib,加载它们时性能开销较大。

App 中依赖的 dylib 越少越好,Apple 官方建议尽量将内嵌 dylib 的个数维持在6个以内。

优化方案

  • 尽量不使用内嵌 dylib;
  • 合并已有内嵌 dylib;
  • 检查 framework 的 optionalrequired 设置,如果 framework 在当前的 App 支持的 iOS 系统版本中都存在,就设为 required,因为设为 optional 会有额外的检查;
  • 使用静态库作为代替;(不过静态库会在编译期被打进可执行文件,造成可执行文件体积增大,两者各有利弊,开发者自行权衡。)
  • 懒加载 dylib。(但使用 dlopen() 对性能会产生影响,因为 App 启动时是原本是单线程运行,系统会取消加锁,但 dlopen() 开启了多线程,系统不得不加锁,这样不仅会使性能降低,可能还会造成死锁及未知的后果,不是很推荐这种做法。)

这一步,做的是指针重定位

在 dylib 的加载过程中,系统为了安全考虑,引入了 ASLR(Address Space Layout Randomization)技术和代码签名。由于 ASLR 的存在,镜像会在新的随机地址(actual_address)上加载,和之前指针指向的地址(preferred_address)会有一个偏差(slide,slide=actual_address-preferred_address),因此 dyld 需要修正这个偏差,指向正确的地址。具体通过这两步实现:

第一步:Rebase,在 image 内部调整指针的指向。将 image 读入内存,并以 page 为单位进行加密验证,保证不会被篡改,性能消耗主要在 IO。

第二步:Binding,符号绑定。将指针指向 image 外部的内容。查询符号表,设置指向镜像外部的指针,性能消耗主要在 CPU 计算。 通过 LC_DYLD_INFO_ONLY 可以查看各种信息的偏移量和大小。如果想要更方便直观地查看,推荐使用 MachOView 工具。

指针数量越少,指针修复的耗时也就越少。所以,优化该阶段的关键就是减少 __DATA 段中的指针数量。

优化方案

  • 减少 ObjC 类(class)、方法(selector)、分类(category)的数量,比如合并一些功能,删除无效的类、方法和分类等(可以借助 AppCode 的 Inspect Code 功能进行代码瘦身);
  • 减少 C++ 虚函数;(虚函数会创建 vtable,这也会在 __DATA 段中创建结构。)
  • 多用 Swift Structs。(因为 Swift Structs 是静态分发的,它的结构内部做了优化,符号数量更少。)

完成 Rebase 和 Bind 之后,通知 runtime 去做一些代码运行时需要做的事情:

  • dyld 会注册所有声明过的 ObjC 类;
  • 将分类插入到类的方法列表中;
  • 检查每个 selector 的唯一性。

优化方案

Rebase/Binding 阶段优化好了,这一步的耗时也会相应减少。

Rebase 和 Binding 属于静态调整(fix-up),修改的是 __DATA 段中的内容,而这里则开始动态调整,往堆和栈中写入内容。具体工作有:

  • 调用每个 Objc 类和分类中的 +load 方法;
  • 调用 C/C++ 中的构造器函数(用 attribute((constructor)) 修饰的函数);
  • 创建非基本类型的 C++ 静态全局变量。

优化方案

  • 尽量避免在类的 +load 方法中初始化,可以推迟到 +initiailize 中进行;(因为在一个 +load 方法中进行运行时方法替换操作会带来 4ms 的消耗)
  • 避免使用 __atribute__((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时再执行。比如用 dispatch_once()pthread_once()std::once(),相当于在第一次使用时才初始化,推迟了一部分工作耗时。:
  • 减少非基本类型的 C++ 静态全局变量的个数。(因为这类全局变量通常是类或者结构体,如果在构造函数中有繁重的工作,就会拖慢启动速度)

总结一下 pre-main 阶段可行的优化方案:

  • 重新梳理架构,减少不必要的内置动态库数量
  • 进行代码瘦身,合并或删除无效的ObjC类、Category、方法、C++ 静态全局变量等
  • 将不必须在 +load 方法中执行的任务延迟到 +initialize
  • 减少 C++ 虚函数

对于 main() 阶段,主要测量的就是从 main() 函数开始执行到 didFinishLaunchingWithOptions 方法执行结束的耗时。

查看阶段耗时

这里介绍两种查看 main() 阶段耗时的方法。

方法一:手动插入代码,进行耗时计算。
方法二:借助 Instruments 的 Time Profiler 工具查看耗时。
打开方式为:Xcode → Open Developer Tool → Instruments → Time Profiler




main() 被调用之后,didFinishLaunchingWithOptions 阶段,App 会进行必要的初始化操作,而 viewDidAppear 执行结束之前则是做了首页内容的加载和显示。

关于 App 的初始化,除了统计、日志这种须要在 App 一启动就配置的事件,有一些配置也可以考虑延迟加载。如果你在 didFinishLaunchingWithOptions 中同时也涉及到了首屏的加载,那么可以考虑从这些角度优化:

  • 用纯代码的方式,而不是 xib/Storyboard,来加载首页视图
  • 延迟暂时不需要的二方/三方库加载;
  • 延迟执行部分业务逻辑和 UI 配置;
  • 延迟加载/懒加载部分视图;
  • 避免首屏加载时大量的本地/网络数据读取;
  • 在 release 包中移除 NSLog 打印;
  • 在视觉可接受的范围内,压缩页面中的图片大小;
  • ……

如果首屏为 H5 页面,针对它的优化,参考 VasSonic 的原理,可以从这几个角度入手:

终端耗时

webView 预加载:在 App 启动时期预先加载了一次 webView,通过创建空的 webView,预先启动 Web 线程,完成一些全局性的初始化工作,对二次创建 webView 能有数百毫秒的提升。

页面耗时(静态页面)

静态直出:服务端拉取数据后通过 Node.js 进行渲染,生成包含首屏数据的 HTML 文件,发布到 CDN 上,webView 直接从 CDN 上获取; 离线预推:使用离线包。

页面耗时(经常需要动态更新的页面)

随着业务的增长,App 中的模块越来越多,冷启动的时间也必不可少地增加。冷启动本就是一个比较复杂的流程,它的优化没有固定的公式,我们需要结合业务,配合一些性能分析工具和线上监控日志,有耐心、多维度地进行分析和解决。

小讯
上一篇 2026-04-22 20:53
下一篇 2026-04-22 20:51

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/277561.html