在iOS项目中,冷启动时间是指从用户点击应用图标开始,到应用完全加载并呈现出第一个界面(可能需要网络请求必要的数据)所花费的时间。这里以 main 函数为界,分为两个时间段:
- 从用户点击应用图标 ~ invoke main func
- invoke main func ~ 首屏渲染完成
计算 main 函数调用之前的耗时
通过添加环境变量 DYLD_PRINT_STATISTICS
,可以准确计算应用程序的 Pre-main Time(即在 main 函数之前的启动时间)。DYLD_PRINT_STATISTICS
环境变量由动态链接器 dyld 提供,用于输出启动过程中各种操作的时间统计信息。
使用 DYLD_PRINT_STATISTICS 计算 Pre-main Time
- 打开 Xcode 并选择你的项目。
- 选择项目的 Scheme,点击 Edit Scheme…。
- 在左侧选择 Run 选项卡,然后选择 Arguments 子选项卡。
- 在 Environment Variables 部分,点击
+
按钮添加新的环境变量:- Name:
DYLD_PRINT_STATISTICS
- Value:
1
- Name:
- 点击 Close 保存更改。
运行应用并查看输出
- 运行你的应用程序。在 Xcode 的调试控制台中,你会看到 dyld 输出的详细启动时间统计信息,包括以下几个部分:
- Total pre-main time:从启动应用程序到调用 main 函数之间的总时间。
- dylib loading time:加载动态库所花费的时间。
- rebase/binding time:重定位和绑定符号的时间。
- ObjC setup time:Objective-C 运行时环境的初始化时间。
- initializer time:执行全局和静态变量的初始化时间。
解析输出示例
以下是调试控制台中可能看到的输出示例:
Total pre-main time: 200.21 milliseconds (100.0%)
dylib loading time: 50.03 milliseconds (25.0%)
rebase/binding time: 30.01 milliseconds (15.0%)
ObjC setup time: 20.05 milliseconds (10.0%)
initializer time: 100.12 milliseconds (50.0%)
解释输出
- Total pre-main time:总的 Pre-main 时间,从应用程序启动到 main 函数开始执行的时间。这包括动态库加载、重定位和符号绑定、Objective-C 运行时初始化、以及全局和静态变量的初始化时间。
- dylib loading time:加载动态库(dylibs)所花费的时间。
- rebase/binding time:重定位(rebase)和符号绑定(binding)所花费的时间。
- ObjC setup time:初始化 Objective-C 运行时环境所花费的时间。
- initializer time:执行全局和静态变量初始化所花费的时间。
通过这些数据,可以了解应用启动过程中各个阶段的时间消耗,特别是 Pre-main 时间,这对于优化应用的启动性能非常有帮助。
优化建议
根据 Pre-main 时间的统计信息,可以针对性地进行优化,例如:
- 减少动态库数量:减少应用程序依赖的动态库数量,可以显著减少动态库加载时间。
- 优化符号绑定:通过减少符号绑定和重定位的数量,可以降低 rebase/binding 时间。
- 优化全局变量初始化:避免复杂和耗时的全局变量初始化,尽量推迟到实际使用时再初始化。
- 减少 Objective-C 类的数量:减少 Objective-C 类的数量和复杂性,可以降低 ObjC setup 时间。
通过这些优化措施,可以有效减少 Pre-main 时间,从而提升应用的启动速度。
小结
通过添加环境变量 DYLD_PRINT_STATISTICS
,可以准确计算和分析应用的 Pre-main 时间。这为优化应用的启动性能提供了详细的参考数据,使开发者能够更有针对性地进行优化。
计算 main 函数到首屏渲染完成的耗时
从 main
函数到完全呈现出第一个界面的时间,特别是在第一个界面涉及网络请求的情况下,计算这一段时间可以通过以下几步来实现:
方法 1:使用 os_signpost API 记录时间
在应用的关键点添加 os_signpost
标记,并结合 Instruments 工具进行分析。
步骤 1:设置 os_signpost 标记
在 main
函数中和第一个界面呈现完成后添加 os_signpost
标记。
import os.signpost
let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Performance")
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var signpostID = OSSignpostID(log: log)
static func main() {
os_signpost(.begin, log: log, name: "App Launch", signpostID: OSSignpostID(log: log))
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self))
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 初始化代码...
return true
}
}
class YourViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 假设这是进行网络请求并更新界面的方法
fetchDataAndUpdateUI()
}
func fetchDataAndUpdateUI() {
// 开始网络请求
let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Performance")
let signpostID = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Fetch Data", signpostID: signpostID)
// 模拟网络请求
DispatchQueue.global().async {
sleep(2) // 模拟网络延迟
DispatchQueue.main.async {
// 完成网络请求并更新UI
os_signpost(.end, log: log, name: "Fetch Data", signpostID: signpostID)
os_signpost(.end, log: log, name: "App Launch", signpostID: OSSignpostID(log: log))
// 更新UI代码...
}
}
}
}
步骤 2:使用 Instruments 工具进行分析
- 打开 Xcode 并选择你的项目。
- 选择 Product > Profile 或使用快捷键
Command + I
启动 Instruments。 - 在 Instruments 中选择 Signpost Instrument 模板并点击 Choose。
- 在 Instruments 界面中,点击 Record 按钮,启动你的应用。
- 在左侧的活动记录(Activity Trace)中,可以看到
App Launch
和Fetch Data
的开始和结束时间。
通过这种方法,可以准确地测量从 main
函数开始到第一个界面完全呈现出来的时间,包括网络请求所花费的时间。
方法 2:手动记录时间戳
在关键点手动记录时间戳,并计算总耗时。
步骤 1:记录时间戳
在 AppDelegate
和视图控制器中记录时间戳。
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static var launchStartTime: TimeInterval?
static var firstViewControllerLoadTime: TimeInterval?
static func main() {
launchStartTime = Date().timeIntervalSince1970
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self))
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 初始化代码...
return true
}
}
class YourViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 假设这是进行网络请求并更新界面的方法
fetchDataAndUpdateUI()
}
func fetchDataAndUpdateUI() {
// 开始网络请求
let startTime = Date().timeIntervalSince1970
// 模拟网络请求
DispatchQueue.global().async {
sleep(2) // 模拟网络延迟
DispatchQueue.main.async {
// 完成网络请求并更新UI
let endTime = Date().timeIntervalSince1970
if let launchStartTime = AppDelegate.launchStartTime {
let totalLaunchTime = endTime - launchStartTime
print("Total launch time including network request: \(totalLaunchTime) seconds")
}
// 更新UI代码...
}
}
}
}
步骤 2:运行并查看输出
- 运行你的应用程序。
- 在调试控制台中查看总的启动时间,包括网络请求的时间。
小结
通过上述方法,可以准确计算从 main
函数到第一个界面完全呈现出来的时间,尤其是在涉及网络请求的情况下。使用 os_signpost
API 结合 Instruments 工具进行分析,可以提供更详细和精确的性能测量数据。而手动记录时间戳的方法则相对简单,但同样能提供一个大致的时间估计。