You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

UIKit中如何准确捕获首帧渲染耗时?

关于App启动阶段首帧及扩展阶段耗时精准捕获的问题

问题背景

为应用实现遥测功能,尝试捕获WWDC 2019《Optimizing App Launch》视频中提到的各阶段耗时,目前难以精准捕获首帧及扩展阶段的时间。

我的假设

  • application:didFinishLaunchingWithOptions:方法在首帧渲染前调用(如视频所述)
  • 首帧会在当前RunLoop结束后完成渲染

捕获代码

void PerformWhenIdle(dispatch_block_t block) {
  CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,
    kCFRunLoopAfterWaiting,
    NO,
    0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
      block();
    });
}

// ----------------------------
// AppDelegate.m
- (BOOL)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [logger logFirstFrameRenderingStarted];
  PerformWhenIdle(^{
    [logger logFirstFrameRenderingFinished];
    [logger logExtendedStageStarted];
  });
}

// ----------------------------
// in TableVC.m
- (void)viewDidAppear {
  [self.tableView reloadData];
  PerformWhenIdle(^{
    [logger logExtendedStageFinished];
  });
}

遇到的问题

通过Instruments查看Signpost时,首帧的起始点较为准确,但结束点偏差较大,导致扩展阶段时长显示异常:Signpost显示225.23ms,而App生命周期仪器显示仅1.78ms。

疑问

  1. 我的假设是否错误?若错误,是否意味着仪器数据有误?
  2. 若假设错误,应如何精准捕获首帧渲染的起止时间?

解答

1. 你的假设存在错误,仪器数据是准确的

application:didFinishLaunchingWithOptions:确实在首帧渲染前调用,但首帧渲染完成的时机并非当前RunLoop结束后。你使用的kCFRunLoopAfterWaiting事件触发时机太晚,它会等到RunLoop进入等待状态才执行,而首帧渲染实际上在RunLoop的kCFRunLoopBeforeWaiting之前的循环迭代中就已经完成——具体来说,首帧渲染是在UIApplicationMain事件循环处理CATransaction提交阶段完成的,通常发生在applicationDidFinishLaunching返回后的第一个RunLoop周期内的kCFRunLoopBeforeWaiting之前。

另外,用viewDidAppear触发的PerformWhenIdle标记扩展阶段结束也不准确,viewDidAppear执行后RunLoop可能还会处理其他任务,且扩展阶段的定义是首帧渲染完成后到应用进入idle状态前的初始化工作,和viewDidAppear没有直接绑定关系。

仪器的App生命周期数据是系统层面的精准统计,不存在错误,问题出在你的捕获逻辑对首帧和扩展阶段的时机判断有误。

2. 精准捕获首帧渲染起止时间的正确方式

方式一:利用系统Signpost或私有API(调试用,注意上架风险)

苹果系统本身会标记首帧渲染的Signpost,可直接监听com.apple.UIKit.signpost类别下的first-render事件。如果需要手动标记,可借助UIKit私有API(仅用于调试,不可上架):

// 首帧开始标记
extern void UIApplicationNotifyFirstRenderStart(void);
// 首帧结束标记
extern void UIApplicationNotifyFirstRenderEnd(void);

调用这两个函数能和系统仪器的数据完全对齐。

方式二:基于RunLoop观察者的正确时机

若不想用私有API,可调整RunLoop观察者的事件类型,结合CATransaction提交状态判断:

void ObserveFirstFrameCompletion(void) {
    static BOOL firstFrameCompleted = NO;
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,
                                                                       kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                                                       YES,
                                                                       0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        if (!firstFrameCompleted && [UIApplication sharedApplication].keyWindow) {
            // 检查无未提交的CATransaction,判定首帧完成
            if (![CATransaction flush]) {
                firstFrameCompleted = YES;
                [logger logFirstFrameRenderingFinished];
                [logger logExtendedStageStarted];
                CFRunLoopObserverInvalidate(observer);
            }
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
}

// 在application:didFinishLaunchingWithOptions:中调用
- (BOOL)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [logger logFirstFrameRenderingStarted];
    ObserveFirstFrameCompletion();
    return YES;
}

该逻辑会在主RunLoop即将进入等待状态前,检查KeyWindow存在且无未提交事务时,判定首帧渲染完成,时机更接近系统实际的首帧结束点。

扩展阶段结束的标记

扩展阶段结束的标志是应用进入真正的idle状态,即主RunLoop无待处理任务。可通过监听kCFRunLoopAfterWaiting事件,连续检测到多次RunLoop进入等待状态后标记:

void ObserveExtendedStageCompletion(void) {
    static NSInteger idleCount = 0;
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,
                                                                       kCFRunLoopAfterWaiting,
                                                                       YES,
                                                                       0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        idleCount++;
        // 连续2次进入等待状态,判定为进入idle
        if (idleCount >= 2) {
            [logger logExtendedStageFinished];
            CFRunLoopObserverInvalidate(observer);
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
}

// 在首帧完成后调用此函数

内容的提问来源于stack exchange,提问作者Emanuel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 04:18:16