Pre-main 时间打点,统计 pre-mian 耗时的具体实现
- 记录进程创建时间
在 UIApplication+LJUIProbe.m:190–208,我们 swizzle 了 -setDelegate:,调用
ljtol_current_task_create_timestamp()(通过 sysctl 读取当前进程的 p_starttime)并写入“海神”埋点对象,标记为
LJUPAppStateProc,这样就把 dyld 加载、类加载(+load)这段系统逻辑也纳入统计。
2. 记录 main 入口时间
- 又在同一个文件的 ljup_UIApplicationMain(UIApplication+LJUIProbe.m:130–134)里用 fishhook hook 了
UIApplicationMain,首次调用 ljuplaunch_appendMainStamp()。但 wrapper 已经是在 main 函数内部调用 UIApplicationMain 时才执行,严格来说它标记的是 UIApplicationMain 调用边界,不等于 main 的第一条指令。
因此真正用于估算 pre-main 边界的点,应优先放在自己 App 的 main.m 顶部。fishhook 点可以作为兜底或一致性检查,但两个点要分开命名,不能混成同一个“精准 main 入口”。
接下来,我们还 hook 了 application:willFinishLaunchingWithOptions: 和 …didFinishLaunchingWithOptions:(同文件 140–
167 行)做后续埋点,最终就形成了“进程启动 → main 入口 → willFinish → didFinish”全流程闭环,从而精准量化了 Pre‑main
阶段的近似整体耗时。
具体代码实现
在我们的埋点方案里,并不是单纯记录 main 执行到 UIApplicationMain 的时长,而是同时对“进程创建”做了埋点,覆盖系统加载
阶段。关键在这段 swizzle 代码:
// Pods/LJBaseUIProber/.../UIApplication+LJUIProbe.m:190–208
[RSSwizzle swizzleInstanceMethod:@selector(setDelegate:)
inClass:[self class]
newImpFactory:^id(RSSwizzleInfo *info) {
return ^void (id self, id delegate) {
// 调用原始 setDelegate:
typedef void (*imp_t)(id, SEL, id);
((imp_t)[info getOriginalImplementation])(self, info.selector, delegate);
// 记录进程创建时间(系统层面)
ljtol_time_msec procCreate = ljtol_current_task_create_timestamp();
if (procCreate) {
LJUPDot *dot = [LJUPDot dotWithType:LJUPAppStateProc
moment:LJUPDotMomentNone
timestamp:procCreate];
[LJPROBER_APP_LAUNCH.launch addDot:dot];
}
// Hook 后续 willFinish/didFinish 埋点...
ljup_UIApplication_launch_hook(delegate);
};
} mode:RSSwizzleModeOncePerClass key:"ljup_setDelegate"];
- ljtol_current_task_create_timestamp() 通过 sysctl(KERN_PROC_PID) 读取 kinfo_proc.kp_proc.p_starttime。它是进程记录里的 wall-clock 时间,不是与 mach_continuous_time 相同的单调时钟源。
- 我们把它标记为 LJUPAppStateProc(进程启动点),因此即使在 main 之前、系统调用流程里(dyld 加载 Mach‑O、+load 执行
等)也能纳入统计。
随后在拦截的 UIApplicationMain wrapper 里,再次打点 main 时刻:
// Pods/.../UIApplication+LJUIProbe.m:130–134
static int ljup_UIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName) {
// 记录 main 到 UIApplicationMain 入口
ljuplaunch_appendMainStamp();
return ljup_origin_UIApplicationMain(argc, argv, principalClassName, delegateClassName);
}
并在 JGWorkflow/main.m 顶部手动补一遍,保证任意路径下都能准确触发:
// JGWorkflow/main.m:17–20
int main(int argc, char * argv[]) {
ljuplaunch_appendMainStamp();
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
这样一来,从系统创建进程(LJUPAppStateProc)→执行到 main(LJUPAppStatePreMain)→willFinish/didFinish,整个启动流程都
能形成一条启动时间线。但要确认各时间戳使用同一时钟域、单位和设备时间校正策略,否则相减会引入误差。Apple 公开的 MetricKit 启动指标、Instruments App Launch 模板和 DYLD_PRINT_STATISTICS(仅调试诊断)也应作为交叉验证,而不是只信一套 hook。
代码实现细节
// Pods/ljtools/ljtools/Classes/inlines/ljtol_timestamp.h:41-58
LJTOL_STATIC_INLINE
ljtol_time_msec ljtol_current_task_create_timestamp(void) {
int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid() };
struct kinfo_proc *info = (struct kinfo_proc *)ljtol_system_control(mib, 4, NULL);
struct timeval tv = info->kp_proc.p_starttime;
ljtol_time_msec timestamp = tv.tv_sec * 1000 + tv.tv_usec / 1000.;
free(info);
return timestamp;
}
“这段逻辑用 sysctl 向内核请求当前进程信息(CTL_KERN/KERN_PROC/KERN_PROC_PID/getpid()),拿到 struct kinfo_proc 后直接读取其中的 p_starttime 字段,转换为毫秒级时间戳返回。这样就能精确获得系统层面‘进程创建’时刻,补足 main 之前的启动成本统计。