重置 NSOperation

Resetting NSOperation

我的网络请求需要以 FIFO 串行方式发生。如果由于网络问题(即离线、超时)而失败,我需要失败的请求在继续队列之前重试。我怎样才能做到这一点?

当操作失败时,我将执行和完成都设置为 - 假设这将作为 NSOperation 的 'reset'。但是队列永远不会恢复。

有没有我遗漏的东西?

我使用这个自定义 class 来管理 NSOperation。我对 GCDWebServer 中的代码进行了一些修改,并得出了这个存根。其中大部分是不言自明的。

@interface SROperation : NSOperation <NSCopying>


///-------------------------------
/// @name error reporting
///-------------------------------

/**
 The error, if any, that occurred in the lifecycle of the request.
 */
@property (nonatomic, strong) NSError *error;


///-----------------------------------------
/// @name the operation properties
///-----------------------------------------

/**
 a dictionary.
 */
@property (nonatomic, strong) NSDictionary*aDictionary;



///----------------------------------
/// @name Pausing / Resuming Requests
///----------------------------------

+(instancetype)operationWithDictionary:(NSDictionary*)dict;

/**
 Pauses the execution of the request operation.

 A paused operation returns `NO` for `-isReady`, `-isExecuting`, and `-isFinished`. As such, it will remain in an `NSOperationQueue` until it is either cancelled or resumed. Pausing a finished, cancelled, or paused operation has no effect.
 */
- (void)pause;

/**
 Whether the request operation is currently paused.

 @return `YES` if the operation is currently paused, otherwise `NO`.
 */
@property (NS_NONATOMIC_IOSONLY, getter=isPaused, readonly) BOOL paused;

/**
 Resumes the execution of the paused request operation.

 Pause/Resume behavior varies depending on the underlying implementation for the operation class. In its base implementation, resuming a paused requests restarts the original request. However, since HTTP defines a specification for how to request a specific content range, `AFHTTPRequestOperation` will resume downloading the request from where it left off, instead of restarting the original request.
 */
- (void)resume;

@end

SROperation.m

#import "SROperation.h"

typedef NS_ENUM(NSInteger, SROperationState) {
    SROperationPausedState      = -1,
    SROperationReadyState       = 1,
    SROperationExecutingState   = 2,
    SROperationFinishedState    = 3,
};

static NSString * const kSROperationLockName = @"your.application.bundle.id.operationlock"

static inline BOOL SRStateTransitionIsValid(SROperationState fromState, SROperationState toState, BOOL isCancelled) {
    switch (fromState) {
        case SROperationReadyState:
            switch (toState) {
                case SROperationPausedState:
                case SROperationExecutingState:
                    return YES;
                case SROperationFinishedState:
                    return isCancelled;
                default:
                    return NO;
            }
        case SROperationExecutingState:
            switch (toState) {
                case SROperationPausedState:
                case SROperationFinishedState:
                    return YES;
                default:
                    return NO;
            }
        case SROperationFinishedState:
            return NO;
        case SROperationPausedState:
            return toState == SROperationReadyState;
        default: {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
            switch (toState) {
                case SROperationPausedState:
                case SROperationReadyState:
                case SROperationExecutingState:
                case SROperationFinishedState:
                    return YES;
                default:
                    return NO;
            }
        }
#pragma clang diagnostic pop
    }
}


static inline NSString * SRKeyPathFromSROperationState(SROperationState state) {
    switch (state) {
        case SROperationReadyState:
            return @"isReady";
        case SROperationExecutingState:
            return @"isExecuting";
        case SROperationFinishedState:
            return @"isFinished";
        case SROperationPausedState:
            return @"isPaused";
        default: {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
            return @"state";
#pragma clang diagnostic pop
        }
    }
}

@interface SROperation ()

    @property (readwrite, nonatomic, assign) SROperationState state;
    @property (readwrite, nonatomic, strong) NSRecursiveLock *lock;
@end

@implementation SROperation

    +(instancetype)operationWithDictionary:(NSDictionary*)dict {
        return [[self alloc] initWithDictionary:dict];
    }

    -(instancetype)initWithDictionary:(NSDictionary*)aDictionary {
        self = [self init];

        if (self)
        {
            self.aDictionary = aDictionary;
            self.queuePriority = NSOperationQueuePriorityVeryHigh;
        }

        return self;
    }

    -(instancetype)init
    {
        self = [super init];
        if(self) {
            _state = SROperationReadyState;
            _lock = [[NSRecursiveLock alloc] init];
            _lock.name = kSROperationLockName;
        }
        return self;
    }

    #pragma mark -

    - (void)setState:(SROperationState)state {
        if (!SRStateTransitionIsValid(self.state, state, [self isCancelled])) {
            return;
        }

        [self.lock lock];
        NSString *oldStateKey = SRKeyPathFromSROperationState(self.state);
        NSString *newStateKey = SRKeyPathFromSROperationState(state);

        [self willChangeValueForKey:newStateKey];
        [self willChangeValueForKey:oldStateKey];
        _state = state;
        [self didChangeValueForKey:oldStateKey];
        [self didChangeValueForKey:newStateKey];
        [self.lock unlock];
    }

    - (void)pause {
        if ([self isPaused] || [self isFinished] || [self isCancelled]) {
            return;
        }

        [self.lock lock];
        if ([self isExecuting]) {


            dispatch_async(dispatch_get_main_queue(), ^{
                NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
                [notificationCenter postNotificationName:SROperationDidFinishNotification object:self userInfo:nil];
            });
        }

        self.state = SROperationPausedState;
        [self.lock unlock];
    }

    -(void)operationDidPause
    {

    }

    - (BOOL)isPaused {
        return self.state == SROperationPausedState;
    }

    - (void)resume {
        if (![self isPaused]) {
            return;
        }

        [self.lock lock];
        self.state = SROperationReadyState;

        [self start];
        [self.lock unlock];
    }


    - (BOOL)isReady {
        return self.state == SROperationReadyState && [super isReady];
    }

    - (BOOL)isExecuting {
    return self.state == SROperationExecutingState;
    }

    - (BOOL)isFinished {
        return self.state == SROperationFinishedState;
    }

    - (void)start {
        [self.lock lock];
        if ([self isCancelled]) {
            [self cancelConnection];
        } else if ([self isReady]) {
            self.state = SROperationExecutingState;
            [self operationDidStart];

        }
        [self.lock unlock];
    }

    - (void)operationDidStart {
        [self.lock lock];
        if (![self isCancelled]) {
            // YOUR ACTUAL OPERATION CODE

            // finish
            self.state = SROperationFinishedState;
            dispatch_async(dispatch_get_main_queue(), ^{
                 [[NSNotificationCenter defaultCenter] postNotificationName:SROperationDidFinishNotification object:nil];
            });
        }
        [self.lock unlock];
    }


     - (void)cancel {
        [self.lock lock];
        if (![self isFinished] && ![self isCancelled]) {
            [super cancel];

            if ([self isExecuting]) {

            }
        }
        [self.lock unlock];
    }


    #pragma mark - NSObject

    - (NSString *)description {
        [self.lock lock];
        NSString *description = [NSString stringWithFormat:@"<%@: %p, state: %@, cancelled: %@>", NSStringFromClass([self class]), self, SRKeyPathFromSROperationState(self.state), ([self isCancelled] ? @"YES" : @"NO")];
        [self.lock unlock];
        return description;
    }


    #pragma mark - NSCopying

    - (id)copyWithZone:(NSZone *)zone {
        SROperation *operation = [(SROperation *)[[self class] allocWithZone:zone] init];
        // copy more properties

        return operation;
    }

}

您可以构建一个执行重试的网络操作,并且仅在调用网络完成块时设置isFinished,并且确定不需要重试:

class NetworkOperationWithRetry: AsynchronousOperation {
    var session: NSURLSession
    var request: NSURLRequest
    var task: NSURLSessionTask?
    var networkCompletionHandler: ((NSData?, NSURLResponse?, NSError?) -> ())?

    init(session: NSURLSession = NSURLSession.sharedSession(), request: NSURLRequest, networkCompletionHandler: (NSData?, NSURLResponse?, NSError?) -> ()) {
        self.session = session
        self.request = request
        self.networkCompletionHandler = networkCompletionHandler
    }

    override func main() {
        attemptRequest()
    }

    func attemptRequest() {
        print("attempting \(request.URL!.lastPathComponent)")

        task = session.dataTaskWithRequest(request) { data, response, error in
            if error?.domain == NSURLErrorDomain && error?.code == NSURLErrorNotConnectedToInternet {
                print("will retry \(self.request.URL!.lastPathComponent)")
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 * Int64(NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)) {
                    self.attemptRequest()
                }
                return
            }

            print("finished \(self.request.URL!.lastPathComponent)")

            self.networkCompletionHandler?(data, response, error)
            self.networkCompletionHandler = nil
            self.completeOperation()
        }
        task?.resume()
    }

    override func cancel() {
        task?.cancel()
        super.cancel()
    }
}

你可以这样称呼它:

let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 1     // I wouldn't generally do this, but just to illustrate that it's honoring operation queue dependencies/concurrency settings

let requests = urlStrings.map { NSURLRequest(URL: NSURL(string: [=11=])!) }
requests.forEach { request in
    queue.addOperation(NetworkOperationWithRetry(request: request) { data, response, error in
        // do something with the `data`, `response`, and `error`
    })
}

现在,这只是在 NSURLErrorNotConnectedToInternet 上重试,但您可以根据需要更改该逻辑。同样,我可能倾向于添加一些 "max retries" 逻辑。此外,如果缺少 Internet 连接确实是问题所在,而不是在实现 Internet 连接之前重试,我可能倾向于将操作的 isReady 通知设置为可达性。

顺便说一句,上面使用了下面的AsynchronousOperation

/// Asynchronous Operation base class
///
/// This class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
///   necessary and then ensuring that `completeOperation()` is called; or
///   override `cancel` method, calling `super.cancel()` and then cleaning-up
///   and ensuring `completeOperation()` is called.

public class AsynchronousOperation : NSOperation {

    override public var asynchronous: Bool { return true }

    private let stateLock = NSLock()

    private var _executing: Bool = false
    override private(set) public var executing: Bool {
        get {
            return stateLock.withCriticalScope { _executing }
        }
        set {
            willChangeValueForKey("isExecuting")
            stateLock.withCriticalScope { _executing = newValue }
            didChangeValueForKey("isExecuting")
        }
    }

    private var _finished: Bool = false
    override private(set) public var finished: Bool {
        get {
            return stateLock.withCriticalScope { _finished }
        }
        set {
            willChangeValueForKey("isFinished")
            stateLock.withCriticalScope { _finished = newValue }
            didChangeValueForKey("isFinished")
        }
    }

    /// Complete the operation
    ///
    /// This will result in the appropriate KVN of isFinished and isExecuting

    public func completeOperation() {
        if executing {
            executing = false
        }

        if !finished {
            finished = true
        }
    }

    override public func start() {
        if cancelled {
            finished = true
            return
        }

        executing = true

        main()
    }

    override public func main() {
        fatalError("subclasses must override `main`")
    }
}

extension NSLock {

    /// Perform closure within lock.
    ///
    /// An extension to `NSLock` to simplify executing critical code.
    ///
    /// - parameter block: The closure to be performed.

    func withCriticalScope<T>(@noescape block: Void -> T) -> T {
        lock()
        let value = block()
        unlock()
        return value
    }
}