递归调用UIViewPropertyAnimator时APP后台运行异常问题排查
问题:APP后台时UIViewPropertyAnimator触发大量递归调用,如何正确处理前后台动画暂停/恢复
我做了个继承UIView的SpinningCircleView类,用来实现无限旋转的圆形动画,通过ViewController调用展示。前台运行时动画正常,计数器跟踪startSpinningCircleAnimation()调用次数的日志也符合预期:
Starting animation 1: + START Spinning Circle Animation 2: + START Spinning Circle Animation -> recursive call 3: + START Spinning Circle Animation -> recursive call 4: + START Spinning Circle Animation -> recursive call
但APP进入后台后,终端突然刷出几百条"+ START Spinning Circle Animation -> recursive call"日志,回到前台后计数器和日志又恢复正常递增。
我用两个独立的UIViewPropertyAnimator分别实现180度(π)和360度(0度)旋转,在第二个动画器的完成闭包中递归调用startSpinningCircleAnimation()。
请问:
- 为什么APP后台会触发数百次
startSpinningCircleAnimation()调用? - 如何正确实现动画在前后台切换时的暂停与恢复?
原代码
SpinningCircleView类
import UIKit class SpinningCircleView: UIView { private lazy var spinningCircle = CAShapeLayer() private lazy var animator1 = UIViewPropertyAnimator(duration: 1, curve: .linear, animations: nil) private lazy var animator2 = UIViewPropertyAnimator(duration: 1, curve: .linear, animations: nil) public var counter = 0 override init (frame: CGRect) { super.init(frame: frame) configure() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func configure() { frame = CGRect(x: 0, y: 0, width: 100, height: 100) let rect = self.bounds let circularPath = UIBezierPath(ovalIn: rect) spinningCircle.path = circularPath.cgPath spinningCircle.fillColor = UIColor.clear.cgColor spinningCircle.strokeColor = UIColor.systemRed.cgColor spinningCircle.lineWidth = 10 spinningCircle.strokeEnd = 0.25 spinningCircle.lineCap = .round self.layer.addSublayer(spinningCircle) } func startSpinningCircleAnimation() { counter += 1 let criteria1 = animator1.state == .active && !animator1.isRunning let criteria2 = animator2.state == .active && !animator2.isRunning let criteria3 = animator1.state == .inactive && animator2.state == .inactive let criteria4 = (animator1.state == .inactive && animator2.state.rawValue == 5) || (animator2.state == .inactive && animator1.state.rawValue == 5) if (criteria1) { print("\(self.counter): ~ RESUME Spinning Circle Animation") animator1.startAnimation() } else if (criteria2) { print("\(self.counter): ~ RESUME Spinning Circle Animation") animator2.startAnimation() } else if (criteria3 || criteria4) { if (criteria3) { print("\(self.counter): + START Spinning Circle Animation") } else if (criteria4) { print("\(self.counter): + START Spinning Circle Animation -> recursive call") } animator1.addAnimations { self.transform = CGAffineTransform(rotationAngle: .pi) } animator1.addCompletion { _ in self.animator2.addAnimations { self.transform = CGAffineTransform(rotationAngle: 0) } self.animator2.addCompletion { _ in self.startSpinningCircleAnimation() } self.animator2.startAnimation() } animator1.startAnimation() } else { print("\(self.counter): >>>>>>>>>>>>> HERE <<<<<<<<<<<<< \(self.animator1.state) \(self.animator1.isRunning) \(self.animator2.state) \(self.animator2.isRunning)") } } func stopSpinningCircleAnimation() { print("\(self.counter): - STOP Spinning Circle Animation Begin: \(self.animator1.state) \(self.animator1.isRunning) \(self.animator2.state) \(self.animator2.isRunning)") if (self.animator1.isRunning) { self.animator1.pauseAnimation() } else if (self.animator2.isRunning) { self.animator2.pauseAnimation() } print("\(self.counter): - STOP Spinning Circle Animation End: \(self.animator1.state) \(self.animator1.isRunning) \(self.animator2.state) \(self.animator2.isRunning)") } }
ViewController类
class ViewController: UIViewController { private lazy var spinningCircleView = SpinningCircleView() override func viewDidLoad() { super.viewDidLoad() spinningCircleView.frame = CGRect(x: view.center.x - 50, y: 100, width: 100, height: 100) spinningCircleView.tag = 100 view.addSubview(spinningCircleView) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) print("Starting animation") spinningCircleView.startSpinningCircleAnimation() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) print("Pausing animation") spinningCircleView.stopSpinningCircleAnimation() } }
解答
问题原因
- 后台动画强制终止触发完成闭包:iOS后台会暂停所有UI相关动画,
UIViewPropertyAnimator被系统强制终止后会立即触发完成闭包。你在第二个动画器的完成闭包里递归调用startSpinningCircleAnimation(),后台时动画刚启动就被终止,触发闭包,进而无限递归,短时间内刷出大量日志。 - 硬编码状态判断不可靠:你用
animator2.state.rawValue == 5这种未公开的内部值判断状态,后台时动画状态变化会触发错误分支,加剧递归问题。
解决方案
1. 监听前后台通知,主动控制动画
添加UIApplication.didEnterBackgroundNotification和UIApplication.willEnterForegroundNotification监听,后台主动暂停动画,前台恢复,避免系统强制终止动画触发递归。
2. 重构动画逻辑,简化递归
用单个动画器实现完整360度旋转,替代拆分两次半旋转的逻辑,减少递归触发点。
3. 移除硬编码状态判断
改用UIViewPropertyAnimator.State公开枚举值(.inactive/.active/.stopped)判断状态,避免依赖未公开的rawValue。
修改后的代码
SpinningCircleView类
import UIKit class SpinningCircleView: UIView { private lazy var spinningCircle = CAShapeLayer() private var currentAnimator: UIViewPropertyAnimator? public var counter = 0 override init(frame: CGRect) { super.init(frame: frame) configure() setupNotifications() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func configure() { frame = CGRect(x: 0, y: 0, width: 100, height: 100) let rect = bounds let circularPath = UIBezierPath(ovalIn: rect) spinningCircle.path = circularPath.cgPath spinningCircle.fillColor = UIColor.clear.cgColor spinningCircle.strokeColor = UIColor.systemRed.cgColor spinningCircle.lineWidth = 10 spinningCircle.strokeEnd = 0.25 spinningCircle.lineCap = .round layer.addSublayer(spinningCircle) } private func setupNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) } @objc private func didEnterBackground() { stopSpinningCircleAnimation() } @objc private func willEnterForeground() { startSpinningCircleAnimation() } func startSpinningCircleAnimation() { counter += 1 // 恢复暂停的动画 if let animator = currentAnimator, animator.state == .active && !animator.isRunning { print("\(counter): ~ RESUME Spinning Circle Animation") animator.startAnimation() return } // 重置之前的动画器 currentAnimator?.stopAnimation(true) currentAnimator = nil print("\(counter): \(currentAnimator == nil ? "+ START Spinning Circle Animation" : "+ START Spinning Circle Animation -> recursive call")") // 单个动画器实现完整360度旋转,时长2秒(和原逻辑总时长一致) let fullRotation = CGAffineTransform(rotationAngle: .pi * 2) currentAnimator = UIViewPropertyAnimator(duration: 2, curve: .linear) { self.transform = self.transform.concatenating(fullRotation) } // 动画完成后递归调用,实现无限循环 currentAnimator?.addCompletion { [weak self] _ in self?.startSpinningCircleAnimation() } currentAnimator?.startAnimation() } func stopSpinningCircleAnimation() { print("\(counter): - STOP Spinning Circle Animation") currentAnimator?.pauseAnimation() } deinit { NotificationCenter.default.removeObserver(self) } }
ViewController类
class ViewController: UIViewController { private lazy var spinningCircleView = SpinningCircleView() override func viewDidLoad() { super.viewDidLoad() spinningCircleView.frame = CGRect(x: view.center.x - 50, y: 100, width: 100, height: 100) spinningCircleView.tag = 100 view.addSubview(spinningCircleView) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) print("Starting animation") spinningCircleView.startSpinningCircleAnimation() } }
修改说明
- 用单个动画器实现完整旋转,避免拆分两个动画器导致的递归触发问题。
- 前后台通知主动控制动画,后台时不会触发动画完成闭包。
- 移除硬编码状态判断,使用公开枚举值保证逻辑可靠性。
- 添加
weak self避免循环引用,防止内存泄漏。
内容的提问来源于stack exchange,提问作者atrdeveloper
相关产品推荐
相关产品推荐

