如何在后台线程上创建 NSTimer?
Posted
技术标签:
【中文标题】如何在后台线程上创建 NSTimer?【英文标题】:How do I create a NSTimer on a background thread? 【发布时间】:2012-01-08 10:26:28 【问题描述】:我有一个需要每 1 秒执行一次的任务。目前我有一个 NSTimer 每 1 秒重复触发一次。如何在后台线程(非 UI 线程)中触发计时器?
我可以在主线程上触发 NSTimer,然后使用 NSBlockOperation 调度后台线程,但我想知道是否有更有效的方法来执行此操作。
【问题讨论】:
【参考方案1】:如果您需要这样做,以便在您滚动视图(或地图)时计时器仍然运行,您需要将它们安排在不同的运行循环模式下。替换您当前的计时器:
[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 仍然在主线程上运行,仍然在与滚动视图相同的运行循环中。区别在于运行循环模式。查看博客文章以获得清晰的解释。
【讨论】:
正确,这不是后台运行任务,而是在不同的线程中运行定时器,而不是阻塞UI。 这里的代码是有道理的,但是解释是错误的。 mainRunLoop 在主/UI 线程上运行。您在这里所做的只是将其配置为在不同模式下从主线程运行。 我试过这个。这不是在不同的线程中运行。它在主线程中运行。所以它会阻止 UI 操作。 @AnbuRaj 它在主线程上运行,但没有阻塞 UI,因为它以不同的模式运行(正如 Steven Fisher 所说)。另见***.com/a/7223765/4712 对于 Swift:创建计时器:let timer = NSTimer.init(timeInterval: 0.1, target: self, selector: #selector(AClass.AMethod), userInfo: nil, repeats: true)
接下来添加计时器:NSRunLoop.currentRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
【参考方案2】:
如果您想使用纯 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 秒的余地,如果您愿意,可以收紧。
【讨论】:
我喜欢 NSTimer 因为它的invalidate
方法。我会使用dispatch_suspend()
或dispatch_source_cancel()
得到类似的行为吗?两者都需要吗?
@NealEhardt 你明白了吗?我还需要在后台线程上无效。我正在后台线程上执行并发操作,如果它们没有完成,它们需要超时。但如果他们完成了,我需要使超时计时器无效。
@horseshoe7 - dispatch_suspend()
和 dispatch_resume()
将像这样暂停和恢复调度计时器。使用dispatch_source_cancel()
和dispatch_release()
完成删除前的失效操作(在某些操作系统版本上启用 ARC 的应用程序可能不需要后者)。
我采取了简单的方法。这很好地解决了我的问题! github.com/mindsnacks/MSWeakTimer
对我不起作用 :( 在第 3 行说这里不允许函数定义 ...【参考方案3】:
需要将计时器安装到在已运行的后台线程上运行的运行循环中。该线程必须继续运行运行循环才能真正触发计时器。为了让该后台线程继续能够触发其他计时器事件,它需要生成一个新线程来实际处理事件(当然,假设您正在进行的处理需要大量时间)。
无论如何,我认为通过使用 Grand Central Dispatch 或 NSBlockOperation
生成新线程来处理计时器事件是对主线程的完全合理的使用。
【讨论】:
我也更喜欢 NSBlockOperations 方法,除了它似乎没有调度到后台线程。它只为添加的内容执行并发线程,但只在当前线程中。 @David:这取决于您将操作添加到哪个队列。如果将其添加到主队列,那么它将在主线程上运行。如果您将其添加到您自己创建的队列中,那么(至少在 Mac 上)它将在另一个线程上运行。【参考方案4】:这应该可行,
它在后台队列中每 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];
);
希望对你有帮助
【讨论】:
注意,这会在全局后台队列上运行该方法,而不是在特定线程上运行。这可能是也可能不是 OP 想要的。 另外,这并不完全准确。定时器触发或方法处理中引入的任何延迟都会延迟下一次回调。如果您想使用 GCD 为这东西供电,最好使用dispatch_source
计时器。
@HimanshuMahajan 据我所知,它不像单事件计时器那样真正是一个循环,然后它就会停止。这就是为什么在methodToRepeatEveryOneSecond
类方法中必须重新启动它。因此,如果您想停止它,您可以在dispatch_queue_t ...
行上方放置一个条件,以便在您不想继续时返回。
我使用的是命名管道 IPC,需要一个后台循环来检查它。这个特殊的程序对我有用,而来自Marius 的程序在这种情况下对我不起作用。【参考方案5】:
对于 swift 3.0,
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 相关的问题
我对是否需要手动停止 RunLoop 感到有些困惑。根据此处的文档,似乎当没有附加计时器时,它将立即退出。因此,当我们停止计时器时,它应该自身存在。但是,在该文件的末尾,它还说:
从运行循环中删除所有已知的输入源和计时器并不是 保证运行循环将退出。 macOS 可以安装和删除 处理针对 接收者的线程。因此,这些来源可能会阻止运行循环 从退出。
我尝试了文档中提供的以下解决方案,以保证终止循环。但是,将 .run() 更改为下面的代码后,计时器不会触发。
while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) ;
我的想法是在 iOS 上使用 .run() 可能是安全的。因为文档指出 macOS 会根据需要安装和删除其他输入源,以处理针对接收者线程的请求。所以iOS可能没问题。
【讨论】:
请记住,队列不是线程,两个队列实际上可能在同一个线程上运行(甚至在主线程上)。考虑到Timer
绑定到绑定到线程而不是队列的RunLoop
,这可能会导致意外问题。【参考方案6】:
6 年后的今天,我尝试做同样的事情,这里是替代解决方案:GCD 或 NSThread。
定时器与run loops一起工作,一个线程的runloop只能从线程中获取,所以关键是线程中的调度定时器。
除了主线程的runloop,runloop应该手动启动;在运行runloop中应该有一些事件需要处理,比如Timer,否则runloop会退出,如果timer是唯一的事件源,我们可以使用它来退出runloop:使定时器失效。
以下代码是 Swift 4:
解决方案 0:GCD
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
的sync
function 会尽可能调用当前线程上的块。实际上,你在主线程中执行上面的代码,定时器是在主线程中触发的,所以不要使用sync
函数,否则定时器不会在你想要的线程中触发。
您可以通过暂停在 Xcode 中执行的程序来命名线程以跟踪其活动。在 GCD 中,使用:
Thread.current.name = "ThreadWithTimer"
解决方案 1:线程
我们可以直接使用 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()
解决方案 2:子类线程
如果你想使用 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。
【讨论】:
【参考方案7】:我的 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()
【讨论】:
【参考方案8】:仅适用于 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()
【讨论】:
【参考方案9】:class BgLoop:Operation
func main()
while (!isCancelled)
sample();
Thread.sleep(forTimeInterval: 1);
【讨论】:
【参考方案10】:如果您希望您的 NSTimer 在均匀背景下运行,请执行以下操作-
-
在 applicationWillResignActive 方法中调用 [self beginBackgroundTask] 方法
在applicationWillEnterForeground中调用[self endBackgroundTask]方法
就是这样
-(void)beginBackgroundTask
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^
[self endBackgroundTask];
];
-(void)endBackgroundTask
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
【讨论】:
我认为您误解了这个问题。他想在后台线程上运行时间,但不一定在应用程序的后台状态上运行。另外,由于 Cocoa 标签,问题是关于 macOS 的。以上是关于如何在后台线程上创建 NSTimer?的主要内容,如果未能解决你的问题,请参考以下文章
在后台线程上创建 UIViewController 可以吗?
在私有/后台队列上创建 NSManagedObjectContext:怎么办?