AFNetworking/QHTTPOperation/QRunLoopOperation.m
2011-05-31 16:27:34 -05:00

359 lines
14 KiB
Objective-C

/*
File: QRunLoopOperation.m
Contains: An abstract subclass of NSOperation for async run loop based operations.
Written by: DTS
Copyright: Copyright (c) 2010 Apple Inc. All Rights Reserved.
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc.
("Apple") in consideration of your agreement to the following
terms, and your use, installation, modification or
redistribution of this Apple software constitutes acceptance of
these terms. If you do not agree with these terms, please do
not use, install, modify or redistribute this Apple software.
In consideration of your agreement to abide by the following
terms, and subject to these terms, Apple grants you a personal,
non-exclusive license, under Apple's copyrights in this
original Apple software (the "Apple Software"), to use,
reproduce, modify and redistribute the Apple Software, with or
without modifications, in source and/or binary forms; provided
that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the
following text and disclaimers in all such redistributions of
the Apple Software. Neither the name, trademarks, service marks
or logos of Apple Inc. may be used to endorse or promote
products derived from the Apple Software without specific prior
written permission from Apple. Except as expressly stated in
this notice, no other rights or licenses, express or implied,
are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or
by other works in which the Apple Software may be incorporated.
The Apple Software is provided by Apple on an "AS IS" basis.
APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT,
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING
THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
COMBINATION WITH YOUR PRODUCTS.
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY
OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY
OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR
OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
*/
#import "QRunLoopOperation.h"
/*
Theory of Operation
-------------------
Some critical points:
1. By the time we're running on the run loop thread, we know that all further state
transitions happen on the run loop thread. That's because there are only three
states (inited, executing, and finished) and run loop thread code can only run
in the last two states and the transition from executing to finished is
always done on the run loop thread.
2. -start can only be called once. So run loop thread code doesn't have to worry
about racing with -start because, by the time the run loop thread code runs,
-start has already been called.
3. -cancel can be called multiple times from any thread. Run loop thread code
must take a lot of care with do the right thing with cancellation.
Some state transitions:
1. init -> dealloc
2. init -> cancel -> dealloc
XXX 3. init -> cancel -> start -> finish -> dealloc
4. init -> cancel -> start -> startOnRunLoopThreadThread -> finish dealloc
!!! 5. init -> start -> cancel -> startOnRunLoopThreadThread -> finish -> cancelOnRunLoopThreadThread -> dealloc
XXX 6. init -> start -> cancel -> cancelOnRunLoopThreadThread -> startOnRunLoopThreadThread -> finish -> dealloc
XXX 7. init -> start -> cancel -> startOnRunLoopThreadThread -> cancelOnRunLoopThreadThread -> finish -> dealloc
8. init -> start -> startOnRunLoopThreadThread -> finish -> dealloc
9. init -> start -> startOnRunLoopThreadThread -> cancel -> cancelOnRunLoopThreadThread -> finish -> dealloc
!!! 10. init -> start -> startOnRunLoopThreadThread -> cancel -> finish -> cancelOnRunLoopThreadThread -> dealloc
11. init -> start -> startOnRunLoopThreadThread -> finish -> cancel -> dealloc
Markup:
XXX means that the case doesn't happen.
!!! means that the case is interesting.
Described:
1. It's valid to allocate an operation and never run it.
2. It's also valid to allocate an operation, cancel it, and yet never run it.
3. While it's valid to cancel an operation before it starting it, this case doesn't
happen because -start always bounces to the run loop thread to maintain the invariant
that the executing to finished transition always happens on the run loop thread.
4. In this -startOnRunLoopThread detects the cancellation and finishes immediately.
5. Because the -cancel can happen on any thread, it's possible for the -cancel
to come in between the -start and the -startOnRunLoop thread. In this case
-startOnRunLoopThread notices isCancelled and finishes straightaway. And
-cancelOnRunLoopThread detects that the operation is finished and does nothing.
6. This case can never happen because -performSelecton:onThread:xxx
callbacks happen in order, -start is synchronised with -cancel, and -cancel
only schedules if -start has run.
7. This case can never happen because -startOnRunLoopThread will finish immediately
if it detects isCancelled (see case 5).
8. This is the standard run-to-completion case.
9. This is the standard cancellation case. -cancelOnRunLoopThread wins the race
with finish, and it detects that the operation is executing and actually cancels.
10. In this case the -cancelOnRunLoopThread loses the race with finish, but that's OK
because -cancelOnRunLoopThread already does nothing if the operation is already
finished.
11. Cancellating after finishing still sets isCancelled but has no impact
on the RunLoop thread code.
*/
@interface QRunLoopOperation ()
// read/write versions of public properties
@property (assign, readwrite) QRunLoopOperationState state;
@property (copy, readwrite) NSError * error;
@end
@implementation QRunLoopOperation
- (id)init
{
self = [super init];
if (self != nil) {
assert(self->_state == kQRunLoopOperationStateInited);
}
return self;
}
- (void)dealloc
{
assert(self->_state != kQRunLoopOperationStateExecuting);
[self->_runLoopModes release];
[self->_runLoopThread release];
[self->_error release];
[super dealloc];
}
#pragma mark * Properties
@synthesize runLoopThread = _runLoopThread;
@synthesize runLoopModes = _runLoopModes;
- (NSThread *)actualRunLoopThread
// Returns the effective run loop thread, that is, the one set by the user
// or, if that's not set, the main thread.
{
NSThread * result;
result = self.runLoopThread;
if (result == nil) {
result = [NSThread mainThread];
}
return result;
}
- (BOOL)isActualRunLoopThread
// Returns YES if the current thread is the actual run loop thread.
{
return [[NSThread currentThread] isEqual:self.actualRunLoopThread];
}
- (NSSet *)actualRunLoopModes
{
NSSet * result;
result = self.runLoopModes;
if ( (result == nil) || ([result count] == 0) ) {
result = [NSSet setWithObject:NSDefaultRunLoopMode];
}
return result;
}
@synthesize error = _error;
#pragma mark * Core state transitions
- (QRunLoopOperationState)state
{
return self->_state;
}
- (void)setState:(QRunLoopOperationState)newState
// Change the state of the operation, sending the appropriate KVO notifications.
{
// any thread
@synchronized (self) {
QRunLoopOperationState oldState;
// The following check is really important. The state can only go forward, and there
// should be no redundant changes to the state (that is, newState must never be
// equal to self->_state).
assert(newState > self->_state);
// Transitions from executing to finished must be done on the run loop thread.
assert( (newState != kQRunLoopOperationStateFinished) || self.isActualRunLoopThread );
// inited + executing -> isExecuting
// inited + finished -> isFinished
// executing + finished -> isExecuting + isFinished
oldState = self->_state;
if ( (newState == kQRunLoopOperationStateExecuting) || (oldState == kQRunLoopOperationStateExecuting) ) {
[self willChangeValueForKey:@"isExecuting"];
}
if (newState == kQRunLoopOperationStateFinished) {
[self willChangeValueForKey:@"isFinished"];
}
self->_state = newState;
if (newState == kQRunLoopOperationStateFinished) {
[self didChangeValueForKey:@"isFinished"];
}
if ( (newState == kQRunLoopOperationStateExecuting) || (oldState == kQRunLoopOperationStateExecuting) ) {
[self didChangeValueForKey:@"isExecuting"];
}
}
}
- (void)startOnRunLoopThread
// Starts the operation. The actual -start method is very simple,
// deferring all of the work to be done on the run loop thread by this
// method.
{
assert(self.isActualRunLoopThread);
assert(self.state == kQRunLoopOperationStateExecuting);
if ([self isCancelled]) {
// We were cancelled before we even got running. Flip the the finished
// state immediately.
[self finishWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
} else {
[self operationDidStart];
}
}
- (void)cancelOnRunLoopThread
// Cancels the operation.
{
assert(self.isActualRunLoopThread);
// We know that a) state was kQRunLoopOperationStateExecuting when we were
// scheduled (that's enforced by -cancel), and b) the state can't go
// backwards (that's enforced by -setState), so we know the state must
// either be kQRunLoopOperationStateExecuting or kQRunLoopOperationStateFinished.
// We also know that the transition from executing to finished always
// happens on the run loop thread. Thus, we don't need to lock here.
// We can look at state and, if we're executing, trigger a cancellation.
if (self.state == kQRunLoopOperationStateExecuting) {
[self finishWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
}
}
- (void)finishWithError:(NSError *)error
{
assert(self.isActualRunLoopThread);
// error may be nil
if (self.error == nil) {
self.error = error;
}
[self operationWillFinish];
self.state = kQRunLoopOperationStateFinished;
}
#pragma mark * Subclass override points
- (void)operationDidStart
{
assert(self.isActualRunLoopThread);
}
- (void)operationWillFinish
{
assert(self.isActualRunLoopThread);
}
#pragma mark * Overrides
- (BOOL)isConcurrent
{
// any thread
return YES;
}
- (BOOL)isExecuting
{
// any thread
return self.state == kQRunLoopOperationStateExecuting;
}
- (BOOL)isFinished
{
// any thread
return self.state == kQRunLoopOperationStateFinished;
}
- (void)start
{
// any thread
assert(self.state == kQRunLoopOperationStateInited);
// We have to change the state here, otherwise isExecuting won't necessarily return
// true by the time we return from -start. Also, we don't test for cancellation
// here because that would a) result in us sending isFinished notifications on a
// thread that isn't our run loop thread, and b) confuse the core cancellation code,
// which expects to run on our run loop thread. Finally, we don't have to worry
// about races with other threads calling -start. Only one thread is allowed to
// start us at a time.
self.state = kQRunLoopOperationStateExecuting;
[self performSelector:@selector(startOnRunLoopThread) onThread:self.actualRunLoopThread withObject:nil waitUntilDone:NO modes:[self.actualRunLoopModes allObjects]];
}
- (void)cancel
{
BOOL runCancelOnRunLoopThread;
BOOL oldValue;
// any thread
// We need to synchronise here to avoid state changes to isCancelled and state
// while we're running.
@synchronized (self) {
oldValue = [self isCancelled];
// Call our super class so that isCancelled starts returning true immediately.
[super cancel];
// If we were the one to set isCancelled (that is, we won the race with regards
// other threads calling -cancel) and we're actually running (that is, we lost
// the race with other threads calling -start and the run loop thread finishing),
// we schedule to run on the run loop thread.
runCancelOnRunLoopThread = ! oldValue && self.state == kQRunLoopOperationStateExecuting;
}
if (runCancelOnRunLoopThread) {
[self performSelector:@selector(cancelOnRunLoopThread) onThread:self.actualRunLoopThread withObject:nil waitUntilDone:YES modes:[self.actualRunLoopModes allObjects]];
}
}
@end