建议正确管理NSURLSession



我开始开发一个iphone应用程序,我需要一些关于NSURLSession以及如何正确管理数据下载和解析的建议。

我刚刚纠正了nsurlsession中下载数据的错误,但关于我发现理解这些异步请求有多困难,我认为我的解决方案不是很好。。。还有两种不同的下载解决方案出现了下载错误,这让我觉得我忘了做点什么。。。

在我的项目中,我下载了不同的xml文件(有时还有一些带图片的zip文件),我需要在显示它们的信息之前进行解析。这些信息可以很快更改,所以如果我再次加载我的页面,我想再次下载它们。我一直在寻找一种简单的方式来管理所有的下载,就像我不需要重写很多代码一样。

我首先找到了这个项目。

有了这些,我只需要使用这些代码来管理下载:

NSString *downloadUrl = @"https://www.url.com";
NSURL *location = [NSURL URLWithString:downloadUrl];
// DownloadManager  is my version of the CTSessionOperation of the github project
DownloadManager *operation = [DownloadManager new];
operation.downloadUrl = downloadUrl;
operation.completionAction = ^(NSURL *xmlUrl, BOOL success){
dispatch_async(dispatch_get_main_queue(), ^{
if (success){
regions = [[TeamChoiceManager sharedManager] parseRegions:[NSData dataWithContentsOfURL:location]];
[self.tableView performSelectorOnMainThread:@selector(reloadData) withObject:Nil waitUntilDone:YES];
}
});
};
operation.isBackground = YES;
[operation enqueueOperation];

这段代码在我第一次下载时运行得很好。但如果我尝试再次启动下载,下载不会启动(所以没有错误,只是,这段代码下载了一次,仅此而已)。

我通过修改CTSessionOperation/DownloadManager中的方法(NSURLSession *)session来纠正这个错误。我在评论中加入了"dispatch_once"以使其发挥作用,但我认为这不是一个好的解决方案。。。

我尝试了另一个导致相同错误的解决方案。我用这个代码管理下载:

NSString *regionsUrl= @"url";
NSURLSessionConfiguration *sessionConfig =
[NSURLSessionConfiguration defaultSessionConfiguration];
// My solution to the bug          
/*NSURLSessionConfiguration *backgroundConfiguration = [NSURLSessionConfiguration
backgroundSessionConfiguration:[NSString stringWithFormat:@"com.captech.mysupersession.BackgroundSession%d",numBGSession]]; */
//   numBGSession++; this is a static NSInteger

NSURLSession *session =
[NSURLSession sessionWithConfiguration:backgroundConfiguration
delegate:teamChoiceDetailViewController
delegateQueue:nil];
NSURLSessionDownloadTask *sessDLTask =
[session downloadTaskWithURL:[NSURL URLWithString:regionsUrl]];
[sessDLTask resume];

在代表中:

-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
dispatch_async(dispatch_get_main_queue(), ^{
self.regions = [[TeamChoiceManager sharedManager] parseRegions:[NSData dataWithContentsOfURL:location]];
[self.tableView performSelectorOnMainThread:@selector(reloadData) withObject:Nil waitUntilDone:YES];
});
}

有了这个解决方案,我每次尝试下载时都会创建一个自定义的NSURLSessionConfiguration来避免这个错误。

所以我们开始吧。我对这两种解决方案很困惑。我不知道它们是否是管理下载的正确方法,我认为我没有正确纠正错误,而且我一定错过了NSURLSession的逻辑。

你有什么改进这些解决方案的建议吗?或者你认为其中一个比另一个好得多吗?

EDIT:如果有人正在寻找一个更通用的解决方案来轻松正确地管理网络事务,你可以查看AFNetworking,它有一些神奇之处(此外,你还可以找到很多教程)。

我放弃了开发适用于每个案例的东西(尤其是如果它是后台会议,因为我不需要它)。最后,我只创建了一个带有静态会话的类,它管理下载的委托,以及我在didFinishDownloadingToURL中使用的块来管理下载的数据。当然不完美,但目前已经足够好了。

typedef void (^CTCompletionBlock)(NSURL *location, NSError* err);
@interface DownloadManager :  NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate>
@property (nonatomic, retain) NSURLSessionDownloadTask *dlTask;
@property (nonatomic, retain) NSString *location;
@property (strong) CTCompletionBlock afterDLBlock;
+ (DownloadManager *)sharedManager;
-(void)downloadTask;
@end

//
//  DownloadManager.m
//  MVCTest
//
//  Created by 
//
#import "DownloadManager.h"
#import "AppDelegate.h"

static DownloadManager *instance = nil;
static NSURLSession *session = nil;
@implementation DownloadManager
+ (DownloadManager *)sharedManager {
if (instance == nil) {
//session = [DownloadManager sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
instance = [DownloadManager new];
}
return instance;
}
+ (id)new
{
return [[self alloc] init];
}
- (id)init{
self = [super init];
session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
return self;
}
-(void)downloadTask{
self.dlTask = [session downloadTaskWithURL:[NSURL URLWithString:self.location]];
[self.dlTask resume];
}
#pragma mark - NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSError *error;
if (self.afterDLBlock){
self.afterDLBlock(location, error);
}
}
//i still have to manage the delegate...
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {}
#pragma mark - NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {}
-(void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error{}
#pragma mark - NSURLSessionDelegate
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {}
@end

对于这个类,我只需要编写以下代码来管理下载的数据:

typeof(self) __weak weakSelf = self;
NSString *downloadUrl = @"http://www.whatyouwant.com";
[DownloadManager sharedManager].location = downloadUrl;
[DownloadManager sharedManager].afterDLBlock = ^(NSURL *location, NSError *error) {
weakSelf.regions = [[TeamChoiceManager sharedManager] parseRegions:[NSData dataWithContentsOfURL:location]];
dispatch_sync(dispatch_get_main_queue(), ^{
[weakSelf.activityViewIndicator stopAnimating];
[weakSelf.tableView reloadData];
});
};
[[DownloadManager sharedManager] downloadTask];

我仍然需要管理错误和委托,但有了taht解决方案,我将没有太多代码来下载一些数据

几个观察结果:

  1. 如果使用后台NSURLSession,请注意这可能比标准NSURLSession慢。而且,如果你进行了大量下载,你的会话可能会被以前的一些请求积压,所以当你回来时,它似乎什么都没做(事实上,它可能只是忙于完成以前的任务请求,这些任务请求试图更新一个不存在或不再可见的表视图实例)。这可以解释为什么创建新会话似乎可以正常工作(因为新会话不会将新任务排在先前排队的任务之后,而是并行运行它们)。

    我建议:

    • 恢复dispatch_once逻辑(因为你在这里的直觉是正确的;你绝对不想做一堆会话,让旧的会话继续运行);和

    • 当您返回到此视图控制器时,请在启动新请求之前取消任何挂起的请求。

  2. 您最初尝试使用completionAction是有问题的,因为:

    • 您正在保留视图控制器。所以不是:

      operation.completionAction = ^(NSURL *xmlUrl, BOOL success){
      dispatch_async(dispatch_get_main_queue(), ^{
      if (success){
      regions = [[TeamChoiceManager sharedManager] parseRegions:[NSData dataWithContentsOfURL:location]];
      [self.tableView performSelectorOnMainThread:@selector(reloadData) withObject:Nil waitUntilDone:YES];
      }
      });
      };
      

      你可能想要:

      typeof(self) __weak weakSelf = self;
      operation.completionAction = ^(NSURL *xmlUrl, BOOL success){
      dispatch_async(dispatch_get_main_queue(), ^{
      if (success){
      regions = [[TeamChoiceManager sharedManager] parseRegions:[NSData dataWithContentsOfURL:location]];
      [weakSelf.tableView reloadData]; // don't need that `performSelectorOnMainThread` call
      }
      });
      };
      
    • 您还尝试将此completionAction与背景NSURLSession结合使用。请注意,如果应用程序被终止,而下载将在后台完成,则此完成块将丢失(破坏进行后台会话的目的)。

      如果您确实需要使用后台会话,那么应该将此completionAction中的任何逻辑移动到下载委托方法本身中。您不能将此completionAction与后台会话结合使用,并期望它在应用程序终止后仍然存在(即使NSURLSessionDownloadTask对象会存在)。


以下是我的原始答案,我在其中猛烈抨击CTSessionOperation类,因为(a)将完成块与背景NSURLSession结合使用存在认知失调(因为如果应用程序终止,即使下载仍将继续,这些块也会丢失);以及(b)它将非并发NSOperation用于异步任务,击败了使用基于NSOperation的实现的许多关键优势。虽然这两点肯定都有问题,但我认为这些担忧是次要的,但我会将其保留在这里供参考。

原始答案:

你所说的"第一次下载时效果很好;但如果我再次尝试启动,则不会"是什么意思?你说的是应用程序被终止,下载仍在后台进行吗?最初的CTSessionOperation似乎无法正确处理这一问题,因为它试图使用NSOperation和完成块的标准技巧,但所有这些都与即使在应用程序终止后仍在进行的NSURLSession后台会话不兼容。一旦应用程序终止,NSURLSession后台请求将继续进行,但所有操作和完成块都将完全丢失。我认为这个CTSessionOperation在这里没有达到目标。你真的无法享受即使在应用程序终止后仍在进行的丰富的后台会话,并期望保留你第一次启动后台下载任务时创建的操作和完成块。您必须坚持使用NSURLSessionDownloadTask对象,并且只使用委托方法;没有完成块或操作。

尽管我批评了CTSessionOperation的结构缺陷,但如果您需要后台下载,尝试排除dispatch_once并创建唯一的后台会话或创建标准的前台会话肯定不是正确的方法。如果有什么不同的话,如果你试图解决CTSessionOperation在应用程序终止后无法处理后台会话的问题,我认为这实际上是朝着错误的方向发展的。

因此,问题是,你是否真的想享受后台NSURLSession的全部功能(在这种情况下,你必须放弃CTSessionOperation的完成块和操作模式),或者你是否同意非后台NSURLSession,当应用程序本身终止时,下载将终止,你只想在用户再次启动应用程序时能够从头开始重新启动它们。

最新更新