外观
启动时间采集现状、无法完整采集的根因与已做尝试
面向后续维护者。记录 iOS 启动时间(App Launch 九阶段)在当前架构下的真实采集表现、为什么对部分 App 无法 100% 完整采集、以及本轮围绕该问题做过的全部尝试与真机(E4,设备 A / iPhone15,4 / iOS 17.0)证据。核心结论:归因类工具 bug 已修复;剩余不完整来自「App 自身不发信号」与「设备侧最早事件的采集竞态」两类;后者对 App Store 生产 App 无确定性软件解(受 get-task-allow 限制)。
1. 启动九阶段模型与数据来源
启动分析基于设备 CoreProfile(coreprofilesessiontap)的内核 kdebug 生命周期事件(class 0x1F07 dyld/libSystem、0x2B87 UIKit signpost、0x2BDC 过渡、0x31CA 首帧)。九个阶段(PHASES):
- System Interface Initialization(必需)
- Static Runtime Initialization(必需)
- UIKit Initialization(必需)
- UIKit Scene Creation(必需)
- willFinishLaunchingWithOptions()(可选回调)
- didFinishLaunchingWithOptions()(必需)
- sceneWillConnectTo()(可选回调)
- sceneWillEnterForeground()(可选回调)
- Initial Frame Rendering(必需,Total 端点)
每阶段由一对 BEGIN/END kdebug 事件配对成 span,取内核时间戳差(mach ticks 经时钟系数换算)。质量语义:candidate(完整 span)/partial(有值但边界不完整)/missing(未成 span)/not-applicable(App 未运行该回调)。值均为真实内核事件差,不补 0、不估算(诚实红线)。
2. 当前采集表现(E4:设备 A × 10 App × 2 轮)
- 轻/中量 App(微信、支付宝、京东等):稳定 9/9 candidate。微信两轮均 9/9。
- 早期阶段偶发缺失(淘宝、高德、抖音某些轮次):System/Static/UIKit Init 间歇性 missing——同一 App 轮次间波动(例:高德 r1 缺 UIKit Init/Static、r2 全齐;京东 r1 缺、r2 全)。
- 重型 App 的 scene 阶段(QQ、美团、北京银行、抖音):
sceneWillConnectTo/sceneWillEnterForeground稳定缺失,现已判not-applicable并隐藏(见 §4.1)。 - 重型 App 的 Initial Frame / 尾段(百度网盘,首帧渲染慢,总时长 1.2–2.3s):完成等待改为 liveness 判据后(见 §4.1),源仍在传输就逐段续期、停滞才收尾。真机四轮回归:百度网盘、高德各 3/4 完整;剩余各 1 次不全经原始遥测确认为设备采集竞态(百度=头部 UIKit Init 丢、高德=中/尾段 sceneWillConnect→首帧区段丢,
lossFree均为 true → 非主机队列丢),属 §3.2 无确定性解,非本次可修的工具 bug。
3. 无法完整采集的三类根因(均 E4 证据支撑)
3.1 App 自身不发信号(App 行为,非工具 bug)
对 QQ 用 Apple 官方 System Trace 抓冷启动(52639 条 QQ kdebug 行):
- QQ 进程 0 条
0x2B87UIScene 生命周期事件; sceneWillConnectTo/EnterForeground的 code300/312/313全系统不存在。
即 QQ/美团/北京银行/抖音这类重型 App(自定义启动/闪屏/预连接场景)冷启动不发标准 UIScene willConnect/EnterForeground signpost。微信/淘宝/京东会发,我们的工具都能采到。连 Apple 自己的 xctrace/System Trace 也拿不到(因为信号不存在)。→ 归类 not-applicable、隐藏,符合诚实红线。
3.2 设备侧最早事件的采集竞态(间歇、随机、非我方丢包)
对高德 r1(缺 UIKit Init/Static)做原始 kdebug 离线重放取证:
UIKit Init BEGIN(code 90)在整条捕获流的任何线程都不存在(count=0),r2 却有;sourceQuality.lossFree=true——排除了主机侧环形缓冲/队列丢弃,也不是配对/归因 bug(事件不在,无从配对)。
校正(codex 复核):lossFree=true 只覆盖主机侧队列,不证明整条链完整——设备内核 kdebug 缓冲溢出、设备→主机传输链上的丢失都不在其观测范围内。因此上面的取证只能排除"主机队列丢的"与"配对/归因 bug"两种,"设备侧最早事件竞态导致覆盖/未送达"目前是最合理推断,但尚未被硬证据闭合:要把"设备缓冲溢出丢失"与"App 根本不发"彻底分开,需要采集链可靠透出并计数设备的 TRACE_LOST_EVENTS 内核丢事件标记(该审计尚未落地,列入后续方案 A)。
在此前提下的推断:非挂起冷启动时,App 进程开始执行后头几毫秒的最早事件与「设备 kdebug缓冲 → tap → 主机」链路的刷新节奏/每核缓冲状态之间存在竞态。命中好窗口→全采到;落进缝隙→最早事件在被抽走前被覆盖/未送达。哪个 CPU 核启动、缓冲多满、调度时机都在变 → 每次冷启动独立地有一定概率漏采(双向随机:见矩阵中 r1/r2 互有缺失)。
3.3 架构限制:唯一确定性修法被 get-task-allow 挡死
能彻底消除 §3.2 竞态的唯一手段是挂起启动:request_launch(start_suspended=True) 把 App 冻结在 main() 之前 → 确认 tap 在流式 → 再 resume。挂起期间 App 不发任何事件,等 tap 确认后解冻,最早事件必然落在窗口内(非预热/Fixture 路径正是这样做,对可调试 App 有效)。
但真机验证:对高德 start_suspended=True 直接报
"The app must be debuggable and signed with 'get-task-allow'. Permission to debug com.autonavi.amap was denied. Unable to acquire task port."
挂起启动需要 debug 权限(get-task-allow),App Store 生产 App 没有 → 被系统拒绝。 这与 xctrace 无法启动生产 App 同源。这就是预热路径当初就用「非挂起启动」的根本原因——不是设计疏忽,而是生产 App 只能非挂起启动。故 §3.2 竞态对生产 App 无确定性软件解。
4. 已做的尝试与结论(逐项)
4.1 已交付且有效
- 两遍生命周期归因(
launch_trace.py_rebuild_semantics):先建完整 thread→pid 表再发标记,早期阶段不再因「目标线程绑定记录晚到」被丢。E4:System/Static missing 3→0、willFinish 4→1,淘宝/京东恢复,微信保持 9/9,无退步。 - scene/可选回调 not-applicable 分类(startup-timing §7 v45):Total 有效(必需齐全)时,完全未观测的可选回调判
not-applicable;孤立的、被误归因的 END(无 BEGIN,§3.1 的幻影)也判not-applicable,不再显示为 missing;有完整 span 或仍开启 BEGIN 的真实缺口仍保留 missing。工作台隐藏not-applicable阶段。E4:QQ/北京银行两轮 scene+willFinish 全 not-applicable(隐藏),connectTo missing 10→3、enterFG 12→5。- 校正(codex 复核):
not-applicable的正当性依赖 §3.1 的独立证据(System Trace 全系统 0 条),而非仅凭"没观测到 BEGIN"——"未见 BEGIN ≠ 回调没执行",早期竞态(§3.2)同样会只留下孤立 END。隐藏not-applicable只是呈现选择,不改变真实采集完整度;证据不足时保持 missing。
- 校正(codex 复核):
- 完成等待改为 liveness 判据(尾部/中段缺失修复):解码器暴露单调递增的
decodedRecordCount;基础宽限期(STARTUP_CORE_PROFILE_COMPLETION_GRACE_SECONDS,4s)到点时,若源仍在吐新记录 → 消费一次短探测延长(STARTUP_CORE_PROFILE_LIVENESS_PROBE_SECONDS,1s)并随记录持续到来逐段续期至硬上限(STARTUP_CORE_PROFILE_MAX_WAIT_SECONDS,15s);若记录停滞 → 立即收尾(设备丢包,再等无用)。用户立即停止始终优先。回归测试:test_startup_flush_publishes_total_then_restarts_frame_source/test_startup_flush_extends_for_open_initial_frame。- 演进原因(真机四轮回归 + codex 复核):初版窄修法仅在
didFinishLaunchingOpen/initialFrameOpen时延长。百度网盘、高德各四轮验证显示:①该延长分支一次都没触发(extended全程 false,超时那刻开着的常是更早的sceneWillConnectTo/UIKit Scene Creation);②按"哪个阶段 open"逐个加白名单既不完备、也无法区分"启动慢"与"设备丢尾段"。改用 liveness 后,跟得住慢但在传输的启动、对停滞快速放弃,比阶段白名单更普适。
- 演进原因(真机四轮回归 + codex 复核):初版窄修法仅在
- 预热切换 rebind 降级标志传播:空闲切换 App 时不再丢失
startupSourceUnavailable,避免误判"CoreProfile 通道未武装"致开始按钮置灰。
4.2 尝试后否决/回退
- CoreProfile
bm(buffer mode)0→1:真机测 QQ,scene 仍缺,无效,已回退。缓冲大小未被 pymobiledevice3 config 暴露,且扩大设备缓冲越过「pymobiledevice3 为唯一传输所有者、仅对齐冻结配置」的架构边界。 - 挂起启动(方案2):见 §3.3,真机确认对生产 App 被 get-task-allow 拒绝、会导致所有生产 App 采集失败,已回退。
- 自动补采一轮:分析后否决——§3.2 是每次独立的概率事件,补采只是再掷一次骰子,不保证完整(甚至可能第一次全、补采反而缺);且表现为 App 连续冷启动两次,可见、耗时、语义乱。
4.3 取证手段
- Apple xctrace / System Trace:确认 §3.1(生产 App 不发 scene signpost);但
--launch对生产 App 被 get-task-allow 拒、--all-processes对第三方 App 生命周期抓得浅——Apple 自己也无法完整取生产 App 的启动生命周期,我们的 CoreProfile tap 反而更全。 - 离线原始 kdebug 重放:确认 §3.2(最早事件 count=0、lossFree=true)。
5. 结论、当前可用性与诚实边界
- 归因类工具 bug 已消除;大多数轻/中量 App 启动九阶段可稳定完整采集。
- 剩余不完整分两类,均非可修的工具 bug:
- 重型 App 不发 scene 回调(App 行为,已
not-applicable隐藏); - 最早事件的设备采集竞态(间歇、随机;对生产 App 因 get-task-allow 无确定性解)。
- 重型 App 不发 scene 回调(App 行为,已
- 务实缓解:需要完整启动数据时对目标 App 多冷启动几次取最完整的一轮;工具已如实标注缺失与原因,不误导。
- 不做:不为"好看"把真实 missing 隐藏;不越传输所有者边界改设备缓冲;不对生产 App 用挂起启动。
6. 证据索引(E4,设备 A / iPhone15,4 / iOS 17.0,匿名别名)
- 10 App × 2 轮启动矩阵(两遍归因前后对比)。
- QQ / 微信离线码级重放(scene code 300/301/312/313 存在性对比)。
- QQ Apple System Trace(52639 行 QQ kdebug、0 条 0x2B87、code 300/312/313 全系统缺)。
- 高德 r1 离线重放(UIKit Init BEGIN count=0、lossFree=true)。
- 高德
start_suspended=True的 get-task-allow 拒绝错误。
7. 设备侧丢事件标记的直接观测(Phase A 单次样本回放)
用只读脚本 MacApp/scripts/startup-integrity-reconcile.py 对已落盘的样本回放(emit_completed_trace=False、retain_all_records=True),做"接收/落盘 ↔ 解码(任意线程 BEGIN/END) ↔ retain-all 报告"逐阶段照合,并扫描内核 TRACE_LOST_EVENTS(0x07020008)。
样本(各一次冷启动):
| 样本 | 缺失段落(any BEGIN/END) | TRACE_LOST_EVENTS | 解除解码期裁剪后 |
|---|---|---|---|
| 高德(尾部) | sceneWillEnterForeground 0/0;Initial Frame 0/13(BEGIN 缺);sceneWillConnectTo 1/0 | 2 | 未恢复缺失阶段 |
| 百度(头部) | willFinish 0/0;didFinish 0/0;UIKit Init 1/0;Initial Frame 0/20 | 3 | 未恢复缺失阶段 |
| 对照(完整) | 九阶段齐全 | 7 | — |
收紧后的结论(严格限定在本脚本证据范围内):
- 已直接观测到设备侧丢事件标记;对现有样本解除解码期裁剪后,未恢复缺失阶段。
- 完整轮同样带非零标记(7 条)→ 标记存在 ≠ 段落缺失;是"丢到哪些码"的抽签。host 侧
lossFree=true与deviceLostEventMarkerCount>0并存,再次印证lossFree只覆盖主机队列。
不外推为(超出本脚本证据范围):
- 并非"所有接收、落盘前裁剪与交接都已排除"(现有样本本就由裁剪链路产出,无法反证落盘前交接);
- 并非"每一个缺失阶段都已证明由设备丢失导致"。
生产报告透出(本批已实现):启动报告 sourceQuality.deviceLostEventMarkerCount 透出本轮标记数,UI 显示"本轮检测到设备丢事件标记 N 条"。语义护栏:N 为标记数、非丢失事件总数;字段缺省显示"未检查",为 0 显示"未观测到丢失标记(≠ 全链无损)";阶段覆盖与源完整性分开显示(九阶段齐全可与设备丢失告警并存);计数限定于启动采集窗口,不重复计入预热/重复解码/报告刷新。
后续实验触发条件(暂不启动 Phase B):先随正常使用积累样本,分别统计①丢失标记出现率、②必需阶段完整率、③丢失与采集负载的关系。仅当出现"高负载明显伴随更多丢失"等信号,再做精简单源采集对照;届时验证的是降低后续采集的丢失概率,而非找回已丢事件。九阶段继续保持 partial/candidate,不承诺单次稳定采齐。
版本历史
- 2026-09-15 v2:新增 §7——用只读回放脚本直接观测到设备侧
TRACE_LOST_EVENTS标记;生产报告透出deviceLostEventMarkerCount(含语义护栏与显示口径);给出收紧结论与 Phase B 触发条件;样本高德=2/百度=3/完整=7。 - 2026-09-14 v1:首次记录启动采集现状、三类根因与全部尝试;关联启动阶段与预热修复选项与启动指标知识页 §7 v45。