使用AVAssetExportSession链式调用CAAnimation时代理回调顺序异常问题
解决AVVideoCompositionCoreAnimationTool中CAAnimationDelegate回调异常的问题
我之前碰到过一模一样的坑——用AVVideoCompositionCoreAnimationTool做离线视频导出时,CAAnimationDelegate的animationDidStop回调完全不靠谱,要么随机触发,要么顺序乱掉,加断点能正常运行纯粹是因为断点强制同步了线程,刚好补上了回调时序的漏洞。
问题根源
AVFoundation的离线视频渲染和实时UI的动画运行逻辑天差地别:
- 实时UI里,动画由RunLoop驱动,回调时机相对稳定;
- 离线导出时,AVFoundation会做批量渲染优化,不会严格按照实时RunLoop的时序触发delegate回调,甚至可能直接跳过某些回调,导致你在回调里更新图层状态的逻辑彻底失效。
至于移除代理后能正常运行,确实是fillMode = .forwards + isRemovedOnCompletion = false在起作用:这两个配置会让动画结束后自动保持最终的动画值,根本不需要你手动去更新图层属性——AVFoundation在渲染视频帧时会直接使用动画的最终状态,完全不依赖你在回调里的赋值操作。
修复方案
直接抛弃CAAnimationDelegate的回调逻辑,完全依托AVFoundation的动画合成机制来处理状态,具体步骤如下:
- 移除所有delegate相关代码:包括
AnimationChainer类、设置delegate = chainer的代码,以及通过setValue(_:forKey:)传递图层和参数的逻辑。 - 保留动画的fillMode和移除配置:继续使用
isRemovedOnCompletion = false和fillMode = .forwards,让AVFoundation自动维护动画结束后的状态。 - 手动设置图层最终状态(可选但推荐):为了避免渲染时的潜在状态不一致,可以在添加动画前直接把图层的最终状态设置好——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
相关产品推荐
相关产品推荐

