【问题标题】:Subclassing NSOperation to internet operations with retry通过重试将 NSOperation 子类化为 Internet 操作
【发布时间】:2014-03-02 12:17:59
【问题描述】:

我在后台线程中为 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

【问题讨论】:

  • 我建议你不要重新发明圈子,看看AFNetworking。它能够处理您将要实施的网络相关的所有问题,等等。

标签: ios objective-c multithreading nsoperation


【解决方案1】:

几个反应:

  1. 在处理重试逻辑之前,您可能应该将调用 [self completeOperation] 移动到 inside 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. 就我个人而言,我对整个努力持谨慎态度。令我震惊的是,您应该根据错误类型使用逻辑。值得注意的是,如果错误是由于缺少 Internet 连接而导致的故障,您应该使用Reachability 来确定连接并响应通知以在连接恢复时自动重试,而不是简单地以规定的重试间隔的数学进展重试。

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

一些不相关的观察:

    1234563这个操作到后台队列,无论如何)。
  1. 我还删除了try/catch 逻辑,因为与其他语言/平台不同,异常处理不是处理运行时错误的首选方法。 Cocoa 中的运行时错误通常通过NSError 处理。在 Cocoa 中,异常通常仅用于处理程序员错误,而不是处理用户可能遇到的运行时错误。请参阅使用 Objective-C 编程指南中的 Apple 讨论 Dealing with Errors

  2. 如果您在各自的声明期间为您的属性定义适当的 getter 方法,则可以摆脱手动实现的 isExecutingisFinished getter 方法:

    @property (nonatomic, readwrite, getter=isExecuting) BOOL executing;
    @property (nonatomic, readwrite, getter=isFinished)  BOOL finished;
    
  3. 不过,您可能想编写自己的 setExecutingsetFinished 设置器方法,如果您愿意,它们会为您发出通知,例如:

    @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 时,它会为您发出通知,您可以删除散落在代码中的 willChangeValueForKeydidChangeValueForKey

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

    我觉得isCompletedisFinished 是多余的。看来您可以完全消除 completed 属性和 isCompleted 方法。

  5. 通过同时支持NSURLSessionNSURLConnection,您可能会不必要地复制大量网络代码。如果你真的愿意,你可以这样做,但他们向我们保证 NSURLConnection 仍然受支持,所以我觉得这是不必要的(除非你想享受一些适用于 iOS 7+ 设备的 NSURLSession 特定功能,而你不是正在做)。做任何你想做的事,但就个人而言,我使用NSURLConnection 需要支持早期的iOS 版本,而NSURLSession 我不需要,但除非有一些引人注目的业务,否则我不会倾向于同时实现这两者要求这样做。

【讨论】:

  • "你可以摆脱你手动实现的 isExecuting 和 isFinished。"实际上,没有。综合属性访问器对于并发 NSOperations 来说是不够的。
  • @GwendalRoué 也许你可以澄清为什么你认为自动合成的吸气剂是不够的。如果您的属性声明在适当的情况下指定了getter=isExecutinggetter=isFinished,那么getter 具有正确的名称,并且一切正常。多年来,我一直广泛使用这种模式,没有发生任何事故。我从来没有为我的并发操作手动编写过 getter。也许您可以解释为什么您认为合成的吸气剂“不够”。
  • 当然。仅当 NSOperationQueue 中有多个操作并且这些操作相互依赖时,该问题才可见。例如,一个是另一个的依赖项。或者队列不会同时运行超过 N 个操作(某些操作必须等待其他操作完成才能开始)。合成访问器不会通知 NSOperationQueue 操作状态。并且操作的调度不起作用:依赖没有准备好,永远不会启动,队列不会变空等等。
  • 哦,我明白你的意思了。我的回答可能被误解为暗示您不需要执行will/didChangeValueForKey。不,你是对的,你需要那些。我的意思只是你不必编写手动吸气剂。我试图澄清我的答案。
  • 合成吸气剂工作正常。您的答案是发送“isExecuting”和“isFinished”通知,这是不正确的关键路径。 “执行”和“完成”是正确的,并发送自动通知 - 允许依赖项等正常工作。
【解决方案2】:

你的方法:

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”是这些属性的获取(读取)访问器的名称。

对于NSOperation 子类,KVO 通知应自动发送,除非您的班级通过实现+automaticallyNotifiesObserversForKey+automaticallyNotifiesObserversOf&lt;Key&gt; 选择退出自动 KVO 通知以返回 NO。 您可以在 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 在公共接口中将它们声明为只读,您可以选择在私有类扩展中将它们设为 readwrite

就使用重试逻辑实现连接而言,有一个很好的 Apple 示例代码项目可以证明这一点,MVCNetworking

【讨论】:

  • 如上所述,“NSOperationQueue 观察到这些状态的关键路径 'finished' 和 'executing' 是不正确的”。文档非常清楚,KVN 名称是isFinishedisExecuting。我认为这是一个糟糕的设计,但它就是这样工作的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-04-21
  • 2014-11-23
  • 1970-01-01
  • 1970-01-01
  • 2022-12-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多