24

制作串行队列的最佳实践是NSURLSessionTasks什么?

就我而言,我需要:

  1. 获取 JSON 文件中的 URL ( NSURLSessionDataTask)
  2. 在该 URL ( NSURLSessionDownloadTask)下载文件

这是我到目前为止所拥有的:

session = [NSURLSession sharedSession];

//Download the JSON:
NSURLRequest *dataRequest = [NSURLRequest requestWithURL:url];

NSURLSessionDataTask *task =
[session dataTaskWithRequest:dataRequest
           completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

               //Figure out the URL of the file I want to download:
               NSJSONSerialization *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
               NSURL *downloadURL = [NSURL urlWithString:[json objectForKey:@"download_url"]];

               NSURLSessionDownloadTask *fileDownloadTask =
               [session downloadTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:playlistURL]]
                              completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
                                  NSLog(@"completed!");
                              }];

               [fileDownloadTask resume];

           }
 ];

除了在另一个完成中编写完成块看起来很混乱之外,当我调用时,我得到一个 EXC_BAD_ACCESS 错误[fileDownloadTask resume]......即使fileDownloadTask不是零!

那么,最好的排序方式是NSURLSessionTasks什么?

4

4 回答 4

18

您需要使用这种最直接的方法:https ://stackoverflow.com/a/31386206/2308258

或者使用操作队列并使任务相互依赖

==================================================== ======================

关于 HTTPMaximumConnectionsPerHost 方法

实现 NSURLSessionTasks 的先进先出串行队列的一种简单方法是在其 HTTPMaximumConnectionsPerHost 属性设置为 1 的 NSURLSession 上运行所有任务

HTTPMaximumConnectionsPerHost 仅确保一个共享连接将用于该会话的任务,但这并不意味着它们将被串行处理。

您可以使用http://www.charlesproxy.com/在网络级别上验证,您会发现在设置 HTTPMaximumConnectionsPerHost 时,您的任务仍将由 NSURLSession 同时启动,而不是像人们认为的那样串行启动。

实验一:

  • 用 HTTPMaximumConnectionsPerHost 声明一个 NSURLSession 为 1
  • 使用task1:url = download.thinkbroadband.com/20MB.zip
  • 使用task2:url = download.thinkbroadband.com/20MB.zip

    1. 调用 [task1 恢复];
    2. 调用 [task2 恢复];

结果:调用 task1 completionBlock 然后调用 task2 completionBlock

完成块可能会按照您期望的顺序调用,以防任务花费相同的时间但是如果您尝试使用相同的 NSURLSession 下载两个不同的东西,您会发现 NSURLSession 没有任何底层调用顺序,但只有完成先完成的任何事情。

实验 2:

  • 用 HTTPMaximumConnectionsPerHost 声明一个 NSURLSession 为 1
  • 任务1:url = download.thinkbroadband.com/20MB.zip
  • task2: url = download.thinkbroadband.com/10MB.zip(小文件

    1. 调用 [task1 恢复];
    2. 调用 [task2 恢复];

结果:调用 task2 completionBlock 然后调用 task1 completionBlock

总之,您需要自己进行排序,NSURLSession 没有任何关于排序请求的逻辑,即使将每个主机的最大连接数设置为 1,它也只会调用先完成的完成块

PS:抱歉帖子的格式我没有足够的声誉来发布截图。

于 2015-09-18T15:20:11.317 回答
13

编辑:

正如 mataejoon 所指出的,设置HTTPMaximumConnectionsPerHost为 1 并不能保证连接是按顺序处理的。如果您需要一个可靠的NSURLSessionTask.


实现先进先出串行队列的一种简单方法是在属性设置为的 a上NSURLSessionTasks运行所有任务:NSURLSessionHTTPMaximumConnectionsPerHost1

+ (NSURLSession *)session
{
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

        [configuration setHTTPMaximumConnectionsPerHost:1];

        session = [NSURLSession sessionWithConfiguration:configuration];

    });
    return session;
}

然后按您想要的顺序向其中添加任务。

NSURLSessionDataTask *sizeTask = 
[[[self class] session] dataTaskWithURL:url 
                          completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
于 2014-01-09T11:29:36.770 回答
1
#import "SessionTaskQueue.h"

@interface SessionTaskQueue ()

@property (nonatomic, strong) NSMutableArray * sessionTasks;
@property (nonatomic, strong) NSURLSessionTask * currentTask;

@end

@implementation SessionTaskQueue

- (instancetype)init {

    self = [super init];
    if (self) {

        self.sessionTasks = [[NSMutableArray alloc] initWithCapacity:15];

    }
    return self;

}

- (void)addSessionTask:(NSURLSessionTask *)sessionTask {

    [self.sessionTasks addObject:sessionTask];
    [self resume];

}

// call in the completion block of the sessionTask
- (void)sessionTaskFinished:(NSURLSessionTask *)sessionTask {

    self.currentTask = nil;
    [self resume];

}

- (void)resume {

    if (self.currentTask) {
        return;
    }

    self.currentTask = [self.sessionTasks firstObject];
    if (self.currentTask) {
        [self.sessionTasks removeObjectAtIndex:0];
        [self.currentTask resume];
    }

}

@end

并像这样使用

    __block __weak NSURLSessionTask * wsessionTask;
    use_wself();

    wsessionTask = [[CommonServices shared] doSomeStuffWithCompletion:^(NSError * _Nullable error) {
        use_sself();

        [self.sessionTaskQueue sessionTaskFinished:wsessionTask];

        ...

    }];

    [self.sessionTaskQueue addSessionTask:wsessionTask];
于 2017-06-08T16:47:16.167 回答
0

我使用 NSOperationQueue (正如欧文所建议的那样)。将 NSURLSessionTasks 放在 NSOperation 子类中并设置任何依赖项。依赖任务会等到它们所依赖的任务完成后再运行,但不会检查状态(成功或失败),因此添加一些逻辑来控制流程。

在我的例子中,第一个任务检查用户是否有一个有效的帐户,并在必要时创建一个。在第一个任务中,我更新了一个 NSUserDefault 值以指示该帐户有效(或存在错误)。第二个任务检查 NSUserDefault 值,如果一切正常,则使用用户凭据将一些数据发布到服务器。

(将 NSURLSessionTasks 粘贴在单独的 NSOperation 子类中也使我的代码更易于导航)

将 NSOperation 子类添加到 NSOperationQueue 并设置任何依赖项:

 NSOperationQueue  *ftfQueue = [NSOperationQueue new];
 FTFCreateAccount *createFTFAccount = [[FTFCreateAccount alloc]init];
 [createFTFAccount setUserid:@"********"];  // Userid to be checked / created
 [ftfQueue addOperation:createFTFAccount];
 FTFPostRoute *postFTFRoute = [[FTFPostRoute alloc]init];
 [postFTFRoute addDependency:createFTFAccount];
 [ftfQueue addOperation:postFTFRoute];

在第一个 NSOperation 子类中,检查服务器上是否存在帐户:

@implementation FTFCreateAccount
{
    NSString *_accountCreationStatus;
}



- (void)main {

    NSDate *startDate = [[NSDate alloc] init];
    float timeElapsed;

    NSString *ftfAccountStatusKey    = @"ftfAccountStatus";
    NSString *ftfAccountStatus    = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setValue:@"CHECKING" forKey:ftfAccountStatusKey];

    // Setup and Run the NSURLSessionTask
    [self createFTFAccount:[self userid]];

    // Hold it here until the SessionTask completion handler updates the _accountCreationStatus
    // Or the process takes too long (possible connection error)

    while ((!_accountCreationStatus) && (timeElapsed < 5.0)) {
        NSDate *currentDate = [[NSDate alloc] init];     
        timeElapsed = [currentDate timeIntervalSinceDate:startDate];    
    }
    if ([_accountCreationStatus isEqualToString:@"CONNECTION PROBLEM"] || !_accountCreationStatus) [self cancel];

    if ([self isCancelled]) {
        NSLog(@"DEBUG FTFCreateAccount Cancelled" );
        NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
        [userDefaults setValue:@"ERROR" forKey:ftfAccountStatusKey];            
    }    
}

在下一个 NSOperation 发布数据中:

@implementation FTFPostRoute
{
    NSString *_routePostStatus;
}

- (void)main {

    NSDate *startDate = [[NSDate alloc] init];
    float timeElapsed;
    NSString *ftfAccountStatusKey    = @"ftfAccountStatus";
    NSString *ftfAccountStatus    = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];

    if ([ftfAccountStatus isEqualToString:@"ERROR"])
    {
        // There was a ERROR in creating / accessing the user account.  Cancel the post
        [self cancel];
    } else
    {
        // Call method to setup and run json post

        // Hold it here until a reply comes back from the operation
        while ((!_routePostStatus) && (timeElapsed < 3)) {
            NSDate *currentDate = [[NSDate alloc] init];          
            timeElapsed = [currentDate timeIntervalSinceDate:startDate];                
            NSLog(@"FTFPostRoute time elapsed: %f", timeElapsed);                
        }

    }


    if ([self isCancelled]) {
        NSLog(@"FTFPostRoute operation cancelled");
    }
}
于 2013-12-30T12:09:40.383 回答