ios AFNetworking 和后台传输

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/21350125/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 23:15:24  来源:igfitidea点击:

AFNetworking and background transfers

iosobjective-cafnetworkingafnetworking-2

提问by Mario

I'm a bit confuse of how to take advantage of the new iOS 7 NSURLSessionbackground transfers features and AFNetworking(versions 2 and 3).

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

I saw the WWDC 705 - What's New in Foundation Networkingsession, and they demonstrated background download that continues after the app terminated or even crashes.

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

This is done using the new API application:handleEventsForBackgroundURLSession:completionHandler:and the fact that the session's delegate will eventually get the callbacks and can complete its task.

这是使用新的 APIapplication:handleEventsForBackgroundURLSession:completionHandler:以及会话的委托最终将获得回调并可以完成其任务这一事实来完成的。

So I'm wondering how to use it with AFNetworking (if possible) to continue downloading in background.

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

The problem is, AFNetworking conveniently uses block based API to do all the requests, but if the app terminated or crashes those block are also gone. So how can I complete the task?

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

Or maybe I'm missing something here...

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

Let me explain what I mean:

让我解释一下我的意思:

For example my app is a photo messaging app, lets say that I have a PhotoMessageobject that represent one message and this object has properties like

例如,我的应用程序是一个照片消息应用程序,假设我有一个PhotoMessage表示一条消息的对象,并且该对象具有类似的属性

  • state- describe the state of the photo download.
  • resourcePath- the path to the final downloaded photo file.
  • state- 描述照片下载的状态。
  • resourcePath- 最终下载的照片文件的路径。

So when I get a new message from the server, I create a new PhotoMessageobject, and start downloading its photo resource.

因此,当我从服务器收到一条新消息时,我创建了一个新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];   

As you can see, I use the completion block to update that PhotoMessageobject according to the response I get.

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

How can I accomplish that with a background transfer? This completion block won't be called and as a result, I can't update the newPhotoMsg.

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

回答by Rob

A couple of thoughts:

一些想法:

  1. You have to make sure you do the necessary coding outlined in the Handling iOS Background Activitysection of the URL Loading System Programming Guidesays:

    If you are using NSURLSessionin 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.

    That guide shows some examples of what you can do. Frankly, I think the code samples discussed in the latter part of the WWDC 2013 video What's New in Foundation Networkingare even more clear.

  2. The basic implementation of AFURLSessionManagerwill work in conjunction with background sessions if the app is merely suspended (you'll see your blocks called when the network tasks are done, assuming you've done the above). But as you guessed, any task-specific block parameters that are passed to the AFURLSessionManagermethod where you create the NSURLSessionTaskfor uploads and downloads are lost "if the app terminated or crashes."

    For background uploads, this is an annoyance (as your task-level informational progress and completion blocks you specified when creating the task will not get called). But if you employ the session-level renditions (e.g. setTaskDidCompleteBlockand setTaskDidSendBodyDataBlock), that will get called properly (assuming you always set these blocks when you re-instantiate the session manager).

    As it turns out, this issue of losing the blocks is actually more problematic for background downloads, but the solution there is very similar (do not use task-based block parameters, but rather use session-based blocks, such as setDownloadTaskDidFinishDownloadingBlock).

  3. An alternative, you could stick with default (non-background) NSURLSession, but make sure your app requests a little time to finish the upload if the user leaves the app while the task is in progress. For example, before you create your NSURLSessionTask, you can create a UIBackgroundTaskIdentifier:

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

    But make sure that the completion block of the network task correctly informs iOS that it is complete:

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

    This is not as powerful as a background NSURLSession(e.g., you have a limited amount of time available), but in some cases this can be useful.

  1. 您必须确保执行URL 加载系统编程指南处理 iOS 后台活动部分中概述的必要编码:

    如果您NSURLSession在 iOS中使用,当下载完成时您的应用程序会自动重新启动。您的应用程序的application:handleEventsForBackgroundURLSession:completionHandler:应用程序委托方法负责重新创建适当的会话、存储完成处理程序,并在会话调用您的会话委托的URLSessionDidFinishEventsForBackgroundURLSession:方法时调用该处理程序。

    该指南显示了一些您可以做什么的示例。坦率地说,我认为 WWDC 2013 视频“基础网络的新特性”的后半部分中讨论的代码示例更加清晰。

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

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

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

  3. 另一种方法是,您可以坚持使用 default (non-background) 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(例如,您的可用时间有限),但在某些情况下这可能很有用。



Update:

更新:

I thought I'd add a practical example of how to do background downloads using AFNetworking.

我想我会添加一个如何使用 AFNetworking 进行后台下载的实际示例。

  1. First define your background manager.

    //
    //  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
    

    and

    //
    //  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. Make sure app delegate saves completion handler (instantiating the background session as necessary):

    - (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. Then start your downloads:

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

    Note, I don't supply any of those task related blocks, because those aren't reliable with background sessions. (Background downloads proceed even after the app is terminated and these blocks have long disappeared.) One must rely upon the session-level, easily recreated setDownloadTaskDidFinishDownloadingBlockonly.

  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只能轻松重新创建。

Clearly this is a simple example (only one background session object; just saving files to the docs folder using last component of URL as the filename; etc.), but hopefully it illustrates the pattern.

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

回答by Stavash

It shouldn't make any difference whether or not the callbacks are blocks or not. When you instantiate an AFURLSessionManager, make sure to instantiate it with NSURLSessionConfiguration backgroundSessionConfiguration:. Also, make sure to call the manager's setDidFinishEventsForBackgroundURLSessionBlockwith your callback block - this is where you should write the code typically defined in NSURLSessionDelegate's method: URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session. This code should invoke your app delegate's background download completion handler.

回调是否是块应该没有任何区别。实例化 时AFURLSessionManager,请确保使用 实例化它NSURLSessionConfiguration backgroundSessionConfiguration:。此外,请确保setDidFinishEventsForBackgroundURLSessionBlock使用您的回调块调用管理器- 这是您应该编写通常在 NSURLSessionDelegate 方法中定义的代码的地方: URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session。此代码应调用您的应用程序委托的后台下载完成处理程序。

One word of advice regarding background download tasks - even when running in the foreground, their timeouts are ignored, meaning you could get "stuck" on a download that's not responding. This is not documented anywhere and drove me crazy for some time. The first suspect was AFNetworking but even after calling NSURLSession directly, the behaviour remained the same.

关于后台下载任务的一个建议 - 即使在前台运行时,它们的超时也会被忽略,这意味着您可能会“卡住”没有响应的下载。这在任何地方都没有记录,让我发疯了一段时间。第一个嫌疑人是 AFNetworking 但即使在直接调用 NSURLSession 之后,行为仍然保持不变。

Good luck!

祝你好运!

回答by Zhans

AFURLSessionManager

AFURL会话管理器

AFURLSessionManagercreates and manages an NSURLSessionobject based on a specified NSURLSessionConfigurationobject, which conforms to <NSURLSessionTaskDelegate>, <NSURLSessionDataDelegate>, <NSURLSessionDownloadDelegate>, and <NSURLSessionDelegate>.

AFURLSessionManager创建和管理的NSURLSession基于指定的对象上NSURLSessionConfiguration的对象,它符合<NSURLSessionTaskDelegate><NSURLSessionDataDelegate><NSURLSessionDownloadDelegate>,和<NSURLSessionDelegate>

link to documentation here documentation

链接到此处的文档文档