「iOS」————APP启动优化

iOS学习

  • APP的启动流程
    • 启动
    • 流程
      • 缺页错误
    • 主要阶段
      • pre-main阶段
      • main阶段
    • 启动优化
      • pre-main
      • main阶段
      • 启动优化总结
      • 流程总结


APP的启动流程

启动

首先我们来了解启动的概念:

  • 广义上的启动是点击图标到首页数据加载完毕
  • 狭义上的启动是点击图标到启动图完全消失的第一帧

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

启动的计算方式如下:

  • 起点:进程创建的时间
  • 终点:第一个CA::Transaction::commit()

启动分为冷启动和热启动:

冷启动:系统里没有任何进程的缓存信息,典型的是重启手机后直接启动 App

热启动:从后台再次进入App时,这次启动就是热启动,因为进程缓存还在

那么我们使用app时,哪个启动更多呢?

这实际上要看产品的形态,打开的频次越高,热启动比例越高,很好理解。

流程

我们先看图:

请添加图片描述

以下是APP启动的底层细节链

点击APP启动 -> 加载libSystem -> Runtime注册回调函数 -> 加载image(镜像文件) -> 执行map_images和load_images方法 -> 调用main函数。

而其中打开app直到调用main函数之前,就是上图的dyld工作阶段,即pre-main阶段的一部分。

  • 进程创建与环境准备(内核 + SpringBoard)

    • 创建进程、启用 ASLR、加载签名与沙箱、校验权限与 Entitlements

    • 解析 Info.plist 的关键键(Bundle 标识、支持架构/平台、启动场景配置等)

    • 系统根据 LaunchScreen.storyboard/旧版 LaunchImage 在“你的代码尚未执行”时显示启动画面(闪屏由系统渲染,非你主动绘制)

  • dyld 加载与 Mach-O 修复

    • 加载可执行文件及其依赖的动态库(如 libSystem.B.dylib、libobjc.A.dylib、Swift runtime 等)

    • Rebase:把镜像内的指针按 ASLR 偏移修正到“真实地址”

    • Bind:为外部符号(如 NSLog)在运行时绑定到实际实现地址

(dyld3/共享缓存会显著缩短 dylib 加载与绑定时间)

  • 运行时初始化与预执行阶段(均在 main 之前)

    • ObjC Runtime 初始化:注册类、元类、选择子、协议,处理 Category

    • 调度并执行 Objective‑C +load(父类→子类,类→分类;各类/分类各执行一次)

    • 执行 C/C++ 全局构造与带 __ attribute__((constructor)) 的函数(均为“进程初始化时机”,具体与 +load 的跨语言相对顺序不应依赖)

  • 进入程序主入口与应用生命周期

    • 调用 main() → UIApplicationMain(…)

    • 创建 UIApplication 实例,建立主 RunLoop

    • 读取 Info.plist 中的主界面/场景配置:iOS 13+ 走 UIScene,旧版读取 UIApplicationMainStoryboardFile

    • AppDelegate 回调:

      • application:willFinishLaunchingWithOptions:(可选)

      • application:didFinishLaunchingWithOptions:(常用,创建 UIWindow、设 rootVC、makeKeyAndVisible())

    • 首个 UIViewController 生命周期:loadView → viewDidLoad → viewWillAppear → 动画 → viewDidAppear

    • 系统移除启动画面,展示首屏

rebase(偏移修正):任何一个app生成的二进制文件,在二进制文件内部所有的方法、函数调用,都有一个地址,这个地址是在当前二进制文件中的偏移地址。一旦在运行时刻(即运行到内存中),每次系统都会随机分配一个ASLR(Address Space Layout Randomization,地址空间布局随机化)地址值(是一个安全机制,会分配一个随机的数值,插入在二进制文件的开头),例如,二进制文件中有一个 test方法,偏移值是0x0001,而随机分配的ASLR是0x1f00,如果想访问test方法,其内存地址(即真实地址)变为 ASLR+偏移值 = 运行时确定的内存地址(即0x1f00+0x0001 = 0x1f01)。程序每次启动后地址都会随机变化,这样程序里所有的代码地址都需要需要重新对进行计算修复才能正常访问。rebasing这一步主要就是调整镜像内部指针的指向。

binding(绑定):,例如NSLog方法,在编译时期生成的mach-o文件中,会创建一个符号!NSLog(目前指向一个随机的地址),然后在运行时(从磁盘加载到内存中,是一个镜像文件),会将真正的地址给符号(即在内存中将地址与符号进行绑定,是dyld做的,也称为动态库符号绑定),一句话概括:绑定就是给符号赋值的过程

缺页错误

程序运能运行时因为存在物理内存,也就是说程序加入到物理内存中才得以运行,这一步就是虚拟内存映射到物理内存。这个过程是个使用懒加载方式完成系统到CPU的交互(翻译)的过程。

而这个过程因为懒加载映射方式的缘故,它是“有多少拿多少”,所以我们会通过一页一页的方式也就是page的方式去加载的,iOS的页的大小是16kb,而macOS是4kb。
也是因为是懒加载的方式,所以如果需要用到的时候发现物理内存中没有,就会报出“page fault”的缺页错误,然后缺的页会再加载放入物理内存。这个过程很短,可能30ms,也可能是10ms。

主要阶段

主要分为两个阶段:pre-main阶段和main阶段

pre-main阶段:程序启动到main函数执行前

main阶段:在执行main函数后,调用AppDelegate中的-application:didFinishLaunchingWithOptions:方法完成初始化,并展示首页

pre-main阶段

pre-main阶段做的事情与dyld的版本有关,此处以dyld2为例。

  • 加载应用的可执行文件
  • 加载动态链接库加载器dyld(dynamic loader)。
  • dyld递归加载应用所有依赖的dylib(dynamic library 动态链接库)。
  • 进行**rebase指针调整和bind**符号绑定。
  • ObjCruntime初始化(ObjC setup):ObjC相关Class的注册、category注册、selector唯一性检查等。
  • 初始化(Initializers):执行+load()方法、用attribute((constructor))修饰的函数的调用、创建C++静态全局变量等。

dyld流程概述:

我们看这个流程是为了看APP启动到main函数前,也就是dyld是如何将images(镜像文件:如动静态库等)链接到内存中去的。而在objc_init的时候是做了什么操作去调起dyld,以及dyld又如何回调至objc中。

加载链接库

从主执行文件的 header 获取到需要加载的所依赖动态库列表,而 header 早就被内核映射过。然后它需要找到每个 dylib,然后打开文件读取文件起始位置,确保它是 Mach-O 文件。接着会找到代码签名并将其注册到内核。然后在 dylib 文件的每个 segment 上调用 mmap()。应用所依赖的 dylib 文件可能会再依赖其他 dylib,所以 dyld所需要加载的是动态库列表一个递归依赖的集合。一般应用会加载 100400dylib 文件,但大部分都是系统 dylib,它们会被预先计算和缓存起来,加载速度很快。

修正

在加载所有的动态链接库之后,它们只是处在相互独立的状态,需要将它们绑定起来,这就是 Fix-ups。代码签名使得我们不能修改指令,那样就不能让一个 dylib 的调用另一个 dylib。这时需要加很多间接层。 现代 code-gen 被叫做动态 PIC(Position Independent Code),意味着代码可以被加载到间接的地址上。当调用发生时,code-gen 实际上会在 __DATA 段中创建一个指向被调用者的指针,然后加载指针并跳转过去。所以 dyld 做的事情就是修正(fix-up)指针和数据。Fix-up 有两种类型,rebasingbinding

在执行main函数之前,需要把类的信息注册到一个全局的Table中。同时,Objective C支持Category,在初始化的时候,也会把Category中的方法注册到对应的类中,同时会唯一Selector,这也是为什么当你的Cagegory实现了类中同名的方法后,类中的方法会被覆盖。

另外,由于iOS开发时基于Cocoa Touch的,所以绝大多数的类起始都是系统类,所以大多数的Runtime初始化起始在Rebase和Bind中已经完成。

初始化

调用+load方法和C/C++静态初始化对象和标记为 __ attribute__(constructor)的方法

到此结束dyld2的流程

我们再来看一下dyld2与dyld3的区别

请添加图片描述

dyld2是纯粹的in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时候,dyld2才能开始执行任务

dyld3则是部分out-of-process,部分in-process。图中,虚线之上的部分是out-of-process的,在App下载安装和版本更新的时候会去执行,out-of-process会做如下事情

  • 分析Mach-o Headers
  • 分析依赖的动态库
  • 查找需要Rebase & Bind之类的符号
  • 把上述结果写入缓存

此时,在进程内就只需要读取这个closure(闭包)直接从缓存中读取数据,大大减少了加载时间。

dyld是在是在dyld::_main函数中调用的

main阶段

  • 执行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions 初始化Window
  • 初始化基础的ViewController结构(一般是UINavigationController+UITabViewController) 获取数据(Local DB/Network),展示给用户。

启动优化

了解app启动流程和主要阶段,我们就可以来进行启动的优化了。

pre-main

在pre-main阶段,这个阶段最主要的就是dyld的加载。

而dylibs启动的第一步就是加载动态库,加载系统的动态库使很快的,因为可以缓存,而加载内嵌的动态库速度较慢。所以,提高这一步的效率的关键是:减少动态库的数量。

我们还可以考虑合并动态库,但是个人开发就不建议了

Rebase & Bind & Objective C Runtime

Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:

  • 减少__DATA段中的指针数量。
  • 合并Category和功能类似的类。比如:UIView+Frame,UIView+AutoLayout…合并为一个
  • 删除无用的方法和类。
  • 用initialize替代load
  • 减少__atribute__((constructor))的使用,而是在第一次访问的时候才用dispatch_once等方式初始化。
  • 不要创建线程

main阶段

延迟初始化那些不必要的UIViewController

能延迟执行的就延迟执行。比如SDK的初始化,界面的创建。 不能延迟执行的,尽量放到后台执行。比如数据读取,原始JSON数据转对象,日志发送。

启动优化总结

  • pre‑main(dyld/Runtime)

    • 减少/合并第三方与自家动态库数量;能静态链就静态链(SPM/静态 XCFramework)。

    • 避免在 +load / 构造器做耗时:移至懒加载(首次使用)或 didFinishLaunching 后的异步。

    • 精简 ObjC 元数据:删除无用类/方法/Category,合并零碎 Category(权衡维护性)。

    • 减少符号/指针膨胀:避免过度 @objc 暴露与反射;Swift 尽量 final/struct,降低动态性。

    • 尽量避免启动期方法交换(swizzle);必须 swizzle 的延后到需要的子系统启用时。

  • main 之后(App/Scene)

    • 轻量化首屏:小根 VC,延迟创建次级控制器与大型视图树。

    • 避免主线程同步 I/O/网络/大 JSON 解析;放后台队列,首帧后再做。

      • SDK 延迟初始化;按需加载功能模块。
    • 使用 Auto Layout 时约束闭环、减少首帧布局抖动;首屏资源(字体/图片)尽量小并就近。

流程总结

从用户点击 App 图标开始,系统先创建进程,按 Info.plist 做基础配置校验,建立沙箱,并用 LaunchScreen.storyboard 显示启动画面。接着进入 dyld 阶段:把可执行文件和依赖的动态库从共享缓存加载进来,做 ASLR 的 rebase 和符号 binding。随后运行时初始化:Objective‑C/Swift Runtime 注册类与分类,执行 C/C++ 全局构造器和 ObjC 的 +load(这些都在 main 之前完成)。

然后进入 main,调用 UIApplicationMain 创建 UIApplication,启动主 RunLoop。iOS 13 及以后通常走 UIScene:在 scene:willConnectTo: 里配置 window 和 rootViewController;老版本是在 application:didFinishLaunching 里创建 window 并 makeKeyAndVisible。接着首个控制器会依次触发 loadView、viewDidLoad、viewWillAppear,动画结束后 viewDidAppear,系统移除启动图,首帧呈现给用户。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/93470.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/93470.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

知名车企门户漏洞或致攻击者远程解锁汽车并窃取数据

漏洞概况一家大型汽车制造商的在线系统存在安全漏洞,可能导致客户数据泄露,并允许攻击者远程访问车辆。该漏洞由安全研究员Eaton Zveare发现,他已于2025年2月向涉事车企报告并促使漏洞修复。Zveare虽未公开车企名称,但透露这是在美…

Elasticsearch JS 自定义 ConnectionPool / Connection / Serializer、敏感信息脱敏与 v8 平滑迁移

0. 什么时候该用“高阶配置”? 复杂网络/路由需求:自定义“健康节点”判定、权重路由、多租户隔离。替换 HTTP 栈:接入企业内网网关、打通自研代理/审计、细化超时/连接细节。序列化治理:为超大 JSON、Bulk、查询串做定制编码/压缩…

希尔排序专栏

在排序算法的大家庭中,希尔排序(Shell Sort)以其独特的 "分组插入" 思想占据着重要地位。它是对插入排序的创造性改进,通过引入 "增量分组" 策略,大幅提升了排序效率。本文将带你深入理解希尔排序…

Android 欧盟网络安全EN18031 要求对应的基本表格填写

Android 欧盟网络安全EN18031 要求对应的基本表格填写 文章目录Android 欧盟网络安全EN18031 要求对应的基本表格填写一、背景二、18031认证预填表格三、其他1、Android EN 18031 要求对应的基本表格小结2、EN 18031的要求表格内容填写3、一定要做三方认证?4、欧盟网…

《Attention-driven GUI Grounding》论文精读笔记

论文链接:[2412.10840] Attention-driven GUI Grounding: Leveraging Pretrained Multimodal Large Language Models without Fine-Tuning 摘要 近年来,多模态大型语言模型(Multimodal Large Language Models,MLLMs)的…

PIDGenRc函数中lpstrRpc的由来和InitializePidVariables函数的关系

第一部分:./base/ntsetup/syssetup/setupp.h:404:#define MAX_PID30_RPC 5BOOL InitializePidVariables() {//// Get the Pid from HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid//Error RegOpenKeyEx( HKEY_LOCAL_MACHINE,((MiniSetup || OobeSetup) ? szFinalPidKeyNa…

Nginx学习笔记(七)——Nginx负载均衡

⚖️ Nginx学习笔记(七)——Nginx负载均衡 📌 一、负载均衡核心概念 架构定位: #mermaid-svg-00aCvwmJ40DHNd66 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-00aC…

MQ积压如何处理

处理消息队列(MQ)积压是一个需要系统化分析的运维挑战。下面我将结合常见原因,分步骤说明处理方案,并区分应急措施和根本解决方案:​一、快速诊断积压原因(核心!)​​​监控告警分析…

Unity与OpenGL中的材质系统详解

引言 在现代3D图形开发中,材质是定义物体外观的核心元素。Unity引擎提供了强大且直观的材质系统,使得开发者能够轻松实现复杂的视觉效果。然而,对于熟悉OpenGL的开发者来说,理解Unity材质系统的工作原理以及如何在OpenGL中实现类…

k8s安装DragonflyDB取代redis

数据库类型线程模型吞吐量 (QPS)延迟 (μs)内存效率适用场景兼容性Memcached纯内存键值存储多线程100K - 500K10 - 100高缓存、会话存储无原生密码认证DragonflyDB多协议内存数据库多线程1M50 - 200中高高吞吐缓存、Redis 替代兼容 RedisKeyDBRedis 多线程分支多线程500K - 1M5…

Horse3D游戏引擎研发笔记(五):在QtOpenGL环境下,仿three.js的BufferGeometry管理VAO和EBO绘制四边形

一、背景介绍 在三维图形渲染中,几何形状的管理是引擎的核心功能之一。Three.js通过BufferGeometry接口实现了对顶点数据和索引数据的高效管理,而OpenGL则通过顶点数组对象(VAO)和元素数组对象(EBO)来实现…

Ping32 与 IP-GUARD 深度对比:Ping32,引领企业数据安全新方向

在数字化时代,企业数据宛如珍贵的宝藏,是推动业务发展、保持竞争优势的核心资产。但与此同时,数据安全威胁也如影随形,内部员工的误操作、恶意窃取,外部黑客的攻击,都可能让企业数据面临泄露风险&#xff0…

洛谷 P2842 纸币问题 1 -普及-

题目描述 某国有 nnn 种纸币,每种纸币面额为 aia_iai​ 并且有无限张,现在要凑出 www 的金额,试问最少用多少张纸币可以凑出来? 输入格式 第一行两个整数 n,wn,wn,w,分别表示纸币的种数和要凑出的金额。 第二行一行 nn…

第十四节:物理引擎集成:Cannon.js入门

第十四节:物理引擎集成:Cannon.js入门 引言 物理引擎为3D世界注入真实感,让物体遵循重力、碰撞和动量等物理规律。Cannon.js是Three.js生态中最强大的物理引擎之一,本文将深入解析其核心机制,并通过Vue3实现物理沙盒系…

二十四、Mybatis-基础操作-删除(预编译SQL)

mybatis环境准备概述与注意事项(springboot项目引入三项必要的起步依赖)项目目录结构mybatis基础操作-删除对应EmpMapper(接口)代码 package com.itheima.mapper;import org.apache.ibatis.annotations.*;Mapper public interface…

JavaScript 核心基础:类型检测、DOM 操作与事件处理

JavaScript 作为松散类型语言,掌握类型检测规则、DOM 元素获取方式及事件处理逻辑,是写出健壮代码的基础。本文系统梳理 JS 高频基础知识点,结合实战场景解析原理与用法,帮你建立清晰的知识框架。 一、JS 数据类型与类型检测&…

软件开发过程中的维护活动

软件开发过程中的维护活动软件维护是软件生命周期中持续时间最长、成本最高的阶段,它并非简单的“修理”,而是一系列旨在延长软件生命周期、保持其价值和适应性的工程化活动。研究表明,软件维护成本可占总成本的60%以上。理解并有效管理维护活…

STC8单片机驱动I2C屏幕:实现时间、日期与温湿度显示

STC8 单片机驱动 I2C 屏幕:实现时间、日期与温湿度显示 在单片机项目中,“数据可视化” 是核心需求之一 —— 将时间、温湿度等关键信息实时显示在屏幕上,能让项目更具实用性。本文以STC8 系列单片机为核心,搭配 I2C 接口的 OLED…

基于SpringBoot+Vue的智能消费记账系统(AI问答、WebSocket即时通讯、Echarts图形化分析)

🎈系统亮点:AI问答、WebSocket即时通讯、Echarts图形化分析;一.系统开发工具与环境搭建1.系统设计开发工具后端使用Java编程语言的Spring boot框架 项目架构:B/S架构 运行环境:win10/win11、jdk17前端: 技术…

[论文笔记] WiscKey: Separating Keys from Values in SSD-Conscious Storage

阅读 WiscKey 论文时随手记录一些笔记。 这篇论文的核心思想理解起来还是很简单的,但是具体涉及到实现还有一些想不明白的地方,后来看到 TiKV 的 Titan 实现也很有趣,索性把这些问题都记录下来并抛出来。 本文中和论文相关的内容&#xff0…