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

使用AVAssetExportSession链式调用CAAnimation时代理回调顺序异常问题

解决AVVideoCompositionCoreAnimationTool中CAAnimationDelegate回调异常的问题

我之前碰到过一模一样的坑——用AVVideoCompositionCoreAnimationTool做离线视频导出时,CAAnimationDelegate的animationDidStop回调完全不靠谱,要么随机触发,要么顺序乱掉,加断点能正常运行纯粹是因为断点强制同步了线程,刚好补上了回调时序的漏洞。

问题根源

AVFoundation的离线视频渲染和实时UI的动画运行逻辑天差地别:

  • 实时UI里,动画由RunLoop驱动,回调时机相对稳定;
  • 离线导出时,AVFoundation会做批量渲染优化,不会严格按照实时RunLoop的时序触发delegate回调,甚至可能直接跳过某些回调,导致你在回调里更新图层状态的逻辑彻底失效。

至于移除代理后能正常运行,确实是fillMode = .forwards + isRemovedOnCompletion = false在起作用:这两个配置会让动画结束后自动保持最终的动画值,根本不需要你手动去更新图层属性——AVFoundation在渲染视频帧时会直接使用动画的最终状态,完全不依赖你在回调里的赋值操作。

修复方案

直接抛弃CAAnimationDelegate的回调逻辑,完全依托AVFoundation的动画合成机制来处理状态,具体步骤如下:

  1. 移除所有delegate相关代码:包括AnimationChainer类、设置delegate = chainer的代码,以及通过setValue(_:forKey:)传递图层和参数的逻辑。
  2. 保留动画的fillMode和移除配置:继续使用isRemovedOnCompletion = false和fillMode = .forwards,让AVFoundation自动维护动画结束后的状态。
  3. 手动设置图层最终状态(可选但推荐):为了避免渲染时的潜在状态不一致,可以在添加动画前直接把图层的最终状态设置好——AVFoundation会优先使用动画的关键帧值,不会影响动画效果,但能确保导出后的图层状态绝对正确。

修改后的核心代码

// 移除AnimationChainer类,不再需要delegate相关逻辑

/* 动画设置 */
let interval: CFTimeInterval = 2.0
let movingLayer = CALayer()

// 提前手动设置图层的最终状态(可选,确保导出后状态正确)
CATransaction.begin()
CATransaction.setDisableActions(true)
movingLayer.transform = CATransform3DMakeTranslation(0.0, 0.0, 0.0) // 动画最终要回到的位置
CATransaction.commit()

// 第一个平移动画
let translateOnceAnimation = CABasicAnimation(keyPath: "transform.translation.y")
translateOnceAnimation.duration = 0.4
translateOnceAnimation.fromValue = 0
translateOnceAnimation.byValue = -200
translateOnceAnimation.isRemovedOnCompletion = false
translateOnceAnimation.fillMode = .forwards
translateOnceAnimation.beginTime = AVCoreAnimationBeginTimeAtZero

// 第二个平移动画
let translateTwiceAnimation = CABasicAnimation(keyPath: "transform.translation.y")
translateTwiceAnimation.duration = 0.4
translateTwiceAnimation.fromValue = -200
translateTwiceAnimation.byValue = 200
translateTwiceAnimation.isRemovedOnCompletion = false
translateTwiceAnimation.fillMode = .forwards
translateTwiceAnimation.beginTime = AVCoreAnimationBeginTimeAtZero + interval

movingLayer.add(translateOnceAnimation, forKey: "translateUp")
movingLayer.add(translateTwiceAnimation, forKey: "translateDown")

let parentLayer = CALayer()
parentLayer.addSublayer(movingLayer)

/* 导出设置 */
let compo: AVMutableComposition!
let videoCompo: AVMutableVideoComposition!
let videoLayer: CALayer!
videoCompo.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)
let exporter = AVAssetExportSession(asset: compo, presetName: AVAssetExportPresetHighestQuality)
exporter?.videoComposition = videoCompo
exporter?.exportAsynchronously {
    self.exportDidFinish(exporter!)
}

额外说明

如果你的链式动画更复杂(比如需要根据前一个动画的实际结束状态触发下一个),也别用delegate,而是:

  • 精确计算每个动画的beginTime,确保时序完全正确;
  • 或者用CAAnimationGroup来组合有依赖关系的动画;
  • 对于需要动态调整的动画,可以提前在合成阶段计算好所有关键帧,一次性添加到图层上。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 05:32:07