模拟器是否会像物理设备一样终止不结束后台任务的应用程序?
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,应用程序就会被终止,这是预期的。这是输出的屏幕截图:
我正在调试倒数计时器在后台被终止的原因。
为了更好地熟悉自己,我做了一个快速而肮脏的计时器实现。它启动后台任务,启动标准计时器,并将其添加到 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,应用程序就会被终止,这是预期的。这是输出的屏幕截图: