web-dev-qa-db-ja.com

AFNetworkingとバックグラウンド転送

新しいiOS 7 NSURLSessionバックグラウンド転送機能と AFNetworking (バージョン2および3)を利用する方法について少し混乱しています。

私が見ました WWDC 705 - What’s New in Foundation Networkingセッション、そして彼らはアプリの終了後またはクラッシュした後も続くバックグラウンドダウンロードを示しました。

これは、新しいAPI application:handleEventsForBackgroundURLSession:completionHandler:およびセッションのデリゲートが最終的にコールバックを取得し、そのタスクを完了することができるという事実。

だから私はAFNetworkingでそれを使用して(可能であれば)バックグラウンドでダウンロードを続ける方法を疑問に思っています。

問題は、AFNetworkingは便利なことにブロックベースのAPIを使用してすべてのリクエストを実行しますが、アプリが終了またはクラッシュした場合、それらのブロックもなくなることです。それでは、どうすればタスクを完了できますか?

または多分私はここで何かを見逃しています...

意味を説明させてください。

たとえば、私のアプリはフォトメッセージングアプリです。1つのメッセージを表すPhotoMessageオブジェクトがあり、このオブジェクトには次のようなプロパティがあるとします。

  • state-写真のダウンロードの状態を説明します。
  • resourcePath-最終的にダウンロードされた写真ファイルへのパス。

そのため、サーバーから新しいメッセージを受け取ると、新しいPhotoMessageオブジェクトを作成し、その写真リソースのダウンロードを開始します。

PhotoMessage *newPhotoMsg = [[PhotoMessage alloc] initWithInfoFromServer:info];
newPhotoMsg.state = kStateDownloading;

self.photoDownloadTask = [[BGSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
    NSURL *filePath = // some file url
    return filePath;
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
    if (!error) {
        // update the PhotoMessage Object
        newPhotoMsg.state = kStateDownloadFinished;
        newPhotoMsg.resourcePath = filePath;
    }
}];

[self.photoDownloadTask resume];   

ご覧のとおり、完了ブロックを使用して、取得した応答に応じてそのPhotoMessageオブジェクトを更新します。

バックグラウンド転送でこれを達成するにはどうすればよいですか?この完了ブロックは呼び出されないため、newPhotoMsgを更新できません。

52
Mario

いくつかの考え:

  1. URL Loading System Programming GuideiOSバックグラウンドアクティビティの処理 セクションで説明されている必要なコーディングを必ず行う必要があります:

    IOSでNSURLSessionを使用している場合、ダウンロードが完了するとアプリが自動的に再起動されます。アプリのapplication:handleEventsForBackgroundURLSession:completionHandler: appデリゲートメソッドは、適切なセッションを再作成し、完了ハンドラーを保存し、セッションがセッションデリゲートのURLSessionDidFinishEventsForBackgroundURLSession: 方法。

    そのガイドでは、できることの例をいくつか示しています。率直に言って、WWDC 2013ビデオの後半で説明されているコードサンプル Foundation Networkingの新機能 はさらに明確だと思います。

  2. AFURLSessionManagerの基本的な実装は、アプリが単に中断されている場合、バックグラウンドセッションと連携して機能します(上記を実行したと仮定すると、ネットワークタスクが完了するとブロックが呼び出されます)。しかし、ご想像のとおり、アップロードおよびダウンロード用にAFURLSessionManagerを作成するNSURLSessionTaskメソッドに渡されるタスク固有のブロックパラメーターは、「アプリが終了またはクラッシュした場合」に失われます。

    バックグラウンドアップロードの場合、これは面倒です(タスクの作成時に指定したタスクレベルの情報の進行と完了のブロックが呼び出されないため)。ただし、セッションレベルのレンディション(たとえば、setTaskDidCompleteBlockおよびsetTaskDidSendBodyDataBlock)を使用する場合は、適切に呼び出されます(セッションマネージャーを再インスタンス化するときにこれらのブロックを常に設定すると仮定します)。

    判明したように、ブロックを失うというこの問題は、実際にはバックグラウンドでのダウンロードではより問題がありますが、ソリューションは非常に似ています(タスクベースのブロックパラメーターを使用せず、setDownloadTaskDidFinishDownloadingBlock)。

  3. 別の方法として、デフォルト(バックグラウンドではない)NSURLSessionを使用することもできますが、タスクの進行中にユーザーがアプリを離れると、アプリがアップロードを完了するために少し時間を要求することを確認します。たとえば、NSURLSessionTaskを作成する前に、UIBackgroundTaskIdentifierを作成できます。

    UIBackgroundTaskIdentifier __block taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) {
        // handle timeout gracefully if you can
    
        [[UIApplication sharedApplication] endBackgroundTask:taskId];
        taskId = UIBackgroundTaskInvalid;
    }];
    

    ただし、ネットワークタスクの完了ブロックがiOSに完了したことを正しく通知していることを確認してください。

    if (taskId != UIBackgroundTaskInvalid) {
        [[UIApplication sharedApplication] endBackgroundTask:taskId];
        taskId = UIBackgroundTaskInvalid;
    }
    

    これはバックグラウンドNSURLSessionほど強力ではありません(たとえば、利用可能な時間が限られています)が、これは役に立つ場合があります。


更新:

AFNetworkingを使用してバックグラウンドダウンロードを行う方法の実用的な例を追加すると思いました。

  1. 最初にバックグラウンドマネージャを定義します。

    //
    //  BackgroundSessionManager.h
    //
    //  Created by Robert Ryan on 10/11/14.
    //  Copyright (c) 2014 Robert Ryan. All rights reserved.
    //
    
    #import "AFHTTPSessionManager.h"
    
    @interface BackgroundSessionManager : AFHTTPSessionManager
    
    + (instancetype)sharedManager;
    
    @property (nonatomic, copy) void (^savedCompletionHandler)(void);
    
    @end
    

    そして

    //
    //  BackgroundSessionManager.m
    //
    //  Created by Robert Ryan on 10/11/14.
    //  Copyright (c) 2014 Robert Ryan. All rights reserved.
    //
    
    #import "BackgroundSessionManager.h"
    
    static NSString * const kBackgroundSessionIdentifier = @"com.domain.backgroundsession";
    
    @implementation BackgroundSessionManager
    
    + (instancetype)sharedManager {
        static id sharedMyManager = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedMyManager = [[self alloc] init];
        });
        return sharedMyManager;
    }
    
    - (instancetype)init {
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kBackgroundSessionIdentifier];
        self = [super initWithSessionConfiguration:configuration];
        if (self) {
            [self configureDownloadFinished];            // when download done, save file
            [self configureBackgroundSessionFinished];   // when entire background session done, call completion handler
            [self configureAuthentication];              // my server uses authentication, so let's handle that; if you don't use authentication challenges, you can remove this
        }
        return self;
    }
    
    - (void)configureDownloadFinished {
        // just save the downloaded file to documents folder using filename from URL
    
        [self setDownloadTaskDidFinishDownloadingBlock:^NSURL *(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location) {
            if ([downloadTask.response isKindOfClass:[NSHTTPURLResponse class]]) {
                NSInteger statusCode = [(NSHTTPURLResponse *)downloadTask.response statusCode];
                if (statusCode != 200) {
                    // handle error here, e.g.
    
                    NSLog(@"%@ failed (statusCode = %ld)", [downloadTask.originalRequest.URL lastPathComponent], statusCode);
                    return nil;
                }
            }
    
            NSString *filename      = [downloadTask.originalRequest.URL lastPathComponent];
            NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
            NSString *path          = [documentsPath stringByAppendingPathComponent:filename];
            return [NSURL fileURLWithPath:path];
        }];
    
        [self setTaskDidCompleteBlock:^(NSURLSession *session, NSURLSessionTask *task, NSError *error) {
            if (error) {
                // handle error here, e.g.,
    
                NSLog(@"%@: %@", [task.originalRequest.URL lastPathComponent], error);
            }
        }];
    }
    
    - (void)configureBackgroundSessionFinished {
        typeof(self) __weak weakSelf = self;
    
        [self setDidFinishEventsForBackgroundURLSessionBlock:^(NSURLSession *session) {
            if (weakSelf.savedCompletionHandler) {
                weakSelf.savedCompletionHandler();
                weakSelf.savedCompletionHandler = nil;
            }
        }];
    }
    
    - (void)configureAuthentication {
        NSURLCredential *myCredential = [NSURLCredential credentialWithUser:@"userid" password:@"password" persistence:NSURLCredentialPersistenceForSession];
    
        [self setTaskDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *credential) {
            if (challenge.previousFailureCount == 0) {
                *credential = myCredential;
                return NSURLSessionAuthChallengeUseCredential;
            } else {
                return NSURLSessionAuthChallengePerformDefaultHandling;
            }
        }];
    }
    
    @end
    
  2. アプリのデリゲートが完了ハンドラーを保存することを確認します(必要に応じてバックグラウンドセッションをインスタンス化します)。

    - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
        NSAssert([[BackgroundSessionManager sharedManager].session.configuration.identifier isEqualToString:identifier], @"Identifiers didn't match");
        [BackgroundSessionManager sharedManager].savedCompletionHandler = completionHandler;
    }
    
  3. 次に、ダウンロードを開始します。

    for (NSString *filename in filenames) {
        NSURL *url = [baseURL URLByAppendingPathComponent:filename];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        [[[BackgroundSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:nil completionHandler:nil] resume];
    }
    

    バックグラウンドセッションでは信頼性がないため、これらのタスク関連のブロックは提供していません。 (バックグラウンドダウンロードは、アプリが終了し、これらのブロックが長く消えた後でも続行されます。)セッションレベルの簡単に再作成されるsetDownloadTaskDidFinishDownloadingBlockのみに依存する必要があります。

明らかにこれは単純な例です(バックグラウンドセッションオブジェクトが1つのみ、URLの最後のコンポーネントをファイル名として使用してファイルをドキュメントフォルダーに保存するなど)。しかし、うまくいけばパターンを示しています。

77
Rob

コールバックがブロックであるかどうかにかかわらず、違いはありません。 AFURLSessionManagerをインスタンス化するときは、必ず_NSURLSessionConfiguration backgroundSessionConfiguration:_でインスタンス化してください。また、コールバックブロックを使用して、マネージャーのsetDidFinishEventsForBackgroundURLSessionBlockを呼び出してください。これは、NSURLSessionDelegateのメソッドURLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)sessionで通常定義されるコードを記述する場所です。このコードは、アプリデリゲートのバックグラウンドダウンロード完了ハンドラーを呼び出す必要があります。

バックグラウンドダウンロードタスクに関する1つのアドバイス-フォアグラウンドで実行している場合でも、タイムアウトは無視されます。つまり、応答しないダウンロードで「スタック」する可能性があります。これはどこにも文書化されておらず、しばらくの間私を夢中にさせました。最初の容疑者はAFNetworkingでしたが、NSURLSessionを直接呼び出した後でも、動作は同じままでした。

がんばろう!

2
Stavash