【发布时间】:2012-01-08 10:26:28
【问题描述】:
我有一个需要每 1 秒执行一次的任务。目前我有一个 NSTimer 每 1 秒重复触发一次。如何在后台线程(非 UI 线程)中触发计时器?
我可以在主线程上触发 NSTimer,然后使用 NSBlockOperation 调度后台线程,但我想知道是否有更有效的方法来执行此操作。
【问题讨论】:
标签: objective-c cocoa nstimer nsrunloop nsblockoperation
我有一个需要每 1 秒执行一次的任务。目前我有一个 NSTimer 每 1 秒重复触发一次。如何在后台线程(非 UI 线程)中触发计时器?
我可以在主线程上触发 NSTimer,然后使用 NSBlockOperation 调度后台线程,但我想知道是否有更有效的方法来执行此操作。
【问题讨论】:
标签: objective-c cocoa nstimer nsrunloop nsblockoperation
如果您需要这样做,以便在您滚动视图(或地图)时计时器仍然运行,您需要将它们安排在不同的运行循环模式下。替换您当前的计时器:
[NSTimer scheduledTimerWithTimeInterval:0.5
target:self
selector:@selector(timerFired:)
userInfo:nil repeats:YES];
有了这个:
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5
target:self
selector:@selector(timerFired:)
userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
详情请查看这篇博文:Event tracking stops NSTimer
编辑: 第二个代码块,NSTimer 仍然在主线程上运行,仍然在与滚动视图相同的运行循环中。区别在于运行循环模式。查看博客文章以获得清晰的解释。
【讨论】:
let timer = NSTimer.init(timeInterval: 0.1, target: self, selector: #selector(AClass.AMethod), userInfo: nil, repeats: true) 接下来添加计时器:NSRunLoop.currentRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
如果您想使用纯 GCD 并使用调度源,Apple 在其Concurrency Programming Guide 中有一些示例代码:
dispatch_source_t CreateDispatchTimer(uint64_t interval, uint64_t leeway, dispatch_queue_t queue, dispatch_block_t block)
{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
if (timer)
{
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}
return timer;
}
斯威夫特 3:
func createDispatchTimer(interval: DispatchTimeInterval,
leeway: DispatchTimeInterval,
queue: DispatchQueue,
block: @escaping ()->()) -> DispatchSourceTimer {
let timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: 0),
queue: queue)
timer.scheduleRepeating(deadline: DispatchTime.now(),
interval: interval,
leeway: leeway)
// Use DispatchWorkItem for compatibility with iOS 9. Since iOS 10 you can use DispatchSourceHandler
let workItem = DispatchWorkItem(block: block)
timer.setEventHandler(handler: workItem)
timer.resume()
return timer
}
然后您可以使用如下代码设置一秒计时器事件:
dispatch_source_t newTimer = CreateDispatchTimer(1ull * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Repeating task
});
当然,请确保在完成后存储和释放您的计时器。以上为您在触发这些事件时提供了 1/10 秒的余地,如果您愿意,可以收紧。
【讨论】:
invalidate 方法。我会使用dispatch_suspend() 或dispatch_source_cancel() 得到类似的行为吗?两者都需要吗?
dispatch_suspend() 和 dispatch_resume() 将像这样暂停和恢复调度计时器。使用dispatch_source_cancel() 和dispatch_release() 完成删除前的失效操作(在某些操作系统版本上启用 ARC 的应用程序可能不需要后者)。
需要将计时器安装到在已运行的后台线程上运行的运行循环中。该线程必须继续运行运行循环才能真正触发计时器。为了让该后台线程继续能够触发其他计时器事件,它需要生成一个新线程来实际处理事件(当然,假设您正在进行的处理需要大量时间)。
无论如何,我认为通过使用 Grand Central Dispatch 或 NSBlockOperation 生成新线程来处理计时器事件是对主线程的完全合理的使用。
【讨论】:
这应该可行,
它在后台队列中每 1 秒重复一个方法,而不使用 NSTimers :)
- (void)methodToRepeatEveryOneSecond
{
// Do your thing here
// Call this method again using GCD
dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, q_background, ^(void){
[self methodToRepeatEveryOneSecond];
});
}
如果您在主队列中并且想要调用上述方法,您可以这样做,以便在运行之前更改为后台队列:)
dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(q_background, ^{
[self methodToRepeatEveryOneSecond];
});
希望对你有帮助
【讨论】:
dispatch_source 计时器。
methodToRepeatEveryOneSecond 类方法中必须重新启动它。因此,如果您想停止它,您可以在dispatch_queue_t ... 行上方放置一个条件,以便在您不想继续时返回。
Tikhonv 的回答并没有过多解释。这里补充一些我的理解。
为了简短起见,这里是代码。在我创建计时器的地方,它与 Tikhonv 的代码不同。我使用构造函数创建计时器并将其添加到循环中。我认为 scheduleTimer 函数会将计时器添加到主线程的 RunLoop 中。所以最好使用构造函数来创建定时器。
class RunTimer{
let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent)
let timer: Timer?
private func startTimer() {
// schedule timer on background
queue.async { [unowned self] in
if let _ = self.timer {
self.timer?.invalidate()
self.timer = nil
}
let currentRunLoop = RunLoop.current
self.timer = Timer(timeInterval: self.updateInterval, target: self, selector: #selector(self.timerTriggered), userInfo: nil, repeats: true)
currentRunLoop.add(self.timer!, forMode: .commonModes)
currentRunLoop.run()
}
}
func timerTriggered() {
// it will run under queue by default
debug()
}
func debug() {
// print out the name of current queue
let name = __dispatch_queue_get_label(nil)
print(String(cString: name, encoding: .utf8))
}
func stopTimer() {
queue.sync { [unowned self] in
guard let _ = self.timer else {
// error, timer already stopped
return
}
self.timer?.invalidate()
self.timer = nil
}
}
}
首先,创建一个队列以使计时器在后台运行,并将该队列存储为类属性,以便将其重用于停止计时器。我不确定我们是否需要使用相同的队列来启动和停止,我这样做的原因是因为我看到了一条警告消息here。
RunLoop 类通常不被认为是线程安全的,并且 它的方法只能在当前的上下文中调用 线。您永远不应该尝试调用 RunLoop 对象的方法 在不同的线程中运行,因为这样做可能会导致意外 结果。
所以我决定存储队列并为计时器使用相同的队列以避免同步问题。
还创建一个空计时器并将其存储在类变量中。将其设为可选,以便您可以停止计时器并将其设置为 nil。
class RunTimer{
let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent)
let timer: Timer?
}
要启动计时器,首先从 DispatchQueue 调用 async。然后最好先检查计时器是否已经启动。如果计时器变量不为 nil,则 invalidate() 并将其设置为 nil。
下一步是获取当前的 RunLoop。因为我们是在我们创建的队列块中这样做的,所以它将获得我们之前创建的后台队列的 RunLoop。
创建计时器。这里不使用 scheduleTimer,而是调用 timer 的构造函数,并传入任何你想要的 timer 属性,例如 timeInterval、target、selector 等。
将创建的计时器添加到 RunLoop。运行它。
这是一个关于运行 RunLoop 的问题。根据此处的文档,它说它有效地开始了一个无限循环,该循环处理来自运行循环的输入源和计时器的数据。
private func startTimer() {
// schedule timer on background
queue.async { [unowned self] in
if let _ = self.timer {
self.timer?.invalidate()
self.timer = nil
}
let currentRunLoop = RunLoop.current
self.timer = Timer(timeInterval: self.updateInterval, target: self, selector: #selector(self.timerTriggered), userInfo: nil, repeats: true)
currentRunLoop.add(self.timer!, forMode: .commonModes)
currentRunLoop.run()
}
}
正常执行功能。调用该函数时,默认在队列下调用。
func timerTriggered() {
// under queue by default
debug()
}
func debug() {
let name = __dispatch_queue_get_label(nil)
print(String(cString: name, encoding: .utf8))
}
上面的调试功能是用来打印出队列的名字的。如果你担心它是否已经在队列中运行,你可以调用它来检查。
停止计时器很简单,调用 validate() 并将存储在类中的计时器变量设置为 nil。
在这里,我再次在队列下运行它。由于这里的警告,我决定在队列下运行所有与计时器相关的代码以避免冲突。
func stopTimer() {
queue.sync { [unowned self] in
guard let _ = self.timer else {
// error, timer already stopped
return
}
self.timer?.invalidate()
self.timer = nil
}
}
我对是否需要手动停止 RunLoop 感到有些困惑。根据此处的文档,似乎当没有附加计时器时,它将立即退出。因此,当我们停止计时器时,它应该自身存在。但是,在该文件的末尾,它还说:
从运行循环中删除所有已知的输入源和计时器并不是 保证运行循环将退出。 macOS 可以安装和删除 处理针对 接收者的线程。因此,这些来源可能会阻止运行循环 从退出。
我尝试了文档中提供的以下解决方案,以保证终止循环。但是,将 .run() 更改为下面的代码后,计时器不会触发。
while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) {};
我的想法是在 iOS 上使用 .run() 可能是安全的。因为文档指出 macOS 会根据需要安装和删除其他输入源,以处理针对接收者线程的请求。所以iOS可能没问题。
【讨论】:
Timer 绑定到绑定到线程而不是队列的RunLoop,这可能会导致意外问题。
6 年后的今天,我尝试做同样的事情,这里是替代解决方案:GCD 或 NSThread。
定时器与run loops一起工作,一个线程的runloop只能从线程中获取,所以关键是线程中的调度定时器。
除了主线程的runloop,runloop应该手动启动;在运行runloop中应该有一些事件需要处理,比如Timer,否则runloop会退出,如果timer是唯一的事件源,我们可以使用它来退出runloop:使定时器失效。
以下代码是 Swift 4:
weak var weakTimer: Timer?
@objc func timerMethod() {
// vefiry whether timer is fired in background thread
NSLog("It's called from main thread: \(Thread.isMainThread)")
}
func scheduleTimerInBackgroundThread(){
DispatchQueue.global().async(execute: {
//This method schedules timer to current runloop.
self.weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
//start runloop manually, otherwise timer won't fire
//add timer before run, otherwise runloop find there's nothing to do and exit directly.
RunLoop.current.run()
})
}
Timer对target有强引用,runloop对timer有强引用,定时器失效后释放target,所以在target中保持对它的弱引用,并在适当的时候使其失效退出runloop(然后退出线程)。
注意:作为优化,DispatchQueue 的syncfunction 会尽可能调用当前线程上的块。实际上,你在主线程中执行上面的代码,定时器是在主线程中触发的,所以不要使用sync函数,否则定时器不会在你想要的线程中触发。
您可以通过暂停在 Xcode 中执行的程序来命名线程以跟踪其活动。在 GCD 中,使用:
Thread.current.name = "ThreadWithTimer"
我们可以直接使用 NSThread。别怕,写代码很简单。
func configurateTimerInBackgroundThread(){
// Don't worry, thread won't be recycled after this method return.
// Of course, it must be started.
let thread = Thread.init(target: self, selector: #selector(addTimer), object: nil)
thread.start()
}
@objc func addTimer() {
weakTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
RunLoop.current.run()
}
如果你想使用 Thread 子类:
class TimerThread: Thread {
var timer: Timer
init(timer: Timer) {
self.timer = timer
super.init()
}
override func main() {
RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
RunLoop.current.run()
}
}
注意:不要在init中添加timer,否则timer会添加到init的调用者线程的runloop,而不是这个线程的runloop,例如,你在主线程中运行以下代码,如果TimerThread在init方法中添加timer,timer会被调度到主线程的runloop,而不是timerThread的runloop。您可以在timerMethod() 日志中进行验证。
let timer = Timer.init(timeInterval: 1, target: self, selector: #selector(timerMethod), userInfo: nil, repeats: true)
weakTimer = timer
let timerThread = TimerThread.init(timer: timer)
timerThread.start()
P.S 关于Runloop.current.run(),它的文档建议如果我们想终止runloop,不要调用这个方法,使用run(mode: RunLoopMode, before limitDate: Date),实际上run()在NSDefaultRunloopMode中反复调用这个方法,什么模式?更多详情runloop and thread。
【讨论】:
我的 iOS 10+ Swift 3.0 解决方案,timerMethod() 将在后台队列中调用。
class ViewController: UIViewController {
var timer: Timer!
let queue = DispatchQueue(label: "Timer DispatchQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
override func viewDidLoad() {
super.viewDidLoad()
queue.async { [unowned self] in
let currentRunLoop = RunLoop.current
let timeInterval = 1.0
self.timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(self.timerMethod), userInfo: nil, repeats: true)
self.timer.tolerance = timeInterval * 0.1
currentRunLoop.add(self.timer, forMode: .commonModes)
currentRunLoop.run()
}
}
func timerMethod() {
print("code")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
queue.sync {
timer.invalidate()
}
}
}
【讨论】:
仅适用于 Swift(尽管可以修改为与 Objective-C 一起使用)
从https://github.com/arkdan/ARKExtensions 中查看DispatchTimer,它“以指定的时间间隔在指定的调度队列上执行闭包指定的次数(可选)。”
let queue = DispatchQueue(label: "ArbitraryQueue")
let timer = DispatchTimer(timeInterval: 1, queue: queue) { timer in
// body to execute until cancelled by timer.cancel()
}
【讨论】:
class BgLoop:Operation{
func main(){
while (!isCancelled) {
sample();
Thread.sleep(forTimeInterval: 1);
}
}
}
【讨论】:
如果您希望您的 NSTimer 在均匀背景下运行,请执行以下操作-
就是这样
-(void)beginBackgroundTask
{
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundTask];
}];
}
-(void)endBackgroundTask
{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
【讨论】: