问题
I'm subclassing NSOperation for http post in background thread. Those specific http posts doesn't require any value to return.
What I'm trying to do is when I've an error or timeout I want it to send after an increasing delay (fibonacci).
So far I've done this:
NSInternetOperation.h:
#import <Foundation/Foundation.h>
@interface NSInternetOperation : NSOperation
@property (nonatomic) BOOL executing;
@property (nonatomic) BOOL finished;
@property (nonatomic) BOOL completed;
@property (nonatomic) BOOL cancelled;
- (id)initWebServiceName:(NSString*)webServiceName andPerameters:(NSString*)parameters;
- (void)start;
@end
NSInternetOperation.m:
#import "NSInternetOperation.h"
static NSString * const kFinishedKey = @"isFinished";
static NSString * const kExecutingKey = @"isExecuting";
@interface NSInternetOperation ()
@property (strong, nonatomic) NSString *serviceName;
@property (strong, nonatomic) NSString *params;
- (void)completeOperation;
@end
@implementation NSInternetOperation
- (id)initWebServiceName:(NSString*)webServiceName andPerameters:(NSString*)parameters
{
self = [super init];
if (self) {
_serviceName = webServiceName;
_params = parameters;
_executing = NO;
_finished = NO;
_completed = NO;
}
return self;
}
- (BOOL)isExecuting { return self.executing; }
- (BOOL)isFinished { return self.finished; }
- (BOOL)isCompleted { return self.completed; }
- (BOOL)isCancelled { return self.cancelled; }
- (BOOL)isConcurrent { return YES; }
- (void)start
{
if ([self isCancelled]) {
[self willChangeValueForKey:kFinishedKey];
self.finished = YES;
[self didChangeValueForKey:kFinishedKey];
return;
}
// If the operation is not cancelled, begin executing the task
[self willChangeValueForKey:kExecutingKey];
self.executing = YES;
[self didChangeValueForKey:kExecutingKey];
[self main];
}
- (void)main
{
@try {
//
// Here we add our asynchronized code
//
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL *completeURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", kWEB_SERVICE_URL, self.serviceName]];
NSData *body = [self.params dataUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:completeURL];
[request setHTTPMethod:@"POST"];
[request setValue:kAPP_PASSWORD_VALUE forHTTPHeaderField:kAPP_PASSWORD_HEADER];
[request setHTTPBody:body];
[request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)body.length] forHTTPHeaderField:@"Content-Length"];
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
if (__iOS_7_AND_HIGHER)
{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:[Netroads sharedInstance] delegateQueue:[NSOperationQueue new]];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error)
{
NSLog(@"%@ Error: %@", self.serviceName, error.localizedDescription);
}
else
{
//NSString *responseXML = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
//NSLog(@"\n\nResponseXML(%@):\n%@", webServiceName, responseXML);
}
}];
[dataTask resume];
}
else
{
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue new] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError)
{
NSLog(@"%@ Error: %@", self.serviceName, connectionError.localizedDescription);
}
else
{
//NSString *responseXML = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
//NSLog(@"\n\nResponseXML(%@):\n%@", webServiceName, responseXML);
}
}];
}
});
[self completeOperation];
}
@catch (NSException *exception) {
NSLog(@"%s exception.reason: %@", __PRETTY_FUNCTION__, exception.reason);
[self completeOperation];
}
}
- (void)completeOperation
{
[self willChangeValueForKey:kFinishedKey];
[self willChangeValueForKey:kExecutingKey];
self.executing = NO;
self.finished = YES;
[self didChangeValueForKey:kExecutingKey];
[self didChangeValueForKey:kFinishedKey];
}
@end
回答1:
A couple of reactions:
Before you tackle the retry logic, you should probably move your call to
[self completeOperation]to inside the completion block of theNSURLSessionDataTaskorsendAsynchronousRequest. Your current operation class is completing prematurely (and therefore would not honor dependencies and your network operation queue's intendedmaxConcurrentOperationCount).The retry logic seems unremarkable. Perhaps something like:
- (void)main { NSURLRequest *request = [self createRequest]; // maybe move the request creation stuff into its own method [self tryRequest:request currentDelay:1.0]; } - (void)tryRequest:(NSURLRequest *)request currentDelay:(NSTimeInterval)delay { [NSURLConnection sendAsynchronousRequest:request queue:[self networkOperationCompletionQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { BOOL success = NO; if (connectionError) { NSLog(@"%@ Error: %@", self.serviceName, connectionError.localizedDescription); } else { if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode]; if (statusCode == 200) { // parse XML response here; if successful, set `success` to `YES` } } } if (success) { [self completeOperation]; } else { dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){ NSTimeInterval nextDelay = [self nextDelayFromCurrentDelay:delay]; [self tryRequest:request currentDelay:nextDelay]; }); } }]; }Personally, I'm wary about this entire endeavor. It strikes me that you should be employing logic conditional upon the type of error. Notably, if the error is a failure resulting from lacking of internet connectivity, you should use Reachability to determine connectivity and respond to notifications to retry automatically when connectivity is restored, not simply retrying at prescribed mathematical progression of retry intervals.
Other than network connectivity (which is better addressed with Reachability), I'm unclear as to what other network failures warrant a retry logic.
Some unrelated observations:
Note, I eliminated the
dispatch_asyncof the issuing of the request inmainto a background queue because you're using asynchronous methods already (and even if you weren't, you've presumably added this operation to a background queue, anyway).I've also removed the
try/catchlogic because, unlike other languages/platforms, exception handling is not the preferred method of handling runtime errors. Typically runtime errors in Cocoa are handled viaNSError. In Cocoa, exceptions are generally used solely to handle programmer errors, but not to handle the runtime errors that a user would encounter. See Apple's discussion Dealing with Errors in the Programming with Objective-C guide.You can get rid of your manually implemented
isExecutingandisFinishedgetter methods if you just define the appropriate getter method for your properties during their respective declarations:@property (nonatomic, readwrite, getter=isExecuting) BOOL executing; @property (nonatomic, readwrite, getter=isFinished) BOOL finished;You might, though, want to write your own
setExecutingandsetFinishedsetter methods, which do the notification for you, if you want, e.g.:@synthesize finished = _finished; @synthesize executing = _executing; - (void)setExecuting:(BOOL)executing { [self willChangeValueForKey:kExecutingKey]; _executing = executing; [self didChangeValueForKey:kExecutingKey]; } - (void)setFinished:(BOOL)finished { [self willChangeValueForKey:kFinishedKey]; _finished = finished; [self didChangeValueForKey:kFinishedKey]; }Then, when you use the setter it will do the notifications for you, and you can remove the
willChangeValueForKeyanddidChangeValueForKeythat you have scattered about your code.Also, I don't think you need to implement
isCancelledmethod (as that's already implemented for you). But you really should override acancelmethod which calls itssuperimplementation, but also cancels your network request and completes your operation. Or, instead of implementingcancelmethod, you could move to thedelegatebased rendition of the network requests but make sure you check for[self isCancelled]inside thedidReceiveDatamethod.And
isCompletedstrikes me as redundant withisFinished. It seems like you could entirely eliminatecompletedproperty andisCompletedmethod.You're probably unnecessarily duplicating the amount of network code by supporting both
NSURLSessionandNSURLConnection. You can do that if you really want, but they assure us thatNSURLConnectionis still supported, so it strikes me as unnecessary (unless you wanted to enjoy someNSURLSessionspecific features for iOS 7+ devices, which you're not currently doing). Do whatever you want, but personally, I'm usingNSURLConnectionwhere I need to support earlier iOS versions, andNSURLSessionwhere I don't, but I wouldn't be inclined to implement both unless there was some compelling business requirement to do so.
回答2:
Your method:
static NSString * const kFinishedKey = @"isFinished";
static NSString * const kExecutingKey = @"isExecuting";
- (void)completeOperation
{
[self willChangeValueForKey:kFinishedKey];
[self willChangeValueForKey:kExecutingKey];
self.executing = NO;
self.finished = YES;
[self didChangeValueForKey:kExecutingKey];
[self didChangeValueForKey:kFinishedKey];
}
Is manually sending notifications for the key paths "isFinished" and "isExecuting". NSOperationQueue observes the key paths "finished" and "executing" for those states - "isFinished" and "isExecuting" are the names of the get (read) accessors for those properties.
For an NSOperation subclass KVO notifications should be sent automatically unless your class has opted out of automatic KVO notifications by implementing +automaticallyNotifiesObserversForKey or +automaticallyNotifiesObserversOf<Key> to return NO.
You can see this demonstrated in a sample project here.
Your property declarations:
@property (nonatomic) BOOL executing;
@property (nonatomic) BOOL finished;
@property (nonatomic) BOOL cancelled;
Are overriding those in NSOperation without providing the correct get accessor. Change these to:
@property (nonatomic, getter=isExecuting) BOOL executing;
@property (nonatomic, getter=isFinished) BOOL finished;
@property (nonatomic, getter=isCancelled) BOOL cancelled;
To get the correct behavior for an NSOperation. NSOperation declares these as readonly in the public interface, you have the option of making them readwrite in a private class extension.
As far as implementing a connection with retry logic, there is an excellent Apple sample code project that demonstrates this, MVCNetworking
来源:https://stackoverflow.com/questions/22127427/subclassing-nsoperation-to-internet-operations-with-retry