I have a function drawView which is thread safe and does drawing for short periods of game animation. I have functions startAnimating and stopAnimating. I want a background thread to call drawView at a regular rate but only during the period that the animation is enabled.
In startAnimating I was going to call the view's performSelectorInBackground:withObject: to get the thread running.
I'm a little confused about how to do the thread communication and initialize the drawing thread: specifically, setting up a runloop to receive display link messages and then at the end notifying the thread that it should exit and exiting the run loop cleanly when stopAnimating is called from the main thread. I want to ensure that drawView is never called after stopAnimating, and also that the drawing thread is not cancelled abruptly in the middle of the drawing operation. I have seen a lot of very poor answers to this kind of question on line.
OK after reading the Apple pages all evening, I finally solved it with this code:
// object members
NSThread *m_animationthread;
BOOL m_animationthreadrunning;
- (void)startAnimating
{
//called from UI thread
DEBUG_LOG(#"creating animation thread");
m_animationthread = [[NSThread alloc] initWithTarget:self selector:#selector(animationThread:) object:nil];
[m_animationthread start];
}
- (void)stopAnimating
{
// called from UI thread
DEBUG_LOG(#"quitting animationthread");
[self performSelector:#selector(quitAnimationThread) onThread:m_animationthread withObject:nil waitUntilDone:NO];
// wait until thread actually exits
while(![m_animationthread isFinished])
[NSThread sleepForTimeInterval:0.01];
DEBUG_LOG(#"thread exited");
[m_animationthread release];
m_animationthread = nil;
}
- (void)animationThread:(id)object
{
#autoreleasepool
{
DEBUG_LOG(#"animation thread started");
m_animationthreadrunning = YES;
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
CADisplayLink *displaylink = [CADisplayLink displayLinkWithTarget:self selector:#selector(displayLinkAction:)];
[displaylink setFrameInterval:3];
[displaylink addToRunLoop:runLoop forMode:NSDefaultRunLoopMode];
while(m_animationthreadrunning)
{
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
DEBUG_LOG(#"runloop gap");
}
[displaylink removeFromRunLoop:runLoop forMode:NSDefaultRunLoopMode];
DEBUG_LOG(#"animation thread quit");
}
}
- (void)quitAnimationThread
{
DEBUG_LOG(#"quitanimationthread called");
m_animationthreadrunning = NO;
}
- (void)displayLinkAction:(CADisplayLink *)sender
{
DEBUG_LOG(#"display link called");
//[self drawView];
}
The reason I use the line [self performSelector:#selector(quitAnimationThread) onThread:m_animationthread withObject:nil waitUntilDone:NO] and not simply set m_animationthreadrunning = NO in stopAnimating is because the run loop may not return in a timely fashion, but calling a selector forces it to return.
Related
I have an issue. I have a UITableView and a View with a UIProgressView and when i scroll the table, the progressview not refresh the progress value... Only when the scroll is finish, the progress refresh..
I have no clue why is this happening.
I tried to refresh the progress with dispatch_async
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
//I don't know what i have to put here, maybe the NSTimer??
dispatch_async(dispatch_get_main_queue(), ^(void){
//here i set the value of the progress
});
});
but nothing changes...
You're almost there!
I've replicated your problem and fixed it.
This is it without the fix, which is the problem I think you have (note that the progress indicator doesn't update while scrolling):
and this is it with the fix:
The problem is that the scrolling also occurs on the main thread and blocks it. To fix this you just need a small adjustment to your timer. After you initialise your timer, add this:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Here's some example minimal code:
-(instancetype)init{
self = [super init];
if (self) {
_progressSoFar = 0.0;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.progressIndicator.progress = 0.0;
self.myTimer = [NSTimer scheduledTimerWithTimeInterval: 0.1 target: self selector: #selector(callAfterSomeTime:) userInfo: nil repeats: YES];
[[NSRunLoop currentRunLoop] addTimer:self.myTimer forMode:NSRunLoopCommonModes];
[self.myTimer fire];
}
-(void)callAfterSomeTime:(NSTimer *)timer {
if (self.progressSoFar == 1.0) {
[self.myTimer invalidate];
return;
}
// Do anything intensive on a background thread (probably not needed)
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
// Update the progress on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
self.progressSoFar += 0.01;
self.progressIndicator.progress = self.progressSoFar;
});
});
}
The scroll occurs in UITrackingRunLoopMode. You need to make sure that your timer is also in that mode. You shouldn't need any background thread stuff unless you do some fancy calculations but I've included it just in case. Just do any intensive stuff inside the global_queue call but before the main thread call.
How to identify Nsoperation dynamically.
I am creating a NSoperation subclass
- (id)initWithConnectDevice:(ConnectDevice *)cDevice toPeripheral:(CBPeripheral*)peripheral oPerationIndex:(int) index{
if (self = [super init]) {
operationIndex = index;
executing = NO;
finished = NO;
self.connectDevice = cDevice;
[self.connectDevice setDelegate:self];
self.connectedPeripheral = peripheral;
}
return self;
}
-(BOOL)isConcurrent{
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
-(void) terminateOperation {
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
finished = YES;
executing = NO;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
- (void)start {
#autoreleasepool {
if (self.isCancelled){
[timer invalidate];
[self willChangeValueForKey:#"isFinished"];
finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(timerFired:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
}
-(void)timerFired:(id)sender{
NSLog(#"timerFired");
}
I am scanning for BLE devices. For example I will found 3 devices, 3 buttons will create.
I am calling this class every time when I am clicking on that button.That means, When I click on button, I am connecting to bluetooth device and get the data from that device for every second thats why I am using timer in start method.
Like that I have multiple bluetooth devices, whenever I clicked on button, I want to create multiple instances of Operation Queue class.
Now, I want to identify which data is coming from which thread.
Could you please help me....
This the way I am calling above class from viewcontroller
OperationQueue *queue = [[OperationQueue alloc] initWithConnectDevice:connectDevices toPeripheral:peripheral oPerationIndex:operationIndex];
queue.delegate = self;
[[[AppDelegate app] mainQueue] addOperation:queue];
operationIndex = operationIndex+1;
Each of your operations has two identifying properties already - the connected device and the index. When the timer fires, depending what you want to do with the data, you can use these properties to tell where the data is coming from.
You can either have a delegate property on the operation, where a delegate method is called when the timer fires that takes the device and the received data as parameters, or you the operation could have a block property, which takes a block to be executed whenever data is received - the block would have the device and the received data as parameters.
Assuming you want to update the UI when the data is received, be sure to call the delegate method or execute the block on the main thread.
I am trying to timeout my NSOperation with a NSTimer but my timer is not getting fired. Please see below the code I have written inside my class which is sub classing NSOperation.
- (void)start {
// Start the timer for Time out before the ping activity starts
self.timeOutTriggerTimmer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:#selector(cancelTheOperation) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:self.timeOutTriggerTimmer forMode:NSDefaultRunLoopMode];
// Check for cancellation
if ([self isCancelled]) {
[self completeOperation];
return;
}
// Executing
[self willChangeValueForKey:#"isExecuting"];
executing = YES;
[self didChangeValueForKey:#"isExecuting"];
// Begin
[self beginOperation];
}
It's easiest to just add the timer to the main run loop, not the current run loop:
[[NSRunLoop mainRunLoop] addTimer:self.timeOutTriggerTimmer forMode:NSDefaultRunLoopMode];
Alternatively, you can keep your timer as it is (scheduled on the current run loop), but then you have to keep the runloop alive, perhaps adding something like the following to the end of the start method (note, Apple recommends this rather than the run method):
while ([self isExecuting] && [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
But this (like the run method you contemplated) effectively keep the start method running until execution is done (making what appears to be a concurrent operation to behave more like a non-concurrent operation).
I suspect you're doing this already, but just in case, make sure to invalidate your timer when you complete your operation, or else the timer will retain the operation until the timer fires, unnecessarily delaying the freeing of the operation's resources (and calling the cancelTheOperation even though the operation may well already be done).
I found the issue. I need to put the below statement to have it executed.
[[NSRunLoop currentRunLoop] run];
I add a timer on Thread A and start the runloop, after that I dispatch_async a Method_Foo on Thread A, and Method_Foo doesn't run. What I guessing is that the dispatch methods will be blocked by the runLoop on that thread but I not sure. Is that true or am I missing something?
More details:
In the network layer I use delegate to notify network status, the _delegateQueue is from an global instance, let's say Thread_A. And this socket:didConnectToHost:port: method is running in a Thread_B.
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
// called when connected to server
...
dispatch_async(_delegateQueue, ^{
// this runs in Thread_A to notify network status
[theDelegate socketConnection:self didChangeStatus:YES];
});
...
}
Then in the upper layer there's a class run the _startReconnectingProcess method after disconnected with the server in Thread_A. To make the timer to work, I start the runLoop in Thread_A.
// runs in Thread_A to start reconnection process
- (void)_startReconnectingProcess
{
if (self.reconnectionTimer || _forceDisconnection == YES) return;
self.reconnectionTimer = [NSTimer scheduledTimerWithTimeInterval:kReconnectionTimeInterval target:self selector:#selector(_reconnect) userInfo:nil repeats:YES];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:self.reconnectionTimer forMode:NSDefaultRunLoopMode];
// run the runLoop in Thread_A
[runLoop run];
// after the runLoop start, the dispatch_async method in the network layer doesn't work
self.reconnectionCount = 0;
}
So what happens is that when the timer starts, the method [theDelegate socketConnection:self didChangeStatus:YES] never be called, which should.
I have to perform a timer using a NSThread as I need to download text data from the web, and without that, in 3G connection it freezes the UI while downloading. So I've used a NSThread but it still freezes for a while and I don't know how to solve this....
Here's the code I'm using to perform the timer:
- (void)viewDidLoad{
[NSThread detachNewThreadSelector:#selector(onTimerK2) toTarget:self withObject:nil];
}
- (void)onTimerK2{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
timer = [NSTimer scheduledTimerWithTimeInterval:15 target:self selector:#selector(onTimerKY2) userInfo:nil repeats:YES];
[pool release];
}
- (void)onTimerKY2{
NSLog(#"working");
}
You're detaching a new thread to call onTimerK2, which then immediately calls a method back on the main thread, which will of course freeze your interface.
Edit
You should be doing any long-running work not on the main thread (either yourself, or by using the asynchronous nature of NSURLConnection as mentioned elsewhere),and then updating your UI by calling selectors on the main thread as this activity progresses.
Having said that, you may have more success with the following changes/reordering of your code:
- (void)viewDidLoad {
timer = [NSTimer scheduledTimerWithTimeInterval:15
target:self
selector:#selector(onTimerK2)
userInfo:nil
repeats:YES];
}
- (void)onTimerK2{
[NSThread detachNewThreadSelector:#selector(onTimerKY2)
toTarget:self
withObject:nil];
}
- (void)onTimerKY2{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(#"working");
[pool release];
}
It's not very clear how you are trying to solve the UI freeze problem by using timer. But if your UI is freezing due to downloading then you can try asynchronous loading instead of using timer or detaching another thread.
EDIT: Unless you configure a run loop for secondary thread, timer is not going to work from that thread. Check the run loop management in threading programming guide. This can be a far difficult work than to use asynchronous connection.