ios - AFNetworking 和后台传输

我有点困惑如何利用新的 iOS 7 NSURLSession 后台传输功能和 AFNetworking (版本 2 和 3)。

我看到了 WWDC 705 - What's New in Foundation Networking session ,他们演示了在应用程序终止甚至崩溃后继续进行后台下载。

这是使用新的 API application:handleEventsForBackgroundURLSession:completionHandler: 完成的,并且 session 的委托(delegate)最终将获得回调并可以完成其任务。

所以我想知道如何将它与 AFNetworking(如果可能)一起使用以继续在后台下载。

问题是,AFNetworking 方便地使用基于 block 的 API 来执行所有请求,但如果应用程序终止或崩溃,这些 block 也会消失。那么如何才能完成任务呢?

或者我在这里遗漏了一些东西......

让我解释一下我的意思:

例如,我的应用程序是一个照片消息应用程序,假设我有一个 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];   

如您所见,我使用完成 block 根据收到的响应更新该 PhotoMessage 对象。

如何通过后台传输来实现这一点?不会调用此完成 block ,因此我无法更新 newPhotoMsg

最佳答案

一些想法:

  1. 您必须确保完成Handling iOS Background Activity 中列出的必要编码。 URL 加载系统编程指南 部分说:

    If you are using NSURLSession in iOS, your app is automatically relaunched when a download completes. Your app’s application:handleEventsForBackgroundURLSession:completionHandler: app delegate method is responsible for recreating the appropriate session, storing a completion handler, and calling that handler when the session calls your session delegate’s URLSessionDidFinishEventsForBackgroundURLSession: method.

    该指南显示了您可以做什么的一些示例。坦率地说,我认为 WWDC 2013 视频 What’s New in Foundation Networking 后半部分讨论的代码示例更清晰。

  2. 如果应用只是挂起,AFURLSessionManager 的基本实现将与后台 session 一起工作(假设您已经在网络任务完成时看到调用 block )完成上述)。但正如您所猜测的,如果应用程序终止或崩溃,任何传递给您创建 NSURLSessionTask 上传和下载的方法的特定于任务的 block 参数都会丢失。”

    对于后台上传,这是一个烦恼(因为您在创建任务时指定的任务级信息进度和完成 block 不会被调用)。但是,如果您使用 session 级别的再现(例如 setTaskDidCompleteBlocksetTaskDidSendBodyDataBlock),它将被正确调用(假设您在重新实例化 session 管理器时始终设置这些 block )。

    事实证明,这个丢 block 的问题其实对于后台下载来说问题更大,但是那里的解决方案非常相似(不要使用基于任务的 block 参数,而是使用基于 session 的 block ,例如setDownloadTaskDidFinishDownloadingBlock)。

  3. 另一种选择,您可以坚持使用默认(非背景)NSURLSession,但如果用户在执行任务时离开应用程序,请确保您的应用程序请求一点时间来完成上传正在处理。例如,在您创建 NSURLSessionTask 之前,您可以创建一个 UIBackgroundTaskIdentifier:

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

    但是要确保网络任务的完成 block 正确地通知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. 确保应用委托(delegate)保存完成处理程序(根据需要实例化后台 session ):

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

    注意,我不提供任何与任务相关的 block ,因为这些 block 在后台 session 中不可靠。 (即使在应用程序终止并且这些 block 早已消失后,后台下载也会继续进行。)必须依赖于 session 级,仅可轻松重新创建 setDownloadTaskDidFinishDownloadingBlock

显然这是一个简单的示例(只有一个后台 session 对象;只是使用 URL 的最后一个组件作为文件名将文件保存到 docs 文件夹等),但希望它说明了这种模式。

https://stackoverflow.com/questions/21350125/

相关文章:

objective-c - 如何在 Swift 中实现这个多行字符串字面量宏?

ios - Core Text在iOS中计算字母框架

objective-c - performSelectorOnMainThread : and di

objective-c - 最大 CGFloat 值是否有常数?

iphone - 如何将 nil 添加到 nsmutablearray?

objective-c - 主队列上的 dispatch_sync 与 dispatch_async

ios - ld : file not found: linker command failed w

objective-c - Cocoa 的依赖注入(inject)框架?

ios - command/usr/bin/codedesign 失败,退出代码 1-代码符号错误

objective-c - 将 UIColor 保存到 NSUserDefaults 并从中加载