WWDC Quick Look 💓 By SwiftGGTeam
Build a responsive camera app that launches quickly

Build a responsive camera app that launches quickly

观看原视频

Highlight

iOS 26 引入 Deferred Start API,允许相机 App 将非预览输出(如照片、视频输出)的初始化推迟到首帧预览渲染之后,配合 Responsive Capture 可实现启动时间减半,同时不丢失拍摄时机。

核心内容

相机启动慢,用户最直观的感受是什么

你打开相机 App,屏幕却是一片黑。等了几秒,预览画面终于出现,但想拍的那个瞬间已经过去了。演讲者用一个多米诺骨牌的例子说明了这个问题:红色骨牌即将倒下时打开相机,如果启动太慢,预览出现时骨牌早已翻倒。

这个痛点背后有四个阶段:App 启动加载、Session 配置与启动、输出对象初始化、预览帧开始流动。其中第三阶段——初始化 AVCaptureOutput——是最耗时的。

Deferred Start:把重活放到预览之后

以前的做法是所有输出在 startRunning() 时一并初始化。AVCapturePhotoOutputAVCaptureMovieFileOutput 这类重量级对象会拖慢整个启动流程,但它们对预览本身没有贡献。

Deferred Start 的核心思路很简单:只让预览输出先初始化,其他输出延后。每个 AVCaptureOutputAVCaptureVideoPreviewLayer 新增了 isDeferredStartEnabled 属性,设为 true 即可将该输出的初始化推迟。

02:05)演讲者展示了实验室对比:未启用 Deferred Start 时启动耗时接近 1 秒,启用后缩短到约 0.5 秒,启动速度翻倍。配置越复杂的 Session,收益越大。

预览出来了,但快门按不下去怎么办

Deferred Start 有个副作用:预览虽然快了,但照片输出还没初始化完,用户点快门没有反应。这在抓拍场景下是致命的。

Apple 的解决方案是 Responsive Capture。开启后,AVCapturePhotoOutput 会在内部增加缓冲,允许在输出完全初始化前就开始捕获。演讲者再次用多米诺骨牌演示:开启 Responsive Capture 的绿色手机成功抓拍到了红色骨牌倒下的瞬间,未开启的紫色手机则错过了。

14:07

持续性能:别让用户觉得相机”卡”

预览开始流动后,保持稳定的帧率同样重要。设备发热时系统会降频,如果 App 不做适配,预览就会掉帧。

AVCaptureSession 新增了 hardwareCost 属性(取值 0 到 1),表示当前配置占用的硬件资源比例。超过 1 表示配置超出了设备能力,需要在启动前降级。AVCaptureDevicesystemPressureState 则用于运行时监听系统压力,压力升高时应降低帧率或减少 GPU/ANE 使用。

20:16

ProRes 录制的 I/O 难题

高码率视频(如 ProRes)录制对磁盘写入速度要求极高。传统文件系统的 I/O 行为是不确定的——后台索引、内存碎片、存储磨损都会影响写入速度,导致掉帧。

iOS 27 引入 AVProVideoStorage,通过预分配存储空间来提供确定性的写入性能。它是个系统级单例,所有 App 共享。录制前检查 remainingCapacityisBusy,然后设置 usesProVideoStorage = true 即可。

22:17

详细内容

自动 Deferred Start

AVCaptureVideoPreviewLayer 渲染预览的 App,升级到 iOS 26 SDK 重新编译后默认启用自动模式。系统会在预览出现后不久自动完成剩余输出的初始化。

import AVFoundation

class DeferredStartDelegate: NSObject, AVCaptureSessionDeferredStartDelegate {
    func sessionWillRunDeferredStart(_ session: AVCaptureSession) {
        // 延迟初始化开始前调用,可在此创建后台资源
    }

    func sessionDidRunDeferredStart(_ session: AVCaptureSession) {
        // 延迟初始化完成后调用,所有输出已就绪
    }
}

let captureSession = AVCaptureSession()
captureSession.beginConfiguration()
captureSession.automaticallyRunsDeferredStart = true

let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer.isDeferredStartEnabled = false

let photoOutput = AVCapturePhotoOutput()
photoOutput.isDeferredStartEnabled = true
captureSession.addOutput(photoOutput)

captureSession.setDeferredStartDelegate(
    deferredStartDelegate,
    deferredStartDelegateCallbackQueue: sessionQueue
)

captureSession.commitConfiguration()
captureSession.startRunning()

关键点:

  • automaticallyRunsDeferredStart 在 iOS 26 SDK 下默认 true
  • videoPreviewLayer.isDeferredStartEnabled = false 确保预览优先初始化
  • photoOutput.isDeferredStartEnabled = true 将照片输出推迟
  • setDeferredStartDelegate 注册回调,用于感知延迟启动的进度
  • sessionQueue 应使用专门的串行队列,避免阻塞主线程

09:14

手动 Deferred Start

使用 AVCaptureVideoDataOutput 自定义预览渲染的 App,需要采用手动模式。通过 CAMetalLayeraddPresentedHandler 捕获首帧上屏时机,再触发延迟初始化。

import AVFoundation
import QuartzCore

let captureSession = AVCaptureSession()
captureSession.beginConfiguration()
captureSession.automaticallyRunsDeferredStart = false

let videoOutput = AVCaptureVideoDataOutput()
captureSession.addOutput(videoOutput)
videoOutput.isDeferredStartEnabled = false

let photoOutput = AVCapturePhotoOutput()
photoOutput.isDeferredStartEnabled = true
captureSession.addOutput(photoOutput)

captureSession.setDeferredStartDelegate(
    deferredStartDelegate,
    deferredStartDelegateCallbackQueue: sessionQueue
)

captureSession.commitConfiguration()
captureSession.startRunning()

// 在 Metal 渲染循环中
private var firstFramePresented = false

guard let drawable = layer.nextDrawable() else { return }
if !firstFramePresented {
    drawable.addPresentedHandler { _ in
        captureSession.runDeferredStartWhenNeeded()
    }
    firstFramePresented = true
}

关键点:

  • automaticallyRunsDeferredStart = false 关闭自动模式
  • videoOutput.isDeferredStartEnabled = false 确保自定义预览先就绪
  • addPresentedHandler 在首帧呈现后回调
  • runDeferredStartWhenNeeded() 通知系统可以开始延迟初始化
  • 手动模式适合需要精确控制启动流程的 App

11:30

启用 Responsive Capture

Responsive Capture 在 AVCapturePhotoOutput 上增加了一个缓冲层,允许在输出完全初始化前就开始捕获处理。

import AVFoundation

func configurePhotoOutput(for session: AVCaptureSession, device: AVCaptureDevice) {
    let photoOutput = AVCapturePhotoOutput()

    guard session.canAddOutput(photoOutput) else { return }
    session.addOutput(photoOutput)

    photoOutput.maxPhotoQualityPrioritization = .quality
    photoOutput.isResponsiveCaptureEnabled = photoOutput.isResponsiveCaptureSupported
}

关键点:

  • isResponsiveCaptureSupported 先检查设备是否支持
  • isResponsiveCaptureEnabled = true 开启响应式捕获
  • 与 Deferred Start 配合使用效果最好
  • 以略微增加处理延迟为代价,换取快门响应速度

14:07

监控系统压力

import AVFoundation

let captureSession = AVCaptureSession()
let device = activeVideoInput?.device
captureSession.beginConfiguration()
// ... 配置 session ...
captureSession.commitConfiguration()

guard captureSession.hardwareCost <= 1.0 else {
    print("hardwareCost \(captureSession.hardwareCost) — cannot start session. Reconfiguring.")
    setupLowCostConfiguration()
}

captureSession.startRunning()

let systemPressureObserver = device?.observe(
    \.systemPressureState,
    options: [.initial, .new],
    changeHandler: { device, change in
        // 根据压力状态调整帧率或降低 GPU/ANE 使用
    }
)

关键点:

  • hardwareCostcommitConfiguration() 后检查,超过 1.0 需降级配置
  • systemPressureState 通过 KVO 监听,实时响应系统压力变化
  • 压力升高时可降低帧率、使用 binned format 或减少 UI 工作量

20:16

使用 AVProVideoStorage

import AVFoundation

func configureProVideoStorage() {
    guard AVProVideoStorage.isSupported else { return }
    let storage = AVProVideoStorage.shared
    guard storage.remainingCapacity != 0 else {
        storage.openSettings()
        return
    }
}

// 录制配置
guard AVProVideoStorage.isSupported else { return }
guard let pvs = AVProVideoStorage.shared else { return }

let movieOutput = AVCaptureMovieFileOutput()

guard movieOutput.isProVideoStorageSupported else { return }
guard !pvs.isBusy else { return }

let movieFileURL = FileManager.default.temporaryDirectory
    .appendingPathComponent(UUID().uuidString)
    .appendingPathExtension("mov")

movieOutput.usesProVideoStorage = true
movieOutput.startRecording(to: movieFileURL, recordingDelegate: delegate)

关键点:

  • AVProVideoStorage.isSupported 检查设备是否支持
  • remainingCapacity 检查剩余预分配空间,为 0 时引导用户到设置
  • isBusy 检查存储是否正被其他任务占用
  • isProVideoStorageSupported 确认输出支持该特性
  • usesProVideoStorage = true 开启后,录制文件先写入预分配空间,结束后再移到目标位置
  • AVAssetWriter 也支持 usesProVideoStorage 属性

22:17

核心启发

  • 做一款”秒开”的社交相机 App:用 Deferred Start 让预览在 0.5 秒内出现,配合 Responsive Capture 确保用户打开 App 瞬间就能抓拍。实现思路:自动模式下只保留预览层,照片输出延迟初始化,快门按钮在 sessionDidRunDeferredStart 回调后才变为完全可用状态。

  • 为直播 App 添加发热降频保护:通过监听 systemPressureState,在设备过热时自动降低采集分辨率或帧率,避免直播画面卡顿或 App 被系统强制终止。实现思路:在 changeHandler 中根据压力等级切换 activeFormat,使用 frameRateOverride 精确控制帧率。

  • 在 iPhone 上实现 ProRes 专业录制:利用 AVProVideoStorage 消除高码率录制的 I/O 抖动,为视频创作者提供稳定的专业录制体验。实现思路:录制前检查 remainingCapacityisBusy,设置 usesProVideoStorage = true,录制结束后从临时目录移出文件。

  • 用 VideoDataOutput 做实时滤镜预览:自定义 Metal 渲染管线,在 AVCaptureVideoDataOutputcaptureOutput(_:didOutput:from:) 中对每帧应用滤镜后再呈现。实现思路:采用手动 Deferred Start,首帧上屏后调用 runDeferredStartWhenNeeded(),同时把照片输出延迟初始化。

  • 在相机启动时做 UI 分级加载:第一阶段只显示预览和快门按钮,第二阶段在 sessionDidRunDeferredStart 后淡入相册缩略图、模式选择器等非关键 UI。实现思路:将 UI 创建逻辑拆分到两个方法中,通过 Deferred Start 的 delegate 回调触发第二阶段。

关联 Session

评论

GitHub Issues · utterances