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

递归调用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()。

请问:

  1. 为什么APP后台会触发数百次startSpinningCircleAnimation()调用?
  2. 如何正确实现动画在前后台切换时的暂停与恢复?

原代码

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()
    }

}

解答

问题原因

  1. 后台动画强制终止触发完成闭包:iOS后台会暂停所有UI相关动画,UIViewPropertyAnimator被系统强制终止后会立即触发完成闭包。你在第二个动画器的完成闭包里递归调用startSpinningCircleAnimation(),后台时动画刚启动就被终止,触发闭包,进而无限递归,短时间内刷出大量日志。
  2. 硬编码状态判断不可靠:你用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 13:45:30