启动优化-Initializers

Initializers是指在main函数之前执行的初始化代码,包括C++静态构造函数和__attribute__((constructor))标记的函数。这些代码会在Pre-main阶段同步执行,阻塞启动。 问题分析 以下代码会在main之前执行: 1. C++静态构造函数 __attribute__((constructor)) static void MyInitializer() { // 耗时初始化 initializeHeavyResource(); } 2. 全局静态变量的构造函数 // 全局静态变量 static std::vector<std::string> globalCache = loadFromDisk(); // 构造函数在main前调用 3. 非基本类型的全局变量 // 非POD类型的全局变量会触发构造函数 static std::string globalString = "Hello"; static MyClass globalObject; 优化方案 方案1:延迟初始化 将全局变量改为懒加载模式: // 优化前:全局变量在main前初始化 static std::vector<std::string> globalCache = loadFromDisk(); // 优化后(纯C++方案,适用于 .cpp 文件): #include <mutex> static std::vector<std::string>* getGlobalCache() { static std::vector<std::string>* cache = nullptr; static std::once_flag onceFlag; std::call_once(onceFlag, [&] { cache = new std::vector<std::string>(); *cache = loadFromDisk(); }); return cache; } // 优化后(ObjC++方案,适用于 .mm 文件): static std::vector<std::string>* getGlobalCache() { static std::vector<std::string>* cache = nullptr; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cache = new std::vector<std::string>(); *cache = loadFromDisk(); }); return cache; } 方案2:使用Swift懒加载 Swift的懒加载特性可以避免在启动时初始化: ...

May 2, 2026

启动优化-Rebase与Bind

Rebase和Bind是Pre-main阶段的重要步骤,用于修正程序中的指针地址。ObjC类、方法、协议等元数据越多,需要修复的指针越多,启动时间越长。 基本概念 由于ASLR(Address Space Layout Randomization)技术,App每次启动时加载到内存的地址都是随机的,因此需要进行地址修正。关于Rebase和Bind的底层原理,可以参考Mach-O的链接、装载与库。 Rebase(重定位) 修正指向Mach-O内部的指针,将编译时的地址加上ASLR偏移量。 Bind(绑定) 修正指向Mach-O外部的指针,查找符号表,绑定到正确的外部符号地址。 flowchart LR subgraph compile["编译时 (Mach-O文件)"] A1["内部指针: 0x1000(指向内部数据)"] A2["外部符号: _objc_msgSend(待绑定)"] end subgraph runtime["运行时 (内存中)"] B1["内部指针: 0x1000 + slide(Rebase修正)"] B2["外部符号: 实际地址(Bind绑定)"] end A1 -->|"Rebase"| B1 A2 -->|"Bind"| B2 问题分析 以下因素会增加Rebase/Bind的工作量: 因素 影响 ObjC类数量 每个类都有元数据需要修正 Swift类数量 Swift类在Apple平台也会生成ObjC兼容的元数据,同样需要修正 ObjC方法数量 方法列表中的指针需要修正 Category数量 Category的方法列表需要绑定 C++虚函数 虚函数表中的指针需要绑定 全局变量 指向其他符号的全局变量需要绑定 关于缓存机制: 系统动态库:Rebase/Bind 操作已在 dyld shared cache 中预先完成,不会在 App 启动时执行 App 动态库:dyld 3 的 Launch Closure 会缓存 Rebase/Bind 的元数据信息(哪些指针需要修正、修正方式等),但 Rebase/Bind 操作本身仍需每次启动时执行,因为 ASLR slide 每次启动都不同 因此,减少 ObjC 类、Category、C++ 虚函数等仍然是有效的优化手段。 ...

May 2, 2026

启动优化-didFinishLaunching

didFinishLaunchingWithOptions是main阶段的核心入口,在这里初始化大量SDK和服务会阻塞启动。本文介绍如何优化这个阶段的耗时。 问题分析 典型的问题代码: func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // 所有初始化都在主线程同步执行 CrashReporter.setup() Analytics.setup() PushNotification.setup() NetworkManager.setup() DatabaseManager.setup() ImageCache.setup() AdSDK.setup() SocialSDK.setup() // ... 更多SDK return true } 这种写法的问题: 所有初始化都在主线程同步执行 无论是否需要,所有SDK都在启动时初始化 阻塞首帧渲染 优化方案 方案1:分级启动任务管理 将启动任务按优先级分类,只在启动时执行必要的任务: // 启动任务优先级 enum LaunchTaskPriority { case required // 必须在首帧前完成 case high // 首帧后立即执行 case normal // 首屏稳定后执行 case low // 空闲时执行 } // 启动任务管理器 class LaunchTaskManager { static let shared = LaunchTaskManager() private var tasks: [LaunchTaskPriority: [() -> Void]] = [:] func register(priority: LaunchTaskPriority, task: @escaping () -> Void) { if tasks[priority] == nil { tasks[priority] = [] } tasks[priority]?.append(task) } func executeRequiredTasks() { tasks[.required]?.forEach { $0() } } func executeHighPriorityTasks() { DispatchQueue.main.async { self.tasks[.high]?.forEach { $0() } } } func executeNormalTasks() { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.tasks[.normal]?.forEach { $0() } } } func executeLowPriorityTasks() { // 监听RunLoop空闲时执行 CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode) { self.tasks[.low]?.forEach { $0() } } } } 使用示例 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let manager = LaunchTaskManager.shared // 必须在首帧前完成 manager.register(priority: .required) { CrashReporter.setup() // 崩溃收集必须最先初始化 } // 首帧后立即执行 manager.register(priority: .high) { Analytics.setup() PushNotification.setup() } // 首屏稳定后执行 manager.register(priority: .normal) { AdSDK.setup() SocialSDK.setup() } // 空闲时执行 manager.register(priority: .low) { PreloadManager.preloadResources() } // 只执行必须的任务 manager.executeRequiredTasks() return true } 任务分级建议 优先级 适用场景 示例 required 崩溃收集、核心功能依赖 Crash SDK、必要的配置加载 high 用户可能很快用到 推送、统计、网络配置 normal 非首屏功能 广告SDK、社交分享 low 预加载、缓存 图片预加载、数据预热 方案2:并行初始化 将无依赖关系的初始化任务并行执行: ...

May 2, 2026

启动优化-load方法

+load方法是ObjC运行时在类被加载时自动调用的方法,所有+load方法在main函数之前同步执行,会阻塞启动。 问题分析 +load方法的特点: 特性 说明 调用时机 main函数之前,ObjC Runtime初始化时 调用顺序 父类 → 子类 → Category 线程 主线程,同步执行 影响 直接阻塞启动 // 问题代码:+load中执行耗时操作 @implementation HeavyModule + (void)load { // 耗时操作会阻塞启动 [self setupDatabase]; [self preloadResources]; [self registerServices]; } @end +load与+initialize的区别 特性 +load +initialize 调用时机 main之前 类首次使用时 调用次数 只调用一次 可能多次(子类触发) 是否阻塞启动 是 否 线程安全 是 是 调用顺序 父类→子类→Category 父类→子类 更详细的对比请参考:+load与+initialize的区别 优化方案 方案1:使用+initialize替代 将+load中的逻辑迁移到+initialize,延迟到类首次使用时执行: @implementation HeavyModule // 优化前 + (void)load { [self setupDatabase]; } // 优化后 + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 延迟到类首次使用时执行 [self setupDatabase]; }); } @end 注意:+initialize可能被子类触发多次调用,需要使用dispatch_once保证只执行一次。 ...

May 2, 2026

启动优化-二进制重排

二进制重排是一种通过重新排列二进制文件中函数顺序来减少启动时Page Fault的优化技术。 基本原理 Page Fault问题 iOS使用虚拟内存管理,讨论Page Fault时需要先区分两层“内存”: 虚拟地址空间:进程看到的是一段连续的虚拟地址。App启动时,dyld会把Mach-O的__TEXT、__DATA等段映射到进程的虚拟地址空间里。这里的“映射”只是建立虚拟地址和文件偏移的关系,不代表整段二进制代码已经全部进入物理内存。 物理内存:CPU真正执行代码时,需要对应虚拟页背后有可用的物理页。代码页通常是文件映射页,第一次访问某个尚未驻留在物理内存中的代码页时,会触发Page Fault。内核再从App二进制文件中读取这个页的内容,填充到物理页,并更新页表。 所以,更准确地说:程序代码按页映射到虚拟地址空间;启动过程中实际执行到某个函数时,才会按需把该函数所在的代码页调入物理内存。Page Fault本身不是异常崩溃,而是虚拟内存按需调页的正常机制,只是冷启动时如果触发太多文件读取,会增加启动耗时。 默认情况下,编译器按照链接顺序排列函数,导致启动时调用的函数可能分散在不同的代码页中: 优化前(启动函数分散在不同代码页): ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ Page 1 │ │ Page 2 │ │ Page 3 │ │ Page 4 │ │ func_A │ │ func_X │ │ func_B │ │ func_C │ │ ... │ │ ... │ │ ... │ │ ... │ └────────┘ └────────┘ └────────┘ └────────┘ 启动调用 A→B→C 会触达 Page 1, 3, 4。 如果这些代码页还没有驻留在物理内存中,就可能产生3次Page Fault。 二进制重排的效果 通过重排,将启动时调用的函数集中排列在相邻的代码页中: ...

May 8, 2026

启动优化-减少动态库

动态库加载是Pre-main阶段的重要组成部分,每个动态库都需要加载、验证签名、进行符号绑定,这些操作会显著影响启动时间。 问题分析 当App启动时,dyld需要: 分析Mach-O文件的Load Commands,找出依赖的动态库 递归加载所有依赖的动态库 对每个动态库进行签名验证 执行Rebase和Bind操作 动态库数量越多,这些操作的耗时就越长。Apple建议将自定义动态库数量控制在6个以内。关于动态库加载的详细流程,可以参考Mach-O的链接、装载与库。 关于缓存机制: 系统动态库:已被放入 dyld shared cache 中,其加载和符号绑定操作已预先完成,不会影响 App 启动时间 App 动态库:dyld 3 引入的 Launch Closure 机制会缓存依赖分析、Rebase/Bind 信息等元数据。首次启动(或 App 更新后)会生成缓存,后续启动直接使用 但即使有 Launch Closure 缓存,Rebase/Bind 操作本身仍需执行(因为 ASLR slide 每次启动都不同),动态库数量越多,这些操作的耗时仍然越长。 App可执行文件 ├── UIKit.framework │ ├── Foundation.framework │ │ └── CoreFoundation.framework │ └── CoreGraphics.framework ├── 自定义Framework A │ └── 依赖库... └── 自定义Framework B └── 依赖库... 优化方案 1. 合并动态库 将功能相近的动态库合并为一个: ...

May 2, 2026

启动优化-观测

准确测量启动时间是优化的前提。启动时间是用户体验的关键指标之一,研究表明,应用性能直接影响用户留存:页面加载时间每增加1秒,用户流失率会显著上升。对于iOS应用,需要在20秒内完成启动,否则可能会被watchdog强制终止。 本文介绍如何测量和监控iOS应用的启动时间。关于启动流程的详细介绍,请参考 App启动流程。 一、测量方式概览 iOS 提供了多种测量启动时间的方式,适用于不同场景: 方式 适用场景 粒度 是否需要代码 dyld 环境变量 开发调试 定性分析 否 Instruments App Launch 深度分析 函数级 否 代码埋点 开发调试 + 线上监控 自定义 是 MetricKit 线上监控 冷启动/热启动 少量代码 二、开发调试阶段 2.1 dyld 环境变量 通过设置 dyld 环境变量可以在控制台查看启动过程中的加载信息: Edit Scheme → Run → Arguments → Environment Variables 与启动测量相关的环境变量: 环境变量 作用 启动分析用途 DYLD_PRINT_LIBRARIES 打印每个加载的 mach-o 镜像 查看动态库加载顺序,确认是否加载了不必要的库 DYLD_PRINT_LOADERS 打印镜像的加载器类型(JustInTimeLoader / PrebuiltLoader) 分析是否使用了启动闭包优化 DYLD_PRINT_INITIALIZERS 打印每个 initializer 的执行 查看 +load、constructor、C++ 静态构造函数的执行顺序 DYLD_PRINT_BINDINGS 打印每次符号绑定 分析符号绑定情况(输出量很大) DYLD_PRINT_SEARCHING 打印库搜索路径 排查库加载问题 DYLD_PRINT_APIS 打印 dyld API 调用(如 dlopen) 检测运行时动态加载行为 DYLD_PRINT_TO_FILE 将日志输出到指定文件 避免控制台日志过多,方便后续分析 使用示例: ...

May 2, 2026

启动优化

iOS应用的启动时间是用户体验的关键指标之一。研究表明,应用性能直接影响用户留存:页面加载时间每增加1秒,用户流失率会显著上升。对于iOS应用,需要在20秒内完成启动,否则可能会被watchdog强制终止。 本系列文章从启动流程的各个阶段入手,系统性地介绍iOS启动优化的方法和实践。 启动监控是 APM 的重要子系,线上 P50/P90/P99 的采集、冷/温/热启动判定、以及与业务秒开指标的关联,请参考 APM 系列:指标体系、数据采集。 启动流程概述 App的启动流程分为两个主要阶段: 阶段 时间范围 主要工作 Pre-main 进程创建 → main()函数 加载可执行文件、加载动态库、Rebase/Bind、ObjC Runtime初始化、Swift Runtime元数据注册、+load方法、Initializers main main()函数 → 首帧渲染 UIApplicationMain、AppDelegate回调、首帧渲染 flowchart TD Start([App 冷启动]) --> PreMain subgraph PreMain["Pre-main 阶段"] direction TB A[加载可执行文件创建进程、mmap映射、签名验证] --> B[加载动态库递归加载依赖、包括 Swift Runtime] B --> C[Rebase & Bind指针修正、符号绑定] C --> D[ObjC Runtime 初始化类注册、Category 附加] D --> E[Swift Runtime 元数据注册类型元数据、协议遵循表] E --> F[调用 +load 方法] F --> G[执行 InitializersC++静态构造函数、__attribute__] end PreMain --> MainPhase subgraph MainPhase["main 阶段"] direction TB H[main函数] --> I[UIApplicationMain创建UIApplication、AppDelegate、启动RunLoop] I --> J[AppDelegate回调willFinish、didFinishLaunching] J --> K[首帧渲染创建Window、RootViewController、首屏UI] end MainPhase --> End([启动完成]) 文章导航 本系列包含以下文章,建议按顺序阅读: ...

May 2, 2026

耗电-CPU与后台优化

CPU是App最容易"无意识"耗电的模块,而后台则是最容易"偷偷"耗电的场景。本文聚焦这两大问题域,给出具体的优化手段。 一、CPU耗电的根本原因 根据 耗电-原理 中的分析,CPU耗电由三部分构成: 活跃时间:CPU处于C0/P0的时间。 瞬时功率:和P-State频率相关。 唤醒次数:频繁从C-State唤醒无法进入深度休眠。 对应三条优化方向: 优化方向 策略 减少活跃 任务及时结束,该停就停 降低功率 避免无谓的满核满频,使用合适的QoS 合并唤醒 聚合Timer、聚合任务,让CPU能连续工作后进入深休眠 二、识别CPU耗电热点 从MetricKit入手 cumulativeCPUTime 是线上最直接的CPU指标。对比同版本前后、同机型前后的CPU时长,能快速定位回归。 Time Profiler的使用技巧 在Instruments中抓一份Time Profiler Trace后: 按"Heavy Stack"视图查看耗时函数。 关注后台的采样(通过Xcode的生命周期切换复现)。 查看线程调用频率——单次耗时低但高频调用的函数,同样是耗电大头。 代码层面的CPU异味 // 1. 死循环轮询 while isWaitingForData { if dataReady { break } } // 2. 主线程sleep循环 DispatchQueue.global().async { while true { checkStatus() Thread.sleep(forTimeInterval: 0.1) } } // 3. 密集Timer Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { _ in self.updateUI() } // 4. 无限制的CADisplayLink displayLink = CADisplayLink(target: self, selector: #selector(tick)) displayLink.add(to: .main, forMode: .common) // 页面隐藏后忘记invalidate 这些代码在Time Profiler中都会显示出异常高的调用频率。 ...

May 2, 2026

耗电-原理

本文从硬件功耗模型、iOS系统的能效调度机制、以及各个硬件模块的功耗特性三个维度,系统性地讲解iOS App耗电的底层原理。 功耗的基本概念 功率、能量与电量 功率(Power):单位时间内消耗的能量,单位为瓦特(W)。 能量(Energy):功率在时间上的积分,单位为焦耳(J)或毫瓦时(mWh)。 电量(Battery Capacity):电池能存储的总能量,单位为毫安时(mAh)。 \[ Energy = \int P(t)\,dt \]对于一段时间内近似恒定的功率,可以简化为 \( E = P \times t \)。这也是所有耗电优化的起点——要么降功率(P),要么降时间(t)。 电池电压与电流 iPhone使用锂电池,标称电压约为3.7~3.8V。操作系统看到的电流则随着硬件负载实时变化。可以近似认为: \[ P(t) = V \times I(t) \]在App层面,我们无法直接测量V和I,但可以通过 UIDevice.current.batteryLevel 观察电量百分比,通过 IOKit 私有API读取更精细的电池状态(后面检测篇会详细讲)。 硬件功耗特性 不同硬件模块的功耗曲线差异非常大。理解每个模块的特性,才能找到最有效的优化点。 CPU:P-State与C-State 现代CPU(包括Apple Silicon)通过 动态电压频率调整(DVFS) 来平衡性能与功耗。 P-State(Performance State) P-State描述CPU在工作时的电压/频率档位。以Apple A系列芯片为例,大核和小核都有多个P-State: flowchart LR subgraph P-State P0["P0最高频率功耗最大"] P1["P1中高频率"] P2["P2中低频率"] P3["P3低频率功耗小"] end P0 -.->|负载下降| P1 P1 -.-> P2 P2 -.-> P3 P3 -.->|负载上升| P0 CPU的功耗与频率大致呈 立方关系(电压随频率升高,功耗 ≈ V² × f ≈ f³)。这意味着: ...

May 2, 2026