我有一项需要每1秒执行一次的任务。目前我每隔1秒就有一次NSTimer重复射击。如何在后台线程(非UI线程)中触发计时器?
我可以在主线程上使用NSTimer激活然后使用NSBlockOperation来调度后台线程,但我想知道是否有更有效的方法来执行此操作。
答案 0 :(得分:104)
如果您需要这样,那么当您滚动视图(或地图)时,定时器仍会运行,您需要在不同的运行循环模式下安排它们。替换您当前的计时器:
[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仍然在主线程上运行,仍然在与scrollviews相同的运行循环中运行。不同之处在于运行循环模式。查看博客文章以获得清晰的解释。
答案 1 :(得分:49)
如果您想使用纯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秒余地,如果你愿意,可以收紧。
答案 2 :(得分:18)
需要将计时器安装到在已经运行的后台线程上运行的运行循环中。该线程必须继续运行运行循环才能使计时器实际触发。并且对于该后台线程继续能够触发其他定时器事件,它将需要生成一个新线程以实际处理事件(当然,假设您正在进行的处理需要花费大量时间)。 / p>
无论它值多少,我认为通过使用Grand Central Dispatch或NSBlockOperation
生成一个新线程来处理计时器事件是对主线程的合理使用。
答案 3 :(得分:15)
这应该有效,
它在后台队列中每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];
});
希望有所帮助
答案 4 :(得分:12)
Tikhonv的答案并没有解释太多。这增加了我的一些理解。
首先简化,这是代码。在我创建计时器的地方,Tikhonv的代码是 DIFFERENT 。我使用constructer创建计时器并将其添加到循环中。我认为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。然后首先检查计时器是否已经启动是一个好习惯。如果timer变量不是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可能没问题。
答案 5 :(得分:2)
适用于iOS 10+的My 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()
}
}
}
答案 6 :(得分:1)
仅限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()
}
答案 7 :(得分:1)
6年后的今天,我尝试做同样的事情,这里是替代解决方案:GCD或NSThread。
定时器与run循环一起工作,一个线程的runloop只能从线程获取,所以关键是线程中的schedule schedule。
除主线程的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具有对目标的强引用,并且runloop具有对定时器的强引用,在定时器无效后,它释放目标,因此在目标中保持弱引用并在适当的时间使其无效以退出runloop(然后退出线程)。
注意:作为优化,sync
DispatchQueue
函数会在可能的情况下调用当前线程上的块。实际上,你在主线程中执行上面的代码,在主线程中触发Timer,所以不要使用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中添加计时器,否则,计时器会添加到初始调用者的线程的runloop中,而不是此线程的runloop,例如,你在主线程中运行以下代码,如果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关于Runloop.current.run()
,如果我们希望runloop终止,请使用run(mode: RunLoopMode, before limitDate: Date)
,实际run()
在NSDefaultRunloopMode中重复调用此方法,其文档建议不要调用此方法,什么模式? runloop and thread中的更多详细信息。
答案 8 :(得分:0)
class BgLoop:Operation{
func main(){
while (!isCancelled) {
sample();
Thread.sleep(forTimeInterval: 1);
}
}
}
答案 9 :(得分:-1)
如果您希望NSTimer在后台运行,请执行以下操作 -
那是
-(void)beginBackgroundTask
{
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundTask];
}];
}
-(void)endBackgroundTask
{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}