iOS--App启动优化

启动优化

启动类型

  • Cold Launch冷启动:App进程不存在,从未打开过或者被系统。用户杀死,系统需要从磁盘加载二进制文件,创建新进程。
  • Warm Launch温启动:App进程还在内存中,但被挂起,部分数据可能被回收,不需要重新执行main之前的加载,但需要重新恢复状态。
  • Hot Launch热启动:App在后台运行,只是被切换到前台,几乎不要做任何初始化工作

先介绍一些概念:

Mach-O

Mach-O(Mach Object File Format)是一种用于记录可执行文件、对象代码、共享库、动态加载代码的文件格式

App 编译生成的二进制可执行文件就是Mach-O 格式,iOS 工程所有的类编译后会生成对应的目标文件.o文件,而这个可执行文件就是这些.o文件的集合

  • Header头部
    • 标识这是一个 Mach-O 文件
    • CPU架构
    • 文件类型:是可执行文件(Executable)、动态库(Dylib)还是目标文件(Object)
  • Load Commands加载命令
    告诉操作系统如何加载和处理这个文件
    • 加载段(LC_SEGMENT)
    • 需要加载的动态库(LC_LOAD_DYLIB)
    • 程序的入口(LC_MAIN)
    • 加密信息:标示文件是否被加密
  • Data数据区
    这部分存放了实际的代码和数据,被划分为多个 Segment(段),每个Segment 下包含多个 Section(节)。
    • __TEXT(代码段):只读可执行 r-x,存放被执行的代码和只读常量
    • __DATA(数据段):可读可写 rw-,存放全局变量、静态变量等
    • __LINKEDIT(链接信息段):只读 r--,存放链接器需要的信息,比如函数的名称和地址
dylib(动态链接库)

全称 Dynamic Library,后缀通常是.dylib,是 Mach-O 格式的一种。

  • 动态库(.dylib):App运行时,系统才把这个库加载到内存里,可以被多个 App 的进程共享
  • 静态库(.a):编译时,库代码会被完整拷贝进 App 可执行文件中,App 体积会变大,但运行时不需要找外部文件
dyld( 动态链接器)

全称 Dynamic Linker,是系统的一个辅助程序(也是一个 Mach-O 文件),运行在/usr/lib/dyld

dyld 会将 App 依赖的动态库和 App 文件加载到内存。

dyld shared cache(动态库共享内存)

iOS 系统有成百上千个系统动态库,如果每个 App 启动,dyld 都要去磁盘中找到这些文件,打开它们读取header,验证签名,映射到内存... I/O操作会极其频繁,App启动速度变慢。

dyld shared cache 是一个巨型文件,Apple 在构建iOS系统时,把所有常见的系统动态库合并、预链接成一个单一的文件。

系统启动后,这个巨大的 Shared Cache 被映射到内存中,所有运行中的App都可以共享这一份 Shared Cache,dyld 在加载系统动态库时,直接识别到它在 Shared Cache里,直接用内存里的指针就行了。

image

images 在这里不是指图片,而是镜像。每个 App 都是以 images 为单位进行加载的。images 类型包括:

  • executable:应用的二进制可执行文件;
  • dylib:动态链接库;
  • bundle:资源文件,属于不能被链接的 dylib,只能在运行时通过 dlopen() 加载。
framework

framework 可以是动态库,也可以是静态库,是一个包含 dylib、boundle 和头文件的文件夹

冷启动

冷启动的整个过程是从用户点击图标开始到 AppDelegate 中的didFinishLaunchingWithOptions:方法执行完毕为止,并以执行main()函数的实际为分界点,分为 pre-main 和 main() 阶段。

pre-main 阶段

pre-mian 阶段指的是从用户唤起 App 到 main 函数执行之前的过程。

  • 系统内核:
    • 启动一个应用时,系统内核会创建一个新的进程,并为分配虚拟内存
    • 把 App 对应的可执行文件加载到内存空间
    • 把 dyld 加载到内存
  • dyld
    动态链接器接管进程
    • 加载动态库
      • dyld 从主执行文件的 header 获取需要加载的所依赖的动态库列表,找到每个 dylib,而应用所依赖的 dylib 文件可能会再依赖其他 dylib,因此需要加载的是动态库列表递归依赖的集合
    • Rebase和Bind
      由于 ASLR(Address Space Layout Randomization)的存在,image 会在新的随机地址上加载,和之前指针指向的地址会有一个偏差 slide ,因此 dyld 需要修正这个偏差,指向正确的地址
      • Rebase 重定位:在 imgae 内部调整指针的指向,性能消耗在IO。
      • Bind 符号绑定:查询符号表,设置指向 image 外部的指针,性能消耗在 CPU 计算。
    • ObjC setup
      完成 Rebase 和Bind 之后,会通知 Runtime
      • Objc 类注册:读取二进制中的类列表,将其注册到Runtime表中
      • Category 注册:将Category中的方法插入到主类的方法列表中
      • Selector 唯一性检查
    • Initializers
    • 调用每个 Objc 类和分类中的 +load 函数
    • 调用 C/C++ 中的构造器函数
    • 创建非基本类型的 C++ 静态全局变
  • dyld调用App的main()函数

ObjC setup 中 Runtime 注册的类是哪里的类?

  • 注册:
    • 系统库的类: UIView, NSString, NSArray 等。
    • 三方库的类: AFNetworking, SDWebImage 里的类。
    • 你自己写的类: MyViewController, MyModel 等。
  • 原因:
    • Objective-C 是一门动态语言。编译后的二进制文件(Mach-O)里,类只是"数据"。App 启动时,系统(dyld 和 Runtime)需要把这些"数据"读出来,在内存中构建一张巨大的"映射表"。
    • 例如,我调用[MyObject alloc] 时,Runtime 必需在内存里查到 MyObject 对应的方法列表在哪里。要与类进行映射

了解了main()函数之前到加载过程,可以分析出一些影响因素:

  • 动态库加载越多,启动越慢
  • ObjC类、方法、分类越多、启动越慢
  • ObjC的+load越多、启动越慢C的
  • constructor函数越多,启动越慢
  • C++静态对象越多,启动越慢

针对以上几点,我们可以做如下的优化:

在pre-main阶段:

  • 重新梳理架构,减少不必要的内置动态库数量
  • 代码瘦身:减少类、方法、分类的数量,合并一些功能、删除无效的类、方法和分类
  • +load优化:尽量避免在类的+load方法中初始化,可以推迟到+initialize中进行

一般来说,当我们的项目规模不是很大的时候,,Pre-main(main函数执行前)阶段能挖掘的优化空间确实相对有限,因为没有几百个动态库,也没有几十万行的遗留代码。

首先来详细解析一下此阶段的操作。

Post-main 阶段

此阶段,就是从main()函数开始执行到didFinishLaunchingWithOptions 方法执行结束的耗时。

这个阶段是串行执行的,阻塞了主线程,任何耗时操作都会直接导致 App 启动变慢。

1. main() 函数入口

swift 复制代码
int main(int argc, char * argv[]) {
    @autoreleasepool {
        // 调用 UIApplicationMain
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

这里几乎没有耗时,直接进入下一步。

2.UIApplication 系统接管

  • 初始化 UIApplication 单例对象
  • 初始化 AppDelegate 代理对象
  • 建立 Main RunLoop 主线程的runloop
  • 解析 Info.plist,加载 Main.storyboard

3.application:willFinishLaunchingWithOptions:

这是 AppDelegate 的第一个回调,耗时极短

4.application:didFinishLaunchingWithOptions: ❗️耗时处

通常这里会发生什么:

  • 日志/统计 SDK 初始化
  • 广告 SDK 初始化
  • 业务 SDK 初始化
  • UI 配置: 设置 UINavigationBar 全局样式、各种 Appearance 代理
  • 用户登录状态检查: 从本地读取 Token,判断是进登录页还是主页
  • I/O 操作:读取 NSUserDefaults,读取本地缓存文件,读取数据库

5.RootViewController 的生命周期

didFinishLaunching 还没结束(或者刚结束),代码会走到 window.makeKeyAndVisible。此时会触发根控制器的加载:

  • init (初始化)
  • viewDidLoad (加载视图)❗️耗时处
    • 很多人在这里进行网络请求、读取大量数据库数据、解压图片。
  • viewWillAppear
  • viewWillLayoutSubviews

6.首屏渲染

这才是用户眼中的"启动完成"。当 CATransaction 提交,GPU 完成渲染,屏幕上终于不再是 LaunchScreen,而是你的首页内容。

了解完 Post-Main 阶段的过程后,针对一般规模的项目,可以进行如下的优化:

一、先梳理 didFinishLaunchingWithOptions:中的任务,分为三类:

  • A 类 :必需且紧急,不执行就崩溃 / UI没法看
  • B 类:必需但不紧急,晚几秒启动也不迟
  • C 类:非必需

1. 延迟化加载

不要在 didFinishLaunchingWithOptions: 中一下子全部初始化所有 SDK。可以在首屏渲染之后 / 需要用到的时候 再初始化。

例如:支付 SDK 可以在用户点击"支付"按钮那一刻再注册,或者在首页加载完 3 秒后再注册。

2. 多线程分流

有些 I/O 操作不得不做,那就不要阻塞主线程。切换到子线程去操作。

二、精简 viewDidLoad

  1. UI代码 -> 留着
  2. 数据操作 -> 异步处理
  3. 非首屏UI -> 懒加载

参考:

https://juejin.cn/post/6844904085108310024

https://tech.meituan.com/2018/12/06/waimai-ios-optimizing-startup.html

相关推荐
终端安全笔记7 小时前
iOS 27 之后「策略空转」:设备升级不报错,但旧策略不再管它
android·网络·安全·ios·智能手机
97650333510 小时前
iOS 上架 4.3a 被拒【uniapp专讲】
flutter·ios·objective-c·uniapp·swift
黑化旺仔1 天前
【OC】KVO
macos·ios·objective-c·cocoa
HouWan1 天前
SwiftUI 小技巧:如何读取 SwiftUI Font 的详细信息
ios·swiftui·apple
李游Leo1 天前
HarmonyOS 7 实战开发 05:把页面做到可上线状态
ios·harmonyos
终端安全笔记1 天前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程1 天前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne2 天前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记2 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩2 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift