模拟器是否会像物理设备一样终止不结束后台任务的应用程序?

Does the Simulator kill off apps that don't end background tasks just like a physical device does?

我正在调试倒数计时器在后台被终止的原因。

为了更好地熟悉自己,我做了一个快速而肮脏的计时器实现。它启动后台任务,启动标准计时器,并将其添加到 RunLoop。

每当倒计时秒数发生变化时,我都会打印出倒计时还剩多少秒以及 OS 给我的秒数(即 UIApplication.shared.backgroundTimeRemaining)。

然而,当我在模拟器中 运行 启动计时器,并将应用程序置于后台时,计时器工作正常,并且一直持续到倒数为止。

一些注意事项:

我希望计时器在完成之前不会停止,即使在后台也是如此。但是,我知道 OS 通常会在后台显示 3-5 分钟。这就是我的问题的来源。如果我在后台只有 3-5 分钟,那为什么我的计时器 运行ning 基本上只要需要就可以了?模拟器不会在后台与物理设备同时关闭应用程序吗?

此外,我还设置了一个回调,以便在 OS 关闭时触发(即 beginBackgroundTask(withName: 函数提供的 expirationHandler 回调)

对此的任何见解都会有所帮助!这是我的视图控制器 class:

class TimerViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {

    // MARK: - Outlets

    @IBOutlet weak var timeLeftLabel: UILabel!
    @IBOutlet weak var timePicker: UIPickerView!

    // MARK: - Properties

    var timer: Timer?
    var timeLeft: Int = 0 {
        didSet {
            DispatchQueue.main.async {
                self.timeLeftLabel.text = "\(self.timeLeft.description) seconds left"
            }
        }
    }
    var backgroundTask: UIBackgroundTaskIdentifier = .invalid
    let backgroundTaskName = "bgTask"

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }

    @objc func applicationDidMoveToBackground() {
        print("moved to backgorund")
    }

    @objc func applicationWillMoveToForegraund() {
        print("moved to foreground")
    }

    // MARK: - Setup

    func setupUI() {
        NotificationCenter.default.addObserver(self, selector: #selector(applicationDidMoveToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(applicationWillMoveToForegraund), name: UIApplication.willEnterForegroundNotification, object: nil)
        timePicker.tintColor = .white
        timePicker.backgroundColor = .clear
    }

    func registerBackgroundTask() {
        //end any bg tasks
        endBackgroundTask()
        //start new one
        backgroundTask = UIApplication.shared.beginBackgroundTask(withName: backgroundTaskName, expirationHandler: {
            //times up, do this stuff when ios kills me
            print("background task being ended by expiration handler")
            self.endBackgroundTask()
        })
        assert(backgroundTask != UIBackgroundTaskIdentifier.invalid)

        //actual meat of bg task
        print("starting")
        timePicker.isHidden = true
        timeLeftLabel.isHidden = false
        timeLeft = getCurrentPickerViewSeconds()
        timeLeftLabel.text = "\(timeLeft) seconds left"
        setupTimer()
    }

    func endBackgroundTask() {
        UIApplication.shared.endBackgroundTask(self.backgroundTask)
        self.backgroundTask = .invalid
    }

    func setupTimer() {
        timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fire), userInfo: nil, repeats: true)
        if timer != nil {
            RunLoop.current.add(timer!, forMode: .common)
        } else {
            print("timer is nil, didnt add to runloop")
        }
    }

    // MARK: - Helpers

    func getCurrentPickerViewSeconds() -> Int {
        let mins = timePicker.selectedRow(inComponent: 0)
        let seconds = timePicker.selectedRow(inComponent: 1)
        let totalSeconds = seconds + (mins * 60)
        return totalSeconds
    }

    // MARK: - Actions

    @objc func fire() {
        print("current time left: \(timeLeft)")
        print("background time remaining: \(UIApplication.shared.backgroundTimeRemaining)")
        if timeLeft > 0 {
            timeLeft -= 1
        } else {
            print("done")
            stopTimer()
        }
    }

    @IBAction func startTimer() {
        registerBackgroundTask()
    }

    @IBAction func stopTimer() {
        print("stopping")
        endBackgroundTask()
        timePicker.isHidden = false
        timeLeftLabel.isHidden = true
        timer?.invalidate()
        timer = nil
    }

    @IBAction func resetTimer() {
        print("resetting")
        stopTimer()
        startTimer()
    }

    @IBAction func doneTapped() {
        print("done-ing")
        stopTimer()
        dismiss(animated: true, completion: nil)
    }

    // MARK: - Picker View

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 2
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return 59
    }

    func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
        var label = ""
        switch component {
        case 0:
            label = "m"
        case 1:
            label = "s"
        default:
            label = ""
        }
        let result = "\(row) \(label)"
        let attributedResult = NSAttributedString(string: result, attributes: [NSAttributedString.Key.foregroundColor : UIColor.white])
        return attributedResult
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        let seconds = row + (60 * component)
        timeLeft = seconds
    }
}

这是一些输出的屏幕截图(一张来自倒计时开始,一张来自 expirationHandler 被触发,一张来自倒计时结束):

好吧,我想通了!够愚蠢的,我应该只是在物理设备上尝试一下(只是在发布该问题时没有)。

调查结果:

模拟器的行为与物理设备不同,因为它不会在 OS 允许的时间结束后关闭应用程序(即 UIApplication.shared.backgroundTimeRemaining 基本上是 lie/non 在模拟器中时的问题)。

另一方面,对于物理设备,一旦 backgroundTimeRemaining 达到 0,应用程序就会被终止,这是预期的。这是输出的屏幕截图: