didFinishLaunching 与「主线程首次 idle」, 哪个是更优的启动结束时间点 ?

结论先行

在这两个候选时间点里——

  1. application:didFinishLaunchingWithOptions: 执行结束
  2. 主线程第一次进入 idle(RunLoop kCFRunLoopBeforeWaiting

若你只能二选一,以「主线程首次 idle」作为 启动结束 更合理。它比 didFinishLaunchingWithOptions: 更贴近用户真正“看到并可操作界面”的时刻,而误差仍可控制;同时埋点方案也比较稳健,可跨 UIKit / SwiftUI / SceneDelegate 使用。


为什么 didFinishLaunching… 偏早?

  • Apple 的 MXAppLaunchMetric 只统计到 didFinishLaunch()(即 didFinishLaunching…)为止,Uber 等大型团队实测后认为这比他们旧的 首帧渲染 指标“缩水”了一截,需要再补一段自定义测量来涵盖 UI 绘制流程citeturn10view0。
  • didFinishLaunching… 返回时,UI 还没走完 viewDidLoad → viewWillAppear → first draw → viewDidAppear,因此用户仍在看 LaunchScreen 或白屏。把它当作结束点会 乐观低估 启动耗时。

为什么选 “RunLoop 首次 idle” 更合适?

维度didFinishLaunching…RunLoop 首次 idle
用户可见性UI 尚未出现首帧已提交,布局/动画基本结束
度量一致性受初始化代码多少影响大对业务初始化代码鲁棒;只要主线程不被长任务阻塞,就在首个空闲循环采样
实现复杂度最简单略复杂,需要 CFRunLoopObserver
与 Apple 报表对齐❶ 对齐 MXAppLaunchMetric需要自行上报,与官方报表有 10‑50 ms 左右偏移

RunLoop idle 发生点kCFRunLoopBeforeWaiting——系统即将把主线程睡眠,表明当前循环所有 Timer、Source、Layout、展示任务都完成了,首次出现通常紧跟在根 VC 的 viewDidAppear: 之后citeturn13view0。


实战埋点模板(一次性)

/// 在 didFinishLaunching… 末尾安装一次观察者
static void InstallFirstIdleObserver(void) {CFRunLoopRef rl = CFRunLoopGetMain();static CFRunLoopObserverRef sObs;CFOptionFlags activities = kCFRunLoopBeforeWaiting;   // 首次 idlesObs = CFRunLoopObserverCreateWithHandler(NULL, activities, /*repeat*/false, 0, ^(CFRunLoopObserverRef obs, CFRunLoopActivity act) {uint64_t t = mach_absolute_time() - g_appStartTime;[APMLogger recordColdLaunchIdle:t];               // 自定义上报CFRunLoopRemoveObserver(rl, obs, kCFRunLoopDefaultMode);CFRelease(obs);});CFRunLoopAddObserver(rl, sObs, kCFRunLoopDefaultMode);
}

要点

  1. g_appStartTimemain() 里用 mach_absolute_time() 采集。
  2. 只监听一次;如果用户在冷启动流程中立即进入后台可在回前台时重新安装。
  3. 避免在 didFinishLaunching… 里做重量级同步任务,否则 idle 会被推迟。

何时仍可选 didFinishLaunching…

  • 无需精细区分 冷 / 预热 / 热启动,只想和 MetricKit 或 Firebase Perf 默认数据保持同一口径。
  • 业务里真正耗时的初始化都放在 application(_:didFinishLaunching…) 之前(+loadstatic 初始化或主函数),后续 UI 几乎“秒绘”。

TL;DR

“RunLoop 首次空闲” 把 UI 绘制、布局、动画都算进启动耗时,比 didFinishLaunchingWithOptions: 更贴近用户实际体验,且仍保持实现简洁可维护,是多数 APM 场景更合理的 启动结束 定义。


完整代码

下面给出一套 Objective‑C 冷启动监控模板,满足两点要求:

  1. application:didFinishLaunching… 末尾安装一次 RunLoop idle 观察者
  2. 若用户在冷启动阶段马上切后台,再次回前台时自动重装观察者

⚙️ 如需 Swift 版本,可把 C‑API 调用 (CFRunLoopObserver…) 直接放到 Swift AppDelegate 中,逻辑完全一致。

// AppDelegate.m
#import "AppDelegate.h"
#import <mach/mach_time.h>// 记录进程启动瞬间:在 main() 里做
uint64_t gAppStartTime = 0;
__attribute__((constructor))
static void markProcessStart(void) {gAppStartTime = mach_absolute_time();
}/* ------------ 冷启动 RunLoop idle 监控核心 ------------ */@interface AppDelegate ()
@end@implementation AppDelegate {CFRunLoopObserverRef _idleObserver;   // 当前安装的观察者BOOL _coldLaunchDone;                 // 是否已采集完成
}#pragma mark - Observer 安装 / 卸载- (void)installIdleObserverIfNeeded {if (_coldLaunchDone || _idleObserver) return; // 已完成或已安装CFRunLoopRef rl = CFRunLoopGetMain();_idleObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,kCFRunLoopBeforeWaiting,          // RunLoop 即将 idle/*repeats*/ false,                // 只触发一次0,                                // order^(CFRunLoopObserverRef obs, CFRunLoopActivity act) {uint64_t elapsed = mach_absolute_time() - gAppStartTime;[self reportColdLaunch:elapsed];      // ← 你的上报逻辑_coldLaunchDone = YES;CFRunLoopRemoveObserver(rl, obs, kCFRunLoopDefaultMode);CFRelease(obs);_idleObserver = NULL;});CFRunLoopAddObserver(rl, _idleObserver, kCFRunLoopDefaultMode);
}- (void)cancelIdleObserverIfNeeded {if (_idleObserver) {CFRunLoopRemoveObserver(CFRunLoopGetMain(), _idleObserver, kCFRunLoopDefaultMode);CFRelease(_idleObserver);_idleObserver = NULL;}
}#pragma mark - UIApplicationDelegate- (BOOL)application:(UIApplication *)applicationdidFinishLaunchingWithOptions:(NSDictionary *)launchOptions {// …你的初始化代码…// 1️⃣ 仅在冷启动安装一次观察者[self installIdleObserverIfNeeded];[self observeAppLifeCycle];   // 订阅前后台事件return YES;
}#pragma mark - 前后台切换处理- (void)observeAppLifeCycle {NSNotificationCenter *nc = NSNotificationCenter.defaultCenter;[nc addObserver:self selector:@selector(_didEnterBackground)name:UIApplicationDidEnterBackgroundNotification object:nil];[nc addObserver:self selector:@selector(_willEnterForeground)name:UIApplicationWillEnterForegroundNotification object:nil];
}- (void)_didEnterBackground {// 2️⃣ 若尚未结束冷启动,取消当前观察者if (!_coldLaunchDone) {[self cancelIdleObserverIfNeeded];}
}- (void)_willEnterForeground {// 2️⃣ 回前台时,若冷启动仍未完成 → 重新安装观察者[self installIdleObserverIfNeeded];
}#pragma mark - 上报- (void)reportColdLaunch:(uint64_t)elapsedMach {// 将 mach 时间转换为毫秒mach_timebase_info_data_t info;mach_timebase_info(&info);double ms = (double)elapsedMach * info.numer / info.denom / 1e6;NSLog(@"[APM] Cold launch Time‑to‑Idle = %.1f ms", ms);// 调用你自己的 APM / 埋点上报接口……
}@end

关键点说明

位置目的
installIdleObserverIfNeeded在主 RunLoop 进入第一次 idle (kCFRunLoopBeforeWaiting) 时触发;只安装一次,防止重复统计。
后台切换如果在冷启动尚未结束时收到 DidEnterBackground,先移除观察者;回到前台的 WillEnterForeground 再重新安装,保证最终一定能命中“首次 idle”。
_coldLaunchDone成功记录后置为 YES,后续不再重复安装。
mach_absolute_time → 毫秒使用 mach_timebase_info 做单位换算,避免因 CPU 频率变化带来的误差。

如需 Swift 写法,可用 RunLoop.main.add(_:forMode:)CFRunLoopObserverCreateWithHandler 的桥接版本,或直接使用 CFRunLoopObserverCreate 结合 Unmanaged<AnyObject>.fromOpaque 保存 self;逻辑保持一致即可。

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

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

相关文章

Vue3 + TypeScript中defineEmits 类型定义解析

TypeScript 中 Vue 3 的 defineEmits 函数的类型定义&#xff0c;用于声明组件可以触发的事件。以下是分步解释&#xff1a; 1. 泛型定义 ts <"closeDialog" | "getApplySampleAndItemX"> 作用&#xff1a;定义允许的事件名称集合&#xff0c;即组…

树莓派超全系列教程文档--(34)树莓派配置GPIO

配置GPIO GPIO控制gpio 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 GPIO控制 gpio 通过 gpio 指令&#xff0c;可以在启动时将 GPIO 引脚设置为特定模式和值&#xff0c;而以前需要自定义 dt-blob.bin 文件。每一行都对一组引脚应用相同的设…

AladdinEdu(H卡GPU算力平台)使用教程: 1)注册与开通流程 2)插件使用流程

一、注册与开通流程 首先进入AladdinEdu官网&#xff1a;AladdinEdu-同学们用得起的H卡算力平台-高效做AI就上Aladdin 完成注册&#xff0c;并进行学生认证&#xff1a;学生认证账户&#xff0c;认证期间享受教育优惠价。 登录官网进入控制台 二、插件使用流程 VScode中…

精益数据分析(6/126):深入理解精益分析的核心要点

精益数据分析&#xff08;6/126&#xff09;&#xff1a;深入理解精益分析的核心要点 在创业和数据驱动的时代浪潮中&#xff0c;我们都在不断探索如何更好地利用数据推动业务发展。我希望通过和大家分享对《精益数据分析》的学习心得&#xff0c;一起在这个充满挑战和机遇的领…

2.深入剖析 Rust+Axum 类型安全路由系统

摘要 详细解读 RustAxum 路由系统的关键设计原理&#xff0c;涵盖基于 Rust 类型系统的路由匹配机制、动态路径参数与正则表达式验证以及嵌套路由与模块化组织等多种特性。 一、引言 在现代 Web 开发中&#xff0c;路由系统是构建 Web 应用的核心组件之一&#xff0c;它负责…

运筹学之模拟退火

目录 一、历史二、精髓思想三、案例与代码实现 一、历史 问&#xff1a;谁在什么时候提出模拟退火&#xff1f;答&#xff1a;模拟退火算法&#xff08;Simulated Annealing&#xff0c;SA&#xff09;是由斯图尔特柯尔斯基&#xff08;Scott Kirkpatrick&#xff09; 等人在 …

android测试依赖

Android 项目中常用的测试相关库 1. androidx.arch.core:core-testing:2.2.0 作用&#xff1a; 提供与 Android Architecture Components&#xff08;如 LiveData、ViewModel&#xff09;相关的测试工具。主要用于测试基于 LiveData 的异步操作。 常见功能&#xff1a; 即时…

stack,queue和priority_queue

1. stack 1.1 stack 的介绍 栈是一种容器适配器&#xff0c;专门设计用于LIFO环境&#xff08;后进先出&#xff09;&#xff0c;其中元素仅从容器的一端插入和提取。 容器适配器&#xff0c;也就是使用特定容器类的封装对象作为其底层容器&#xff0c;提供一组特定的成员函…

MinnowBoard MAX单板UEFI BIOS代码编译教程

此教程用于UEFI EDK2代码的研究&#xff0c;虽然EDK2框架代码开源&#xff0c;但是都是在模拟器上跑仿真&#xff0c;差点意思&#xff0c;搞过嵌入式的应该有一个共识&#xff0c;是骡子是马&#xff0c;你得把板子点亮啊。MinnowBoard MAX单板是intel10多年前发布的软硬件全部…

AI Transformers 架构体系 权重文件类型 safeterson和gguf格式转换【2-1】

模型权重文件&#xff1a;存储训练好的模型参数,也就是w和b&#xff0c;是模型推理和微调的基础 .pt、.ckpt、.safetensors、gguf 配置文件&#xff1a;确保模型架构的一致性&#xff0c;使得权重文件能够正确加载 config.json、generation_config.json 词汇表文件&#xff1a;…

K8S微服务部署及模拟故障观测

概述 本文介绍了如何在 Kubernetes (K8S) 集群中部署微服务&#xff0c;并模拟常见的故障场景&#xff08;如 Pod 故障、节点故障、网络故障&#xff09;以测试系统的容错能力。通过本实验&#xff0c;了解 Kubernetes 的自动恢复机制以及如何通过监控和日志分析快速定位和解决…

OpenStack Yoga版安装笔记(23)Swift安装

一、官方文档 Object Storage Install Guide — Swift 2.29.3.dev5 documentation 二、环境准备 之前的实验&#xff0c;已经有controller, compute1, block1节点&#xff0c;并已经完成Keystone、Glance、Nova、Neutron、Cinder等主要OpenStack Service的安装。 此处新增…

06-libVLC的视频播放器:推流RTMP

创建媒体对象 libvlc_media_t* m = libvlc_media_new_path(m_pInstance, inputPath.toStdString().c_str()); if (!m) return -1; // 创建失败返回错误 libvlc_media_new_path:根据文件路径创建媒体对象。注意:toStdString().c_str() 在Qt中可能存在临时字符串析构问题,建议…

少儿编程路线规划

少儿编程路线规划—一文写明白 现在有很多的编程机构&#xff0c;五花八门的。我有幸也见识到了大家的营销策略。这些策略有黑有白吧&#xff0c;从业几年&#xff0c;沉淀下来一些客户角度的干货&#xff0c;分享给大家。 如果是想以很远很远的就业为目的&#xff0c;毕业就…

ios app的ipa文件提交最简单的方法

ipa文件是ios的app打包后生成的二级制文件&#xff0c;在上架app store connect或做testflight测试的时候&#xff0c;它提示我们需要使用xcode、transporter或xcode命令行等方式来上传。 而xcode、transporter或xcode命令行的安装都需要使用mac电脑&#xff0c;假如没有mac电…

怎么查看LLM Transformer 架构进行并行计算和设备映射

怎么查看LLM Transformer 架构进行并行计算和设备映射 num_hidden_layers = model.config.num_hidden_layers print(num_hidden_layers) print(model) LLM(大语言模型)通常是基于 Transformer 架构 构建的,它由多个模块化的层(Layer)堆叠组成,每个层都有其独特的作用。…

微信小程序获得当前城市,获得当前天气

// // 获取用户当前所在城市 // wx.getLocation({// type: wgs84, // 默认为 wgs84 返回 gps 坐标,gcj02 返回可用于 wx.openLocation 的坐标 // success: function(res) {// console.log(获取位置成功, res); // // 使用腾讯地图API进行逆地址解析 // wx…

美国国土安全部终止资助,CVE漏洞数据库项目面临停摆危机

&#xff08;图片来源&#xff1a;Jerome460 / Shutterstock&#xff09; 25年漏洞追踪体系即将终结 美国非营利研发组织MITRE宣布&#xff0c;其与美国国土安全部&#xff08;DHS&#xff09;签订的"通用漏洞披露&#xff08;CVE&#xff09;"数据库维护合同将于2…

Kafka下载和使用(Windows版)

Apache Kafka 是一个高吞吐量的分布式消息系统&#xff0c;广泛应用于日志收集、实时流处理等场景。本文将以 Windows 系统为例&#xff0c;详细介绍 Kafka 的安装和使用方法。 一、安装方式 在 Windows 系统上运行 Apache Kafka&#xff0c;通常有两种方式&#xff1a; 1.W…

RBAC的使用

1、简述RBAC的作用及工作流程 Rbac基于角色访问控制&#xff0c;用于管理用户对集群资源的访问权限&#xff0c;通过定义角色和绑定规则&#xff0c;将用户与权限进行关联&#xff0c;作用&#xff1a;权限精细化管理&#xff0c;操作便捷与统一管理&#xff0c;动态调整权限。…