我有一个需要每 1 秒执行一次的任务。目前我有一个 NSTimer 每 1 秒重复触发一次。如何在后台线程(非 UI 线程)中触发计时器?
我可以在主线程上触发 NSTimer,然后使用 NSBlockOperation 调度后台线程,但我想知道是否有更有效的方法来执行此操作。
我有一个需要每 1 秒执行一次的任务。目前我有一个 NSTimer 每 1 秒重复触发一次。如何在后台线程(非 UI 线程)中触发计时器?
我可以在主线程上触发 NSTimer,然后使用 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 仍然在主线程上运行,仍然在与滚动视图相同的运行循环上。不同之处在于运行循环模式。查看博客文章以获得清晰的解释。
如果您想使用纯 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 秒的余地,如果您愿意,可以收紧。
计时器需要安装到在已经运行的后台线程上运行的运行循环中。该线程必须继续运行运行循环才能真正触发计时器。为了让该后台线程继续能够触发其他计时器事件,它需要生成一个新线程来实际处理事件(当然,假设您正在进行的处理需要大量时间)。
不管它值多少钱,我认为通过使用 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];
});
希望能帮助到你
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
}
}
}
首先,创建一个队列以使计时器在后台运行并将该队列存储为类属性,以便将其重用于停止计时器。我不确定我们是否需要使用相同的队列来启动和停止,我这样做的原因是因为我在这里看到了一条警告消息。
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可能没问题。
6 年后的今天,我尝试做同样的事情,这是替代解决方案:GCD 或 NSThread。
定时器与运行循环一起工作,线程的运行循环只能从线程中获取,所以关键是线程中的调度定时器。
除了主线程的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()
})
}
定时器对target有强引用,runloop对定时器有强引用,定时器失效后释放target,所以在target中保持对它的弱引用,并在适当的时候使其失效退出runloop(然后退出线程)。
注意:作为优化,sync
函数DispatchQueue
尽可能在当前线程上调用块。实际上,你在主线程中执行上面的代码,定时器是在主线程中触发的,所以不要使用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中添加定时器,否则定时器是添加到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 和 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()
}
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;
}