iOS7 中的新加入的下载类NSURLSession(随ios版本更新而更新)
想详细的了解网络下载的相关知识,要仔细阅读URL Loading System Programming Guide
这里有篇好文章(http://www.shinobicontrols.com/blog/posts/2013/09/20/ios7-day-by-day-day-1-nsurlsession/)
这里是它的译文 http://blog.csdn.net/ios_suda/article/details/12745457,感谢作者!
这里是另一篇好文章 http://www.cnblogs.com/biosli/p/iOS_Network_URL_Session.html
以往的下载用的是NSURLConnection类,现在这个NSURLSession有什么好处?
In the past networking for iOS was performed using NSURLConnection
which used the global state to manage cookies and authentication. Therefore it was possible to have 2 different connections competing with each other for shared settings.NSURLSession
sets out to solve this problem and a host of others as well.
上面说的是NSURLConnection用的是共享的cookies和认证,可能引起冲突,而NSURLSession解决了这个问题(?具体例子?)。
现在的这个NSURLSession提供了取消下载,恢复下载(断点续传),提供下载进度,后台下载,而且代理方法条理清晰,简洁明了。
涉及到的类分为3大种类(共计10个class)
NSURLSession(设置的代理可以实现的协议有 NSURLSessionDelegate ,NSURLSessionDownloadDelegate,NSURLSessionTaskDelegate,NSURLSessionDataDelegate),
NSURLSessionConfiguration,
NSURLSessionTask(子类有NSURLSessionUploadTask, NSURLSessionDownloadTask, NSURLSessionDataTask)。
使用NSURLSession 进行下载主要步奏如下:
1. 创建NSURLSessionConfiguration
2.利用NSURLSessionConfiguration创建NSURLSession
3.利用NSURLSession创建具体的NSURLSessionTask
4.调用NSURLSessionTask的resume启动下载
在创建SessionTasks时,系统提供了多重方法,大体分为带completion block 和 不带 2类,对于带completion block的初始化方法,有如下解释:
The task bypasses calls to delegate methods for response and data delivery, and instead provides any resulting NSData, URLResponse, and NSError objects inside the completion handler. Delegate methods for handling authentication challenges, however, are still called.
这段文字有点绕口,我翻译一下:创建的task会绕过为了传送数据和response而对delegate的发起的调用,作为替代,它会以在completion handler中提供作为结果的 data,response和 error。但是,仍然会调用 delegate中为了处理认证的方法。
简单地说,就是一但使用了completion block这种初始化方法,即使又指定了deleagate,与之对应的delegate中的类似方法也不会再调用了。
这里有一个非常重要的功能,后台下载功能。在以前,程序处于挂起状态后,即使申请了后台运行,也仅仅有短暂运行时间,这段运行时间往往不能够保证下载大型的数据。另外如果程序被强制结束了(系统结束的,不是被用户结束的),那么下载更不可能继续进行。NSURLSession的后台下载功能解决了上面的问题,我们看一下使用NSURLSession如何进行后台下载。
使用后台的主要特别之处是要利用
class func backgroundSessionConfigurationWithIdentifier(_ identifier: String) -> NSURLSessionConfiguration
Use this method to initialize a configuration object suitable for transferring data files while the app runs in the background. A session configured with this object hands control of the transfers over to the system, which handles the transfers in a separate process. In iOS, this configuration makes it possible for transfers to continue even when the app itself is suspended or terminated.
If an iOS app is terminated by the system and relaunched, the app can use the sameidentifier
to create a new configuration object and session and retrieve the status of transfers that were in progress at the time of termination. This behavior applies only for normal termination of the app by the system. If the user terminates the app from the multitasking screen, the system cancels all of the session’s background transfers. In addition, the system does not automatically relaunch apps that were force quit by the user. The user must explicitly relaunch the app before transfers can begin again.
方法创建一个在可以在后台运行的NSURLSessionConfiguration,利用这个设置启动的NSURLSessionTask,都可以在后台运行!
读到这里,我有2个问题:
关于挂起时的下载,我想问的问题是,能一直下载到全部下载完?如果过了几个小时还没下完怎么办?下载的速度和前台运行时的速度一样?
关于被系统结束时的下载,我想问的问题是,怎么能模拟一下,测试效果?
先看2个回掉方法说明:
optional func URLSessionDidFinishEventsForBackgroundURLSession(_ session: NSURLSession) In iOS, when a background transfer completes or requires credentials, if your app is no longer running, your app is automatically relaunched in the background, and the app’s UIApplicationDelegate is sent an application:handleEventsForBackgroundURLSession:completionHandler: message. This call contains the identifier of the session that caused your app to be launched. Your app should then store that completion handler before creating a background configuration object with the same identifier, and creating a session with that configuration. The newly created session is automatically reassociated with ongoing background activity. When your app later receives a URLSessionDidFinishEventsForBackgroundURLSession: message, this indicates that all messages previously enqueued for this session have been delivered, and that it is now safe to invoke the previously stored completion handler or to begin any internal updates that may result in invoking the completion handler.
optional func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler completionHandler: () -> Void) completionHandler The completion handler to call when you finish processing the events. Calling this completion handler lets the system know that your app’s user interface is updated and a new snapshot can be taken. whether they finished successfully or resulted in an error. The app also calls this method if authentication is required for one or more transfers. Use this method to reconnect any URL sessions and to update your app’s user interface. For example, you might use this method to update progress indicators or to incorporate new content into your views. After processing the events, execute the block in the completionHandler parameter so that the app can take a new snapshot of your user interface. If a URL session finishes its work when your app is not running, the system launches your app in the background so that it can process the event. In that situation, use the provided identifier to create a new NSURLSessionConfiguration and NSURLSession object. You must configure the other options of your NSURLSessionConfiguration object in the same way that you did when you started the uploads or downloads. Upon creating and configuring the new NSURLSession object, that object calls the appropriate delegate methods to process the events. If your app already has a session object with the specified identifier and is running or suspended, you do not need to create a new session object using this method. Suspended apps are moved into the background. As soon as the app is running again, the NSURLSession object with the identifier receives the events and processes them normally. At launch time, the app does not call this method if there are uploads or downloads in progress but not yet finished. If you want to display the current progress of those transfers in your app’s user interface, you must recreate the session object yourself. In that situation, cache the identifier value persistently and use it to recreate your session object.
//
关于第一个问题:
我首先测试了一个40m的下载文件,ios8 iphone5c,下载了2分多后,调用了
application:handleEventsForBackgroundURLSession:completionHandler 和
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session NS_AVAILABLE_IOS(7_0);
方法,正常结束。
第二次测试一个90多m的下载文件,过了25分,没有结束,重新唤醒程序,看到只下载了40多m,不知道是时间过长停止下载了,还是还在慢慢地下载。。。
第三次继续测试90m的文件,6分钟后唤醒程序,下载了百分之42,期间有很长一段时间在待机状态,感觉这可以证明在待机状态也是可以继续下载的,但是不清楚有没有时间限制。唤醒后,又回到后台状态,等待结束。8分钟后,下载成功结束!手机在待机状态也执行了回掉方法。期间也有很长的一段待机时间。这次测试时,没有运行其他的程序,不知道其他程序的运行会不会影响后台下载。
第四期,重复第二次测试,这次多等待一会。。。。25分,不够耐心,手动唤醒,百分之94,我决定再实验一次。
第五次,重复第二次测试,45分,期间正常使用电话,不知道是不是按错了,点回来session没有继续,不知道为什么。
第六次,在第五次基础上再用同一个session发起另一个请求,20分钟后查看,第五次实验的task有了反应。。。0.85进度,而第六次的进度是0.45. 我感觉这2个task都处于停止状态!
由上面的实验可以测出,过长时间的后台下载是不实际的!仔细想想ios提供的后台机制就该想到,这种无限制的后台运行不可能存在。。。。不然不和android一样费电了吗。。。
关于第二个问题,我用用模拟器模拟一下内存溢出,程序下载继续执行,貌似不能说明什么问题。。。
AFNetworking框架也为NSURLSession做了相应的调整。
关于AFNetworking framework的基本使用方法,请参见github上的说明文档,也可以参见readme文档。
最新版的AFNetworking framework 提供了2套网络上传下载的解决方案,一套基于以前的NSURLConnection,另一套基于新出的NSURLSession。由于NSURLConnection产生的较早,没有集成与线程相关的代码,所以如果在其他线程中进行下载,需要自己建立NSOperation Queue,并把相应的operation加入到quque中。注意,如果你要支持ios7以前的版本,那么必须用这套方案。主要涉及AFURLConnectionOperation,AFHTTPRequestOperation,AFHTTPRequestOperationManager3个类,其中最重要的就是AFURLConnectionOperation类,它提供了多个block属性,可以让你指定block,实现认证,显示进度等功能。
示例代码:
NSMutableArray *mutableOperations = [NSMutableArray array]; for (NSURL *fileURL in filesToUpload) { NSURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:@"http://example.com/upload" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) { [formData appendPartWithFileURL:fileURL name:@"images[]" error:nil]; }]; AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; [mutableOperations addObject:operation]; } NSArray *operations = [AFURLConnectionOperation batchOfRequestOperations:@[...] progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) { NSLog(@"%lu of %lu complete", numberOfFinishedOperations, totalNumberOfOperations); } completionBlock:^(NSArray *operations) { NSLog(@"All operations in batch complete"); }]; [[NSOperationQueue mainQueue] addOperations:operations waitUntilFinished:NO];
-----------------------------------------------------------------------------------------------------------
而NSURLSession不同,它本身就拥有了task的概念,使用起来也比较方便,例如
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration]; NSURL *URL = [NSURL URLWithString:@"http://example.com/upload"]; NSURLRequest *request = [NSURLRequest requestWithURL:URL]; NSURL *filePath = [NSURL fileURLWithPath:@"file://path/to/image.png"]; NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithRequest:request fromFile:filePath progress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { if (error) { NSLog(@"Error: %@", error); } else { NSLog(@"Success: %@ %@", response, responseObject); } }]; [uploadTask resume];
这里task的代理没有单独的设置,都是调用session对象的代理。而且session对象的代理在初始化时就要指定。在创建task时,如果你使用了带complete回调块的创建函数,系统就不会再调用session中相应的进度监听回掉方法。如果想调用代理中的进度方法,就不能使用带complete回调块的创建函数。
task 创建后不会自动开始,需要调用resume函数开始。
afnetworking 和 nsurlsession 中都没有提供串行下载的方法,即无法在一个下载完成后再继续另一个下载。需要研究下是不是真的不行????????
------------------------------------------------------------------------
再说一点题外话:
AFNetworking 的另一大功能是它提供了一套序列化的代码,看一下demo:
NSURL *URL = [NSURL URLWithString:@"http://example.com/resources/123.json"]; NSURLRequest *request = [NSURLRequest requestWithURL:URL]; AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request]; op.responseSerializer = [AFJSONResponseSerializer serializer]; [op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { NSLog(@"JSON: %@", responseObject); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { NSLog(@"Error: %@", error); }]; [[NSOperationQueue mainQueue] addOperation:op];
-----------------------------------------------------------------