通过重试将NSOperation子类化为互联网操作

时间:2014-03-02 12:17:59

标签: ios objective-c multithreading nsoperation

我正在为后台线程中的http post子类化NSOperation。 这些特定的http帖子不需要返回任何值。

我正在尝试做的是当我发生错误或超时时我希望它在增加延迟后发送(斐波那契)。

到目前为止,我已经这样做了:

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

2 个答案:

答案 0 :(得分:6)

一些反应:

  1. 在您解决重试逻辑之前,您应该将来电[self completeOperation]移至{em> {}}} NSURLSessionDataTasksendAsynchronousRequest的完成模块。您当前的操作类过早完成(因此不会尊重依赖项和您的网络操作队列的预期maxConcurrentOperationCount)。

  2. 重试逻辑似乎并不起眼。也许是这样的事情:

    - (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];
                });
            }
        }];
    }
    
  3. 就个人而言,我对这整个努力都很谨慎。令我感到震惊的是你应该根据错误的类型使用逻辑。值得注意的是,如果由于缺少互联网连接而导致错误失败,则应使用Reachability确定连接并响应通知,以便在恢复连接时自动重试,而不是仅按重复间隔的规定数学级别重试。 / p>

    除了网络连接(可通过Reachability更好地解决)之外,我还不清楚其他网络故障是否需要重试逻辑。

  4. 一些不相关的观察结果:

    1. 请注意,我已将dispatch_asyncmain请求的try消除到后台队列,因为您已经使用了异步方法(即使您不是,也是无论如何,大概都会将此操作添加到后台队列中。

    2. 我还删除了catch / NSError逻辑,因为与其他语言/平台不同,异常处理不是处理运行时错误的首选方法。通常,Cocoa中的运行时错误通过isExecuting处理。在Cocoa中,异常通常仅用于处理程序员错误,但不用于处理用户将遇到的运行时错误。请参阅使用Objective-C编程指南中的Apple讨论Dealing with Errors

    3. 如果您只是在各自的声明中为您的属性定义适当的getter方法,则可以删除手动实现的isFinished@property (nonatomic, readwrite, getter=isExecuting) BOOL executing; @property (nonatomic, readwrite, getter=isFinished) BOOL finished; getter方法:

      setExecuting
    4. 但是,您可能希望编写自己的setFinished@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]; } setter方法,如果您愿意,可以为您执行通知,例如:

      willChangeValueForKey

      然后,当您使用setter时,它会为您执行通知,并且您可以删除分散在代码中的didChangeValueForKeyisCancelled

    5. 另外,我认为您不需要实现cancel方法(因为已经为您实现了)。但是你真的应该覆盖调用其super实现的cancel方法,但也取消你的网络请求并完成你的操作。或者,您可以转移到基于delegate的网络请求再现,而不是实施[self isCancelled]方法,但请确保在didReceiveData方法中检查isCompleted

      isFinished completed使我isCompleted多余。您似乎可以完全取消NSURLSession属性和NSURLConnection方法。

    6. 您可能通过支持NSURLConnectionNSURLSession来不必要地重复网络代码的数量。如果你真的想要,你可以这样做,但是他们向我们保证NSURLConnection仍然受到支持,所以这对我来说是不必要的(除非你想享受iOS 7+设备的一些NSURLSession特定功能,你现在没有这样做。做任何你想做的事,但就个人而言,我正在使用{{1}}我需要支持早期的iOS版本,{{1}}我不支持,但我不会倾向于实现两者,除非那里这是一个令人信服的业务要求。

答案 1 :(得分:0)

你的方法:

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];
}

手动发送关键路径“isFinished”和“isExecuting”的通知。 NSOperationQueue观察那些状态的“完成”和“执行”的关键路径 - “isFinished”和“isExecuting”是这些属性的get(读取)访问器的名称。

These are not the key paths you were looking for

对于NSOperation子类KVO通知应自动发送,除非您的类通过实施+automaticallyNotifiesObserversForKey+automaticallyNotifiesObserversOf<Key>选择退出NO来选择退出自动KVO通知。 您可以在sample project here中看到这一点。

您的财产声明:

@property (nonatomic) BOOL executing;
@property (nonatomic) BOOL finished;
@property (nonatomic) BOOL cancelled;

在没有提供正确的get访问器的情况下覆盖NSOperation中的那些。将这些更改为:

@property (nonatomic, getter=isExecuting) BOOL executing;
@property (nonatomic, getter=isFinished) BOOL finished;
@property (nonatomic, getter=isCancelled) BOOL cancelled;

获取NSOperation的正确行为。 NSOperation在公共接口中将这些声明为readonly,您可以选择在私有类扩展中将它们设为readwrite

就实现与重试逻辑的连接而言,有一个很好的Apple示例代码项目,用于演示这一点,MVCNetworking