精确计算应用的冷启动耗时

在iOS项目中,冷启动时间是指从用户点击应用图标开始,到应用完全加载并呈现出第一个界面(可能需要网络请求必要的数据)所花费的时间。这里以 main 函数为界,分为两个时间段:

  • 从用户点击应用图标 ~ invoke main func
  • invoke main func ~ 首屏渲染完成

一、通过添加环境变量 DYLD_PRINT_STATISTICS,可以准确并完整地计算出应用程序的 Pre-main Time(即在 main 函数之前的启动时间)么 ?

很遗憾,答案是否定的,DYLD_PRINT_STATISTICS 环境变量只能帮助测量 Dyld 的启动时间,无法帮助我们完整地测量 main() 方法执行之前的所有时间。

苹果对 DYLD_PRINT_STATISTICS 定义是 Logs statistical information on an application’s launch process, such as how many images were loaded, when the application finishes launching. 这个说明与其真实的表现可以说是天差地别,早已过时!

  1. DYLD_PRINT_STATISTICS 环境变量由动态链接器 dyld 提供,用于输出启动过程中各种操作的时间统计信息(当然,这里说的各种操作仅限于和 dyld 相关的操作)。
  2. statistic in 有道: [N-COUNT] Statistics are facts which are obtained from analysing information expressed in numbers, for example information about the number of times that something happens. 统计数据

使用 DYLD_PRINT_STATISTICS 计算与 dyld 相关的 Pre-main Time

  1. 打开 Xcode 并选择你的项目
  2. 选择项目的 Scheme,点击 Edit Scheme…
  3. 在左侧选择 Run 选项卡,然后选择 Arguments 子选项卡。
  4. Environment Variables 部分,点击 + 按钮添加新的环境变量:
    • Name: DYLD_PRINT_STATISTICS
    • Value: 1
  5. 点击 Close 保存更改。

运行应用并查看输出

  1. 运行你的应用程序。在 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:与 dyld 相关的 Pre-main 时间,这包括动态库加载、重定位和符号绑定、Objective-C 运行时初始化、以及全局和静态变量的初始化时间。
  • dylib loading time:加载动态库(dylibs)所花费的时间。
  • rebase/binding time:重定位(rebase)和符号绑定(binding)所花费的时间。
  • ObjC setup time:初始化 Objective-C 运行时环境所花费的时间。
  • initializer time:执行全局和静态变量初始化所花费的时间。

通过这些数据,可以了解应用启动过程中与 dyld 相关的各个阶段的时间消耗,但是要注意,这并不是完整的 Pre-main 时间,认识到这一点是很有必要的!

优化建议

根据 Pre-main 时间的统计信息,可以针对性地进行优化,例如:

  1. 减少动态库数量:减少应用程序依赖的动态库数量,可以显著减少动态库加载时间。
  2. 优化符号绑定:通过减少符号绑定和重定位的数量,可以降低 rebase/binding 时间。
  3. 优化全局变量初始化:避免复杂和耗时的全局变量初始化,尽量推迟到实际使用时再初始化。
  4. 减少 Objective-C 类的数量:减少 Objective-C 类的数量和复杂性,可以降低 ObjC setup 时间。

通过这些优化措施,可以有效减少 Pre-main 时间,从而提升应用的启动速度。


二、准确计算 main 函数调用之前的耗时


三、计算 main 函数到首屏渲染完成的耗时(使用 os_signpost API 记录时间)

在应用的关键点添加 os_signpost 标记,并结合 Instruments 工具进行分析。

main 函数到完全呈现出第一个界面的时间,特别是在第一个界面涉及网络请求的情况下,计算这一段时间可以通过以下几步来实现:

步骤 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 工具进行分析

  1. 打开 Xcode 并选择你的项目
  2. 选择 Product > Profile 或使用快捷键 Command + I 启动 Instruments。
  3. 在 Instruments 中选择 Signpost Instrument 模板并点击 Choose
  4. 在 Instruments 界面中,点击 Record 按钮,启动你的应用。
  5. 在左侧的活动记录(Activity Trace)中,可以看到 App LaunchFetch Data 的开始和结束时间。

通过这种方法,可以准确地测量从 main 函数开始到第一个界面完全呈现出来的时间,包括网络请求所花费的时间。

四、计算 main 函数到首屏渲染完成的耗时(手动记录 TimeInterval 时间戳)

在关键点手动记录时间戳,并计算总耗时。

步骤 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:运行并查看输出

  1. 运行你的应用程序
  2. 在调试控制台中查看总的启动时间,包括网络请求的时间。

五、对于计算 main 函数之前时间的小结

通过上述方法,可以准确计算从 main 函数到第一个界面完全呈现出来的时间,尤其是在涉及网络请求的情况下。使用 os_signpost API 结合 Instruments 工具进行分析,可以提供更详细和精确的性能测量数据。而手动记录时间戳的方法则相对简单,但同样能提供一个大致的时间估计。

  • 42
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

依旧风轻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值