web-dev-qa-db-ja.com

NSURLSessionTaskのキューを作成するためのベストプラクティス

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

           }
 ];

別の補完内に補完ブロックを書き込むのが面倒に見えるという事実は別として、[fileDownloadTask resume]...を呼び出すと、EXC_BAD_ACCESSエラーが発生します。fileDownloadTaskはnilではありません!

それで、NSURLSessionTasksを配列する最良の方法は何ですか?

23
Eric

最も簡単なこのアプローチを使用する必要があります: https://stackoverflow.com/a/31386206/2308258

または、操作キューを使用して、タスクを相互に依存させる

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

HTTPMaximumConnectionsPerHostメソッドについて

NSURLSessionTasksの先入れ先出しシリアルキューを実装する簡単な方法は、HTTPMaximumConnectionsPerHostプロパティが1に設定されているNSURLSessionですべてのタスクを実行することです。

HTTPMaximumConnectionsPerHostは、1つのshared接続がそのセッションのタスクに使用されることを保証するだけですが、それらが順次処理されることを意味しません。

http://www.charlesproxy.com/ を使用してネットワークレベルで確認できます。HTTPMaximumConnectionsPerHostを設定すると、NSURLSessionによって同時にタスクが同時に開始され、信じられているように連続していない。

実験1:

  • 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に宣言する
  • task1:url = download.thinkbroadband.com/20MB.Zip
  • task2:url = download.thinkbroadband.com/10MB.Zip(小さいファイル

    1. [task1再開]を呼び出す;
    2. [task2再開]を呼び出す;

結果:task2 completionBlockが呼び出され、次にtask1 completionBlockが呼び出されます

結論として、注文を自分で行う必要があります。NSURLSessionには、リクエストの注文に関するロジックがありません。ホストごとの最大接続数を1に設定した場合でも、最初に終了するものの完了ブロックを呼び出すだけです。

PS:申し訳ありませんが、スクリーンショットを投稿するのに十分な評判がありません。

18
matanwrites

編集:

Mataejoonが指摘したように、HTTPMaximumConnectionsPerHostを1に設定しても、接続がシリアルに処理されることは保証されません。 NSURLSessionTaskの信頼できるシリアルキューが必要な場合は、別のアプローチを試してください(元の回答は次のとおりです)。


NSURLSessionTasksの先入れ先出しシリアルキューを実装する簡単な方法は、 NSURLSession があるHTTPMaximumConnectionsPerHostですべてのタスクを実行することですプロパティを1に設定:

+ (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) {
13
Eric
#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];
1
Peter Lapisu

私はNSOperationQueueを使用しています(Owenが示唆しています)。 NSURLSessionTasksをNSOperationサブクラスに配置し、依存関係を設定します。依存するタスクは、実行する前に、依存するタスクが完了するまで待機しますが、ステータス(成功または失敗)をチェックしないため、プロセスを制御するロジックを追加します。

私の場合、最初のタスクはユーザーが有効なアカウントを持っているかどうかをチェックし、必要に応じて作成します。最初のタスクでは、NSUserDefault値を更新して、アカウントが有効(またはエラー)であることを示します。 2番目のタスクはNSUserDefault値をチェックし、すべてのOKがユーザー資格情報を使用してサーバーにデータを投稿するかどうかを確認します。

(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");
    }
}
0
Peter Todd