백그라운드 스레드에서 NSTimer를 어떻게 생성합니까?
1 초마다 수행해야하는 작업이 있습니다. 현재 1 초마다 NSTimer가 반복적으로 실행됩니다. 백그라운드 스레드 (비 UI 스레드)에서 타이머를 어떻게 실행합니까?
주 스레드에서 NSTimer를 실행 한 다음 NSBlockOperation을 사용하여 백그라운드 스레드를 디스패치 할 수 있지만이 작업을 수행하는 더 효율적인 방법이 있는지 궁금합니다.
타이머는 이미 실행중인 백그라운드 스레드에서 작동하는 런 루프에 설치되어야합니다. 해당 스레드는 타이머가 실제로 실행되도록 실행 루프를 계속 실행해야합니다. 그리고 그 백그라운드 스레드가 다른 타이머 이벤트를 계속 발생시킬 수 있으려면 어쨌든 실제로 이벤트를 처리 할 새 스레드를 생성해야합니다 (물론 처리하는 데 상당한 시간이 걸린다고 가정).
어떤 가치가 있든간에 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는 여전히 메인 스레드에서 실행되며 여전히 scrollviews와 동일한 실행 루프에서 실행됩니다. 차이점은 런 루프 모드 입니다. 명확한 설명은 블로그 게시물을 확인하십시오.
순수 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
}
그런 다음 다음과 같은 코드를 사용하여 1 초 타이머 이벤트를 설정할 수 있습니다.
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 초의 여유를 제공하며, 원하는 경우 강화할 수 있습니다.
이것은 작동합니다.
NSTimers를 사용하지 않고 백그라운드 큐에서 1 초마다 메서드를 반복합니다. :)
- (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];
});
도움이되기를 바랍니다.
신속한 3.0의 경우
Tikhonv의 대답은 너무 많이 설명하지 않습니다. 여기에 내 이해 중 일부가 추가됩니다.
먼저 짧게 만들기 위해 코드가 있습니다. 그것은이다 DIFFERENT 내가 타이머를 만들 장소에서 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의 생성자를 호출하고 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는 괜찮을 수 있습니다.
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 only (although can probably be modified to use with Objective-C)
Check out DispatchTimer
from https://github.com/arkdan/ARKExtensions, which "Executes a closure on specified dispatch queue, with specified time intervals, for specified number of times (optionally). "
let queue = DispatchQueue(label: "ArbitraryQueue")
let timer = DispatchTimer(timeInterval: 1, queue: queue) { timer in
// body to execute until cancelled by timer.cancel()
}
Today after 6 years, I try to do same thing, here is alternative soltion: GCD or NSThread.
Timers work in conjunction with run loops, a thread's runloop can be get from the thread only, so the key is that schedule timer in the thread.
Except main thread's runloop, runloop should start manually; there should be some events to handle in running runloop, like Timer, otherwise runloop will exit, and we can use this to exit a runloop if timer is the only event source: invalidate the timer.
The following code is Swift 4:
Solution 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 has strong reference to target, and runloop has strong reference to timer, after timer invalidate, it release target, so keep weak reference to it in target and invalidate it in appropriate time to exit runloop(and then exit thread).
Note: as an optimization, sync
function of DispatchQueue
invokes the block on the current thread when possible. Actually, you execute above code in main thread, Timer is fired in main thread, so don't use sync
function, otherwise timer is not fired at the thread you want.
You could name thread to track its activity by pausing program executing in Xcode. In GCD, use:
Thread.current.name = "ThreadWithTimer"
Solution 1: Thread
We could use NSThread directly. Don't afraid, code is easy.
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()
}
Solution 2: Subclass Thread
If you want to use Thread subclass:
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()
}
}
Note: don't add timer in init, otherwise, timer is add to init's caller's thread's runloop, not this thread's runloop, e.g., you run following code in main thread, if TimerThread
add timer in init method, timer will be scheduled to main thread's runloop, not timerThread's runloop. You can verify it in timerMethod()
log.
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 About Runloop.current.run()
, its document suggest don't call this method if we want runloop to terminate, use run(mode: RunLoopMode, before limitDate: Date)
, actually run()
repeatedly invoke this method in the NSDefaultRunloopMode, what's mode? More details in runloop and thread.
class BgLoop:Operation{
func main(){
while (!isCancelled) {
sample();
Thread.sleep(forTimeInterval: 1);
}
}
}
If you want your NSTimer to run in even background, do the following-
- call [self beginBackgroundTask] method in applicationWillResignActive methods
- call [self endBackgroundTask] method in applicationWillEnterForeground
That's it
-(void)beginBackgroundTask
{
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundTask];
}];
}
-(void)endBackgroundTask
{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
참고URL : https://stackoverflow.com/questions/8304702/how-do-i-create-a-nstimer-on-a-background-thread
'program story' 카테고리의 다른 글
MTOM은 어떻게 작동합니까? (0) | 2020.11.19 |
---|---|
postDelayed에 의해 추가 된 핸들러 객체에서 실행 파일을 제거하는 방법은 무엇입니까? (0) | 2020.11.19 |
AngularJS에서 루트 스코프에 브로드 캐스트 이벤트를 등록 해제하려면 어떻게해야합니까? (0) | 2020.11.19 |
IE6 + IE7 CSS 문제 오버플로 : 숨김; (0) | 2020.11.19 |
UIActivityViewController에서 메일 제목을 설정하는 방법은 무엇입니까? (0) | 2020.11.19 |