我有一项任务,需要每1秒执行一次。目前,我有一个NSTimer每1秒重复触发一次。如何在后台线程(非UI线程)中触发计时器?
我可以在主线程上触发NSTimer,然后使用NSBlockOperation调度一个后台线程,但是我想知道是否有更有效的方法来执行此操作。
Answers:
计时器需要安装到在已经运行的后台线程上运行的运行循环中。该线程将必须继续运行run循环,以使计时器实际触发。为了使该后台线程能够继续触发其他计时器事件,它需要产生一个新线程以实际处理事件(当然,假设您正在执行的处理要花费大量时间)。
不管值多少钱,我认为通过使用Grand Central Dispatch生成新线程来处理计时器事件还是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];
有关详细信息,请查看此博客文章:事件跟踪停止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在其《并发编程指南》中提供了一些示例代码:
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()
(在某些OS版本上,启用ARC的应用程序可能不需要后者),然后使之无效。
这应该工作,
它在后台队列中每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
如果您想使用GCD来驱动此功能,最好使用计时器。
methodToRepeatEveryOneSecond
class方法中必须再次重新启动它的原因。因此,如果您想停止它,则可以在该dispatch_queue_t ...
行上方放置一个条件,以便在不想继续时返回。
季霍夫的答案并不能解释太多。在这里增加了我的一些理解。
首先,下面是代码。它与我创建计时器的地方的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
}
}
}
首先,创建一个队列以使计时器在后台运行,并将该队列存储为类属性,以便将其重用于停止计时器。我不确定是否需要使用相同的队列来启动和停止,我这样做的原因是因为我在此处看到警告消息。
通常不将RunLoop类视为线程安全的,并且只能在当前线程的上下文中调用其方法。您永远不要尝试调用在不同线程中运行的RunLoop对象的方法,因为这样做可能会导致意外的结果。
因此,我决定存储队列并为计时器使用相同的队列,以避免同步问题。
还创建一个空计时器,并将其存储在class变量中。将其设为可选,以便您可以停止计时器并将其设置为nil。
class RunTimer{
let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent)
let timer: Timer?
}
要启动计时器,请首先从DispatchQueue调用异步。然后,最好先检查计时器是否已经启动。如果计时器变量不是nil,则将其invalidate()并将其设置为nil。
下一步是获取当前的RunLoop。因为我们是在创建的队列块中执行此操作的,所以它将获得我们之前创建的后台队列的RunLoop。
创建计时器。在这里,我们不使用ScheduledTimer,而是调用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。
计时器与运行循环结合使用,线程的运行循环只能从线程中获取,因此关键是线程中的调度计时器。
除主线程的运行循环外,运行循环应手动启动。在运行的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()
})
}
计时器对目标有很强的引用,而运行循环对计时器有很强的引用,在计时器无效后,它释放目标,因此在目标中保留对它的弱引用,并在适当的时间使它无效,以退出运行循环(然后退出线程)。
注意:作为一种优化,sync
函数DispatchQueue
尽可能在当前线程上调用该块。实际上,您在主线程中执行了以上代码,在主线程中触发了Timer,因此请不要使用sync
函数,否则,不会在您想要的线程上触发Timer。
您可以通过暂停在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中添加计时器,否则,将计时器添加到init调用者线程的运行循环中,而不是此线程的运行循环中,例如,您在主线程中运行以下代码,如果TimerThread
在init方法中添加计时器,则计时器将被调度到主线程中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()
PS About Runloop.current.run()
,其文件建议如果我们想让runloop终止,请不要调用此方法run(mode: RunLoopMode, before limitDate: Date)
,实际上run()
在NSDefaultRunloopMode中反复调用此方法是什么模式?在runloop和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一起使用)
DispatchTimer
从https://github.com/arkdan/ARKExtensions中检出,该操作“在指定的调度队列上以指定的时间间隔,以指定的次数(可选)执行关闭。”
let queue = DispatchQueue(label: "ArbitraryQueue")
let timer = DispatchTimer(timeInterval: 1, queue: queue) { timer in
// body to execute until cancelled by timer.cancel()
}
如果您想让NSTimer在后台运行,请执行以下操作-
而已
-(void)beginBackgroundTask
{
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundTask];
}];
}
-(void)endBackgroundTask
{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}