I am creating a new Thread which runs one of my method:
Now what i am doing is as follows:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:#selector(myThreadFunc) object:nil];
[thread start];
in myThreadFunc
{
while(isRunning){
[self updateSomething];
[NSThread sleepForTimeInterval:3.0];
}
NSLog(#"out");
}
In another func,i set isRunning = NO and thread = nil or [thread cancel] but myThreadFunc is sleeping so thread cannot exit.
How can i control this case?
Thanks so much.
Don't use a thread. Use a timer. If the something is expensive, dispatch it off to some queue other than the main queue and set some state variable to show it's still running (if the something isn't meant to run concurrently). Then, just cancel your timer. A simple example of the timer call-back function might be:
- (void)doSomething:(NSTimer*)timer
{
// this assumes that this "something" only ever
// runs once at a time no matter what, adjust this
// to an ivar if it's per-class instance or something
static BOOL alreadyDoingSomething = NO;
if( alreadyDoingSomething ) return;
alreadyDoingSomething = YES;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self updateSomething];
alreadyDoingSomething = NO;
});
}
Now, if you simply cancel the timer, this will stop running. When you're ready to start it again, schedule a new timer with this method as the designated selector. To make this behave similar to your example above, you might set the timer interval to three seconds.
Related
I have an app which communicates with an ExternalAccessory over Bluetooth, there is some delay in responses so I want the IO to happen on a background thread.
I setup an NSOperationQueue for single-threaded operation to enqueue my requests:
self.sessionQueue = [NSOperationQueue new];
self.sessionQueue.maxConcurrentOperationCount = 1;
If I schedule reads and writes to the EAAccessory streams from that queue, my app crashes because data from the socket can't be delivered without an NSRunLoop on the thread that the queue is using. Immediately after initializing the queue, I create a run loop with an empty NSMachPort to keep it running and start it:
[self.sessionQueue addOperationWithBlock:^{
NSRunLoop* queueLoop = [NSRunLoop currentRunLoop];
[queueLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
[queueLoop run]; // <- this blocks
}];
This blocks the queue as the run loop will never exit, but I'm not sure how to correctly manage the run loop so that I can successfully read from the accessory streams.
You shouldn't try to run a run loop inside an NSOperation. Grand Central Dispatch owns the thread on which the operation is running. You should start your own thread and use its run loop for your session streams.
However, you need to be aware that NSRunLoop is not generally thread safe, but CFRunLoop is. This means that you need to drop down to the CFRunLoop level when you want to run a block on your session-handling thread.
Also, the only way to get a reference to a background thread's run loop is to run something on that background thread. So step one is to create your own NSThread subclass that exports its own run loop:
typedef void (^MyThreadStartCallback)(CFRunLoopRef runLoop);
#interface MyThread: NSThread
/// After I'm started, I dispatch to the main queue to call `callback`,
// passing my runloop. Then I destroy my reference to `callback`.
- (instancetype)initWithCallback:(MyThreadStartCallback)callback;
#end
#implementation MyThread {
MyThreadStartCallback _callback;
}
- (instancetype)initWithCallback:(MyThreadStartCallback)callback {
if (self = [super init]) {
_callback = callback;
}
return self;
}
- (void)main {
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
dispatch_async(dispatch_get_main_queue(), ^{
_callback(runLoop);
});
_callback = nil;
CFRunLoopRun();
}
#end
Now you can create an instance of MyThread, passing in a callback. When you start MyThread, it will make that callback run back on the main thread, and it will pass its own (MyThread's) run loop to the callback. So you can use a MyThread as your session-handling thread, like this:
#implementation Thing {
CFRunLoopRef _sessionRunLoop;
}
- (void)scheduleStreamsOfSession:(EASession *)session {
MyThread *thread = [[MyThread alloc] initWithCallback:^(CFRunLoopRef runLoop) {
// Here I'm on the main thread, but the session-handling thread has
// started running and its run loop is `runLoop`.
[self scheduleStreamsOfSession:session inRunLoop:runLoop];
}];
[thread start];
}
- (void)scheduleStreamsOfSession:(EASession *)session inRunLoop:(CFRunLoopRef)runLoop {
// Here I'm on the main thread. I'll save away the session-handling run loop
// so I can run more blocks on it later, perhaps to queue data for writing
// to the output stream.
_sessionRunLoop = runLoop;
NSInputStream *inputStream = session.inputStream;
NSOutputStream *outputStream = session.outputStream;
// Here I'm on the main thread, where it's not safe to use the
// session-handling thread's NSRunLoop, so I'll send a block to
// the session-handling thread.
CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, ^{
// Here I'm on the session-handling thread, where it's safe to
// use NSRunLoop to schedule the streams.
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
[inputStream scheduleInRunLoop:currentRunLoop forMode:NSRunLoopCommonModes];
[outputStream scheduleInRunLoop:currentRunLoop forMode:NSRunLoopCommonModes];
});
// CFRunLoopPerformBlock does **not** wake up the run loop. Since I want
// to make sure the block runs as soon as possible, I have to wake up the
// run loop manually:
CFRunLoopWakeUp(_sessionRunLoop);
}
#end
Any thread can have an NSRunLoop created for it if needed, the main thread of any Cocoa or AppKit application has one running by default and any secondary threads must run them programmatically. If you were spawning an NSThread the thread body would be responsible for starting the NSRunLoop but an NSOperationQueue creates it's own thread or threads and dispatches operations to them.
When using an API which expects an NSRunLoop to deliver events to and from a background thread, either of your own creation, or one that libdispatch has created, you are responsible for making sure the NSRunLoop is run. Typically you will want to run the loop until some condition is met in each of your NSBlockOperation tasks, I wrote a category on NSRunLoop which simplifies this:
#import <Foundation/Foundation.h>
#interface NSRunLoop (Conditional)
-(BOOL)runWhileCondition:(BOOL *)condition inMode:(NSString *)mode inIntervals:(NSTimeInterval) quantum;
#end
#pragma mark -
#implementation NSRunLoop (Conditional)
-(BOOL)runWhileCondition:(BOOL *)condition inMode:(NSString *)mode inIntervals:(NSTimeInterval) quantum {
BOOL didRun = NO;
BOOL shouldRun = YES;
NSPort *dummyPort = [NSMachPort port];
[self addPort:dummyPort forMode:NSDefaultRunLoopMode];
while (shouldRun) {
#autoreleasepool {
didRun = [self runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:quantum]];
shouldRun = (didRun ? *condition : NO);
}
}
[self removePort:dummyPort forMode:NSDefaultRunLoopMode];
return didRun;
}
#end
With this condition you can schedule an NSBlockOperation which will start the run loop and run until the specified condition is NO:
__block BOOL streamOperationInProgress = YES;
[self.sessionQueue addOperationWithBlock:^{
NSRunLoop *queueLoop = [NSRunLoop currentRunLoop];
NSStream *someStream = // from somewhere...
[someStream setDelegate:self];
[someStream scheduleInRunLoop:queueLoop forMode:NSDefaultRunLoopMode]:
// the delegate implementation of stream:handleEvent:
// sets streamOperationInProgress = NO;
[queueLoop
runWhileCondition:&streamOperationInProgress
inMode:NSDefaultRunLoopMode
inIntervals:0.001];
}];
The wrinkle in the above example is putting the BOOL someplace that the delegate can set it to NO when the operation is complete.
Here's a gist of the NSRunLoop+Condition category.
I'm attempting to debug some iOS crash logs that contain the following error message:
*** Terminating app due to uncaught exception 'NSDestinationInvalidException', reason: '*** -[SomeClass
performSelector:onThread:withObject:waitUntilDone:modes:]: target
thread exited while waiting for the perform
The relevant section of the code is:
- (void) runInvocationOnMyThread:(NSInvocation*)invocation {
NSThread* currentThread = [NSThread currentThread];
if (currentThread != myThread) {
//call over to the correct thread
[self performSelector:#selector(runInvocationOnMyThread:) onThread:myThread withObject:invocation waitUntilDone:YES];
}
else {
//we're okay to invoke the target now
[invocation invoke];
}
}
This is similar to the issue discussed here, except that I'm not trying to cancel my onThread: thread. In fact, in my case onThread: is being passed a reference to the application's main thread, so it should not be possible for it to terminate unless the entire app is terminating.
So the first question is, is the "target" thread referred to in the error message the one I'm passing to onThread:, or the one that's waiting for the invocation to complete on the onThread: thread?
I've assumed that it's the second option, as if the main thread really has terminated the crash of the background thread is kind of moot anyways.
With that in mind, and based upon the following discussion from the reference docs for performSelector:onThread:...:
Special Considerations
This method registers with the runloop of its
current context, and depends on that runloop being run on a regular
basis to perform correctly. One common context where you might call
this method and end up registering with a runloop that is not
automatically run on a regular basis is when being invoked by a
dispatch queue. If you need this type of functionality when running on
a dispatch queue, you should use dispatch_after and related methods to
get the behavior you want.
...I've modified my code to prefer the use of GCD over performSelector:onThread:..., as follows:
- (void) runInvocationOnMyThread:(NSInvocation*)invocation {
NSThread* currentThread = [NSThread currentThread];
if (currentThread != myThread) {
//call over to the correct thread
if ([myThread isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
[invocation invoke];
});
}
else {
[self performSelector:#selector(runInvocationOnMyThread:) onThread:myThread withObject:invocation waitUntilDone:YES];
}
}
else {
//we're okay to invoke the target now
[invocation invoke];
}
}
Which seems to work fine (though no idea if it fixes the crash, as it's an exceedingly rare crash). Perhaps someone can comment on whether this approach is more or less prone to crashing than the original?
Anyways, the main problem is that there's only an obvious way to use GCD when the target thread is the main thread. In my case, this is true, but I'd like to be able to use GCD regardless of whether or not the target thread is the main thread.
So the more important question is, is there a way to map from an arbitrary NSThread to a corresponding queue in GCD? Ideally something along the lines of dispatch_queue_t dispatch_get_queue_for_thread(NSThread* thread), so that I can revise my code to be:
- (void) runInvocationOnMyThread:(NSInvocation*)invocation {
NSThread* currentThread = [NSThread currentThread];
if (currentThread != myThread) {
//call over to the correct thread
dispatch_sync(dispatch_get_queue_for_thread(myThread), ^{
[invocation invoke];
});
}
else {
//we're okay to invoke the target now
[invocation invoke];
}
}
Is this possible, or is there not a direct mapping from NSThread to GCD queue that can be applied?
Given your stated goal of wrapping a 3rd party API that requires thread affinity, you might try something like using a forwarding proxy to ensure methods are only called on the correct thread. There are a few tricks to doing this, but I managed to whip something up that might help.
Let's assume you have an object XXThreadSensitiveObject with an interface that looks something like this:
#interface XXThreadSensitiveObject : NSObject
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (void)foo;
- (void)bar;
- (NSInteger)addX: (NSInteger)x Y: (NSInteger)y;
#end
And the goal is for -foo, -bar and -addX:Y: to always be called on the same thread.
Let's also say that if we create this object on the main thread, then our expectation is that the main thread is the blessed thread and all calls should be on the main thread, but that if it's created from any non-main thread, then it should spawn its own thread so it can guarantee thread affinity going forward. (Because GCD managed threads are ephemeral, there is no way to have thread affinity with a GCD managed thread.)
One possible implementation might look like this:
// Since NSThread appears to retain the target for the thread "main" method, we need to make it separate from either our proxy
// or the object itself.
#interface XXThreadMain : NSObject
#end
// This is a proxy that will ensure that all invocations happen on the correct thread.
#interface XXThreadAffinityProxy : NSProxy
{
#public
NSThread* mThread;
id mTarget;
XXThreadMain* mThreadMain;
}
#end
#implementation XXThreadSensitiveObject
{
// We don't actually *need* this ivar, and we're skankily stealing it from the proxy in order to have it.
// It's really just a diagnostic so we can assert that we're on the right thread in method calls.
__unsafe_unretained NSThread* mThread;
}
- (instancetype)init
{
if (self = [super init])
{
// Create a proxy for us (that will retain us)
XXThreadAffinityProxy* proxy = [[XXThreadAffinityProxy alloc] initWithTarget: self];
// Steal a ref to the thread from it (as mentioned above, this is not required.)
mThread = proxy->mThread;
// Replace self with the proxy.
self = (id)proxy;
}
// Return the proxy.
return self;
}
- (void)foo
{
NSParameterAssert([NSThread currentThread] == mThread || (!mThread && [NSThread isMainThread]));
NSLog(#"-foo called on %#", [NSThread currentThread]);
}
- (void)bar
{
NSParameterAssert([NSThread currentThread] == mThread || (!mThread && [NSThread isMainThread]));
NSLog(#"-bar called on %#", [NSThread currentThread]);
}
- (NSInteger)addX: (NSInteger)x Y: (NSInteger)y
{
NSParameterAssert([NSThread currentThread] == mThread || (!mThread && [NSThread isMainThread]));
NSLog(#"-addX:Y: called on %#", [NSThread currentThread]);
return x + y;
}
#end
#implementation XXThreadMain
{
NSPort* mPort;
}
- (void)dealloc
{
[mPort invalidate];
}
// The main routine for the thread. Just spins a runloop for as long as the thread isnt cancelled.
- (void)p_threadMain: (id)obj
{
NSThread* thread = [NSThread currentThread];
NSParameterAssert(![thread isMainThread]);
NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];
mPort = [NSPort port];
// If we dont register a mach port with the run loop, it will just exit immediately
[currentRunLoop addPort: mPort forMode: NSRunLoopCommonModes];
// Just loop until the thread is cancelled.
while (!thread.cancelled)
{
[currentRunLoop runMode: NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]];
}
[currentRunLoop removePort: mPort forMode: NSRunLoopCommonModes];
[mPort invalidate];
mPort = nil;
}
- (void)p_wakeForThreadCancel
{
// Just causes the runloop to spin so that the loop in p_threadMain can notice that the thread has been cancelled.
}
#end
#implementation XXThreadAffinityProxy
- (instancetype)initWithTarget: (id)target
{
mTarget = target;
mThreadMain = [[XXThreadMain alloc] init];
// We'll assume, from now on, that if mThread is nil, we were on the main thread.
if (![NSThread isMainThread])
{
mThread = [[NSThread alloc] initWithTarget: mThreadMain selector: #selector(p_threadMain:) object:nil];
[mThread start];
}
return self;
}
- (void)dealloc
{
if (mThread && mThreadMain)
{
[mThread cancel];
const BOOL isCurrent = [mThread isEqual: [NSThread currentThread]];
if (!isCurrent && !mThread.finished)
{
// Wake it up.
[mThreadMain performSelector: #selector(p_wakeForThreadCancel) onThread:mThread withObject: nil waitUntilDone: YES modes: #[NSRunLoopCommonModes]];
}
}
mThreadMain = nil;
mThread = nil;
}
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature *sig = [[mTarget class] instanceMethodSignatureForSelector:selector];
if (!sig)
{
sig = [NSMethodSignature signatureWithObjCTypes:"#^v^c"];
}
return sig;
}
- (void)forwardInvocation:(NSInvocation*)invocation
{
if ([mTarget respondsToSelector: [invocation selector]])
{
if ((!mThread && [NSThread isMainThread]) || (mThread && [mThread isEqual: [NSThread currentThread]]))
{
[invocation invokeWithTarget: mTarget];
}
else if (mThread)
{
[invocation performSelector: #selector(invokeWithTarget:) onThread: mThread withObject: mTarget waitUntilDone: YES modes: #[ NSRunLoopCommonModes ]];
}
else
{
[invocation performSelectorOnMainThread: #selector(invokeWithTarget:) withObject: mTarget waitUntilDone: YES];
}
}
else
{
[mTarget doesNotRecognizeSelector: invocation.selector];
}
}
#end
The ordering here is a little wonky, but XXThreadSensitiveObject can just do its work. XXThreadAffinityProxy is a thin proxy that does nothing other than ensuring that the invocations are happening on the right thread, and XXThreadMain is just a holder for the subordinate thread's main routine and some other minor mechanics. It's essentially just a workaround for a retain cycle that would otherwise be created between the thread and the proxy which has philosophical ownership of the thread.
The thing to know here is that threads are a relatively heavy abstraction, and are a limited resource. This design assumes that you're going to make one or two of these things and that they will be long lived. This usage pattern makes sense in the context of wrapping a 3rd party library that expects thread affinity, since that would typically be a singleton anyway, but this approach won't scale to more than a small handful of threads.
To your first Q:
I think the thread, sending the message is meant. But I cannot explain how this can happen.
Second: I would not mix NSThread and GCD. I think that there will be more problems than solutions. This is because of your last Q:
Each block is running on one thread. At least this is done, because thread migration for a block would be expensive. But different blocks in a queue can be distributed to many threads. This is obvious for parallel queues, but true for serial, too. (And have seen this in practice.)
I recommend to move your whole code to GCD. Once you are convenient with it, it is very easy to use and less error prone.
There is no mapping at all between queues and threads, with the sole exception of the main queue which always runs on the main thread. Any queue which targets the main queue will, of course, also run on the main thread. Any background queue can run on any thread, and can change thread from one block execution to the next. This is equally true for serial queues and for concurrent queues.
GCD maintains a thread pool which gets used for executing blocks according to the policies determined by the queue to which the block belongs. You are not supposed to know anything about those particular threads.
I am having some trouble updating my UI using performSelectorOnMainThread. Here is my situation. In my viewDidLoad I set up an activity indicator and a label. Then I call a selector to retrieve some data from a server. Then I call a selector to update the UI after a delay. Here's the code:
- (void)viewDidLoad
{
[super viewDidLoad];
self.reloadSchools = [[UIAlertView alloc] init];
self.reloadSchools.message = #"There was an error loading the schools. Please try again.";
self.reloadSchools.title = #"We're Sorry";
self.schoolPickerLabel = [[UILabel alloc]init];
self.schoolPicker = [[UIPickerView alloc] init];
self.schoolPicker.delegate = self;
self.schoolPicker.dataSource = self;
self.server = [[Server alloc]init];
schoolList = NO;
_activityIndicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self.view addSubview:_activityIndicator];
[self.view bringSubviewToFront:_activityIndicator];
[_activityIndicator startAnimating];
[NSThread detachNewThreadSelector: #selector(getSchoolList) toTarget: self withObject: nil];
[self performSelector:#selector(updateUI) withObject:nil afterDelay:20.0];
}
The selector updateUI checks to see if the data was retrieved, and calls a selector on the main thread to update the UI accordingly. Here is the code for these parts:
-(void)updateUI
{
self.schools = [_server returnData];
if(!(self.schools == nil)) {
[self performSelectorOnMainThread:#selector(fillPickerView) withObject:nil waitUntilDone:YES];
}
else {
[self performSelectorOnMainThread:#selector(showError) withObject:nil waitUntilDone:YES];
}
}
-(void)showError {
NSLog(#"show error");
[_activityIndicator stopAnimating];
[self.reloadSchools show];
}
-(void)fillPickerView {
NSLog(#"fill picker view");
schoolList = YES;
NSString *schoolString = [[NSString alloc] initWithData:self.schools encoding:NSUTF8StringEncoding];
self.schoolPickerLabel.text = #"Please select your school:";
self.shoolArray = [[schoolString componentsSeparatedByString:#"#"] mutableCopy];
[self.schoolPicker reloadAllComponents];
[_activityIndicator stopAnimating];
}
When the selector fillPickerView is called the activity indicator keeps spinning, the label text doesn't change, and the picker view doesn't reload its content. Can someone explain to me why the method I am using isn't working to update my ui on the main thread?
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//load your data here.
dispatch_async(dispatch_get_main_queue(), ^{
//update UI in main thread.
});
});
First of all you should not be using detachNewThreadSelector. You should use GCD and submit your background task to an async queue. Threads are costly to create. GCD does a much better job of managing system resources.
Ignoring that, your code doesn't make a lot of sense to me. You submit a method, getSchoolList, to run on a background thread. You don't show the code that you are running in the background.
Then use performSelector:withObject:afterDelay to run the method updateUI on the main thread after a fixed delay of 20 seconds.
updateUI checks for self.schools, which presumably was set up by your background thread, and may or may not be done. If self.schools IS nil, you call fillPickerView using performSelectorOnMainThread. That doesn't make sense because if self.schools is nil, there is no data to fill the picker.
If self.schools is not nil, you display an error, again using performSelectorOnMainThread.
It seems to me that the logic on your check of self.schools is backwards. If it is nil you should display an error and if it is NOT nil you should fill the picker.
Next problem: In both cases you're calling performSelectorOnMainThread:withObject:waitUntilDone: from the main thread. Calling that method from the main thread doesn't make sense.
Third problem: It doesn't make sense to wait an arbitrary amount of time for a background task to run to completion, and then either succeed or fail. You won't have any idea what's going on for the full 20 seconds. If the background task finishes sooner, you'll never know.
Instead, you should have your background task notify the main thread once the task is done. That would be a valid use of performSelectorOnMainThread:withObject:waitUntilDone:, while calling it from the main thread is not. (Again, though, you should refactor this code to use GCD, not using threads directly.
It seems pretty clear that you are in over your head. The code you posted needs to be rewritten completely.
My code is like below, I am trying ot call a cpu intensive method inside an async block, but that method is not being always called, sometimes it's called, sometimes not.
-(IBAction) aMethod
//some code
dispatch_async(backgroundQueue, ^{
NSArray *allFiles = [appDel getAllFiles];
totalFilesCount = [allFiles count];
});
//some code
}
backgroundQueue is an instance variable declared in the interface file and created in the viewDidLoad method:
backgroundQueue = dispatch_queue_create("com.myapp.backgroundprocessing", NULL);
The method getAllFiles is sometimes called and sometimes not (checked with breakpoints), the reason I am calling it into a dispatch_async block is that it took a lot of time to return the result. What seems to be wrong with my approach, should it be called in the main thread although it's cpu intensive method?
backgroundQueue = dispatch_queue_create(..., NULL);
creates a serial queue, and not a concurrent queue as you assumed.
Therefore the block dispatched with
dispatch_async(backgroundQueue, ^{ ... });
is executed when all previous blocks dispatched to that queue have finished.
So if the first invocation is still running, it may look as if the second block
is not executed at all.
To create a concurrent queue, you would use
backgroundQueue = dispatch_queue_create(..., DISPATCH_QUEUE_CONCURRENT);
But you should check if it makes sense in your case to start another
(long running) [appDel getAllFiles] operation if the previous operation is still
running.
A possible alternative would be to start another [appDel getAllFiles] operation only
if the previous one has already finished, and to do nothing if the previous operation
is still running. That can be done e.g. with an NSOperationQueue:
if ([opQueue operationCount] == 0) {
[opQueue addOperationWithBlock:^{
NSArray *allFiles = [appDel getAllFiles];
totalFilesCount = [allFiles count];
}];
}
I have a method that runs concurrently with recording a video. When the method ends it fires off a chain of other methods that continues until the recording ends. I want to be able to press a button to stop the recording prematurely that also exits the method at the same time. The way I'm currently trying to do it is with an NSTimer that checks to see if the recording is still happening, and if it isn't, it stops playing audio and should also call return to stop the method.
-(void) method
{
self.stopTimer = [NSTimer scheduledTimerWithTimeInterval:.05 target:self selector:#selector(checkRecording) userInfo:nil repeats:YES];
// Stuff happens
}
-(void) checkRecording
{
if (isRecording == NO)
{
if (player.playing == YES)
{
[player stop];
}
return;
}
}
This stops the audio immediately but the method continues to run until it's done. It doesn't call the next method in the sequence, which is a step in the right direction, but I need it to stop immediately. My only theory is that it's because I'm not calling return inside the actual method that I want to stop and instead in a different method, but even if that's the case I'm not really sure how to fix that because as far as I know timers can only point to other methods and I can't just tell it what I want it to do inside of the method that I want to stop. And if that's not the issue then I'm really not sure why this isn't working.
If a timer is valid you can invalidate it (that stops the timer).
I'm not sure if all the checking is really necessary (& the last line) but I do it currently that way:
if ( myTimer != nil && [myTimer isValid] )
{
[myTimer invalidate];
myTimer = nil;
}
EDITED:
if ( [myTimer isValid] )
{
[myTimer invalidate];
myTimer = nil;
}
My only theory is that it's because I'm not calling return inside the actual method that I want to stop and instead in a different method
Your theory is correct. return ends the function or method it is in, and none other. It pops the current function's context off the stack and returns execution to the calling function.
I'm not really sure how to fix that because as far as I know timers can only point to other methods and I can't just tell it what I want it to do inside of the method that I want to stop
We can use objects to store state and use that state to control the flow of our program. That state can be continually updated and checked. With a long-running task that needs to be cancelled in response to changes in that state, the state must be updated in parallel with the task. Since you say the timer works for stopping audio, but that the work done in method doesn't, I'm assuming that method is performing its long-running task asynchronously already.
This need to do an asynchronous long-running task (or series of tasks) in the background, with the possibility of cancellation, is nicely matched to the NSOperation and NSOperationQueue classes.
You can perform your work inside NSOperation objects, either via implementing methods or blocks. Implement your code to check if the operation has been cancelled at all appropriate times, and bail out as soon as that happens.
Below is an example that hopefully matches your use case. It was created in an iOS app 'empty application' template an everything is in the application delegate. Our app delegate keeps track of the state necessary to make the decision of whether to cancel or not, and also schedules a timer to poll for changes in that state. If it does determine that it should cancel, it delegates the actual cancellation of work to the operation queue and its operations.
#import "AppDelegate.h"
#interface AppDelegate ()
#property (nonatomic) BOOL shouldStop; // Analogous to your isRecording variable
#property (nonatomic, strong) NSOperationQueue *operationQueue; // This manages execution of the work we encapsulate into NSOperation objects
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Typical app delegate stuff
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
// Start our long running method - analogous to method in your example
[self method];
return YES;
}
- (void)method
{
// allocate operation queue and set its concurrent operation count to 1. this gives us basic ordering of
// NSOperations. More complex ordering can be done by specifying dependencies on operations.
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
// We create three NSBlockOperations. They only sleep the thread a little while,
// check if they've been cancelled and should stop, and keep doing that for a few seconds.
// When they are completed (either through finishing normally or through being cancelled, they
// log a message
NSMutableArray *operations = [NSMutableArray array];
for (int i = 0; i < 3; i++) {
// Block operations allow you to specify their work by providing a block.
// You can override NSOperation to provide your own custom implementation
// of main, or start, depending. Read the documentation for more details.
// The principle will be the same - check whether one should cancel at each
// appropriate moment and bail out if so
NSBlockOperation *operation = [[NSBlockOperation alloc] init];
// For the "weak/strong dance" to avoid retain cycles
__weak NSBlockOperation *weakOperation = operation;
[operation addExecutionBlock:^{
// Weak/strong dance
NSBlockOperation *strongOperation = weakOperation;
// Here is where you'd be doing actual work
// Either in a block or in the main / start
// method of your own NSOperation subclass.
// Instead we sleep for some time, check if
// cancelled, bail out if so, and then sleep some more.
for (int i = 0; i < 300; i++) {
if ([strongOperation isCancelled]) {
return;
}
usleep(10000);
}
}];
// The completion block is called whether the operation is cancelled or not.
operation.completionBlock = ^{
// weak/strong dance again
NSBlockOperation *strongOperation = weakOperation;
NSLog(#"Operation completed, %# cancelled.", [strongOperation isCancelled] ? #"WAS" : #"WAS NOT");
};
[operations addObject:operation];
}
// Set up a timer that checks the status of whether we should stop.
// This timer will cancel the operations if it determines it should.
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(checkShouldKeepGoing:) userInfo:nil repeats:YES];
// Use GCD to simulate a stopped recording to observe how the operations react to that.
// Comment out to see the usual case.
double delayInSeconds = 5;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
self.shouldStop = YES;
});
// Add the operations to the operation queue, exeuction will start asynchronously from here.
[self.operationQueue addOperations:operations waitUntilFinished:NO];
}
// If we should stop, cancel the operations in the queue.
- (void)checkShouldKeepGoing:(NSTimer *)timer
{
if (self.shouldStop) {
NSLog(#"SHOULD STOP");
[timer invalidate];
[self.operationQueue cancelAllOperations];
}
}
#end