最近一直在学习AFNetWorking这个框架,我们知道这个框架主要就是封装的的NSURLSession这个类及其相关的类,那么我们要想搞清楚AFNetworking这个框架,首先就要搞清楚NSURLSession这个类相关的类。
首先学习一下在NSURLSession之前使用的一个类:NSURLConnection这个类。

一.NSURLConnection

NSURLConnection这个类是在NSURLSession之前进行网络请求使用的类。这个类对象通过NSURLRequest对象来加载内容,NSURLConnection的接口非常少,只提供了开始和取消异步网络请求的接口。大部分的网络请求的配置要通过设置NSURLRequest去进行。

1. NSURLConnection的代理

NSURLConnection的代理有三个,NSURLConnectionDelegate,NSURLConnectionDataDelegate,NSURLConnectionDownloadDelegate,NSURLConnection对象会调用这些代理方法来提供网络请求的进度和状态。

NSURLConnectionDelegate

这个协议主要使用来处理认证相关的问题,但是也处理网络请求失败时的回调,所以所有NSURLConnection的delegate必须实现这个协议。
主要的协议方法:

@在将要向请求发送认证挑战时回调这个啊方法
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;@当网络请求失败时会回调这个方法
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

NSURLConnectionDataDelegate

除非你是使用Newsstand这个框架,否则你也应该遵守NSURLConnectionDataDelegate这个协议,这个协议在上传文件时会提供进度信息,在下载数据时会周期性的回调下载的数据片等等。
主要的协议方法:

@当收到响应的时候会回调这个方法
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;@在获取从服务器端返回的数据时这个方法会被周期性的回调,每次的data都是服务器端新返回的数据,我们把这些数据拼接在一起就可以得到完整的数据。
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;@在上传请求体的数据时,回调这个方法来显示上传的进度
- (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite;@网络请求成功完成的时候会回调这个方法
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;

NSURLConnectionDownloadDelegate

如果使用了Newsstand这个框架,那么这个协议也应该遵守。

2.NSURLConnection的基础API

同步加载网络请求:

+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse * _Nullable *)response error:(NSError * _Nullable *)error;

异步加载网络请求:

@使用NSURLRequest创建一个NSURLConnection对象,并且给这个对象指定代理,然后返回这个对象,并开始网络请求
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id)delegate;@通过NSURLRequest创建一个NSURLConnection对象,并给它指定delegate,返回这个对象,并开始网络请求
- (instancetype)initWithRequest:(NSURLRequest *)request delegate:(id)delegate;@和上面方法一样,只是多了一个是否立刻开始进行网络请求
- (instancetype)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL)startImmediately;@通过NSURLRequest进行异步请求,请求完成之后在执行的队列里执行回调的操作
+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse *response, NSData *data, NSError *connectionError))handler;

开始和取消网络请求:

@开始网络请求
- (void)start;@取消网络请求,注意只有在异步的时候才能取消,同步请求的时候是不能取消网络请求的。
- (void)cancel;

3.NSURLConnection的简单使用

1.get请求

//由于NSURLRequest默认的就是get请求,所以这里不需要设置HTTPmethodNSString *urlString = @"http://www.qingmooc.com";NSURL *url = [NSURL URLWithString:urlString];NSURLRequest *request = [NSURLRequest requestWithURL:url];NSURLResponse *response;NSError *error = 0;//NSURLConnection 发送同步请求/*@param1:请求对象@param2:响应头@param3:错误信息@return:响应体-返回的数据**/NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

2.异步post请求

NSString *urlString = @"http://www.qingmooc.com";NSURL *url = [NSURL URLWithString:urlString];NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];//由于方法不是默认的get方法,所以要设置方法参数request.HTTPMethod = @"POST";//设置请求超时时间request.timeoutInterval = 10.0;//设置请求体NSString *params = [NSString stringWithFormat:@"username=%@", @"pdd"];request.HTTPBody = [params dataUsingEncoding:NSUTF8StringEncoding];//设置请求头[request setValue:[[UIDevice currentDevice] systemVersion] forHTTPHeaderField:@"User-Agent"];//NSURLConnection 发送异步请求/*@param1:请求对象@param2:队列,决定block在哪个队列调用@param3:block块,当请求结束之后j就会i调用block 参数:response:响应头 data:响应体**/[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {}];

4.使用NSURLConnection进行断点下载

使用NSURLConnection进行断点下载主要是对HTTP请求的一个首部的应用,这个首部就是Content-Range,客户端可以通过添加Content-Range范围请求来向服务器指定要获取实体的哪一部分内容,比如Content-Range:20224-30224,说明要向服务器获取实体从20224字节到30224字节的内容。Content-Range:20224-,说明要向服务器获取实体从20225字节开始到结束的所有内容。

我们可以使用代理方法,在每次获取到数据的时候把数据写入文件中,并读取文件中数据的长度作为最新的数据长度,这样如果暂停后再开始,那么就从这个数据长度开始请求实体。具体代码如下:

@interface NSURLConnectionDemoViewController ()<NSURLConnectionDelegate, NSURLConnectionDataDelegate>@property (nonatomic, strong)NSURLConnection *connection;
@property (nonatomic, strong)NSFileHandle *handle;
@property (nonatomic, assign)NSInteger currentLength;
@property (nonatomic, assign)NSInteger totalLength;
@property (nonatomic, strong)NSString *filePath;@end@implementation NSURLConnectionDemoViewController- (void)viewDidLoad {[super viewDidLoad];}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{NSString *urlString = @"http://www.qingmooc.com";NSURL *url = [NSURL URLWithString:urlString];NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];//设置请求超时时间request.timeoutInterval = 10.0;//设置请求体NSString *params = [NSString stringWithFormat:@"username=%@", @"pdd"];request.HTTPBody = [params dataUsingEncoding:NSUTF8StringEncoding];//设置请求头[request setValue:[[UIDevice currentDevice] systemVersion] forHTTPHeaderField:@"User-Agent"];//通过Content-rRange请求头部来进行断点下载[request setValue:[NSString stringWithFormat:@"bytes=%zd-", self.currentLength] forHTTPHeaderField:@"Content-Range"];self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}#pragma mark - <NSURLConnectionDelegate>
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{NSLog(@"网络请求失败了");
}#pragma mark - <NSURLConnectionDataDelegate>
//收到响应的时候回调这个方法
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{//只在第一次开始下载的时候才进行下面的操作if (self.currentLength > 0) {return;}//创建文件NSString *caches = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;NSString *filePath = [caches stringByAppendingString:response.suggestedFilename];self.filePath = filePath;[[NSFileManager defaultManager] createFileAtPath:self.filePath contents:nil attributes:nil];//得到文件的总长度,为了显示下载进度self.totalLength = response.expectedContentLength;
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{//使用文件句柄把数据b拼接到文件结尾self.handle = [NSFileHandle fileHandleForWritingAtPath:self.filePath];[self.handle seekToEndOfFile];[self.handle writeData:data];//由于又获取了新的数据,所以更新currentLengthNSData *fileData = [[NSFileManager defaultManager] contentsAtPath:self.filePath];self.currentLength = fileData.length;//显示下载进度NSLog(@"progress = %ld", self.currentLength / self.totalLength);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{[self.handle closeFile];self.handle = nil;
}
@end

5.NSURLRequest

NSURLConnection的基本内容都讲完了,我们看到,NSURLConnection提供的接口非常简单,几乎所有的配置都要通过NSURLRequest来完成,那么下面我们就来学习一下NSURLRequest。

NSURLRequest类封装了网络请求的两个基本元素,一个是网络请求的URL,还有一个就是缓存策略。

由于NSURLRequest只能使用默认的请求方法get方法,所以其可定制性比较差,我们一般用的是其子类NSMutableURLRequest,下面我们就直接看其子类NSMutableURLRequest。
直白的讲,NSMutableURLRequest就是封装了请求的URL,然后可以定制请求头,还可以设置请求的一些其他的配置,如请求超时时间,缓存策略,是否允许使用蜂窝网等。
设置缓存策略:

@property NSURLRequestCachePolicy cachePolicy;typedef enum NSURLRequestCachePolicy : NSUInteger {NSURLRequestUseProtocolCachePolicy = 0,NSURLRequestReloadIgnoringLocalCacheData = 1,NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4,NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,NSURLRequestReturnCacheDataElseLoad = 2,NSURLRequestReturnCacheDataDontLoad = 3,NSURLRequestReloadRevalidatingCacheData = 5
} NSURLRequestCachePolicy;

设置请求方法和请求体:

//默认方法是get
@property(copy) NSString *HTTPMethod;
@property(copy) NSData *HTTPBody;

设置请求头

//字典类型,返回所有的请求头
@property(copy) NSDictionary<NSString *,NSString *> *allHTTPHeaderFields;
//添加请求头,注意 ,只添加key,不设置value
- (void)addValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
//为请求头设值
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;

设置请求行为:

//设置请求超时时间
timeoutInterval
//布尔值,设置是否使用默认的Cookie处理方法,默认是YES
HTTPShouldHandleCookies
//布尔值,设置是否支持蜂窝网络,默认是YES
allowsCellularAccess

需要注意的是,我们不应该使用设置请求头的方法来设置下列请求头:

  • Authorization

  • Connection

  • Host

  • Proxy-Authenticate

  • Proxy-Authorization

  • WWW-Authenticate

6.NSURLResponse

不管什么时候我们在进行HTTP请求的时候,实际上返回的都是NSURLResponse的子类:NSHTTPURLResponse对象。

NSURLResponse对象并不包含真正的响应的实体数据,只是包含一些响应的实体相关的信息。实体数据是通过代理的回调一部分一部分的返回或者是在请求完成时整个全部返回,这取决于我们初始化NSURLRequest时用的是什么方法。
NSURLResponse的属性:

//预期的响应的实体的长度,常用它来显示进度
expectedContentLength
//响应数据的建议的文件名,常用它来拼接成存放实体文件的路径
suggestedFilename
//响应的MIME类型
MIMEType
//响应的URL
URL

NSURLRsponse的相关属性非常有限,下面看一下NSHTTPURLResponse,通过NSHTTPURLResponse可以获取响应的起始行的状态码和描述短语,还有响应头的信息。

//字典类型,返回响应头的信息
allHeaderFields
//通过状态码返回描述性短语
+ (NSString *)localizedStringForStatusCode:(NSInteger)statusCode;
//返回响应的状态码
statusCode

开始 NSURLSession学习

NSURLSession

NSURLSessioniOS7中推出,NSURLSession的推出旨在替换之前的NSURLConnectionNSURLSession的使用相对于之前的NSURLConnection更简单,而且不用处理Runloop相关的东西。

2015年RFC 7540标准发布了http 2.0版本,http 2.0版本中包含很多新的特性,在传输速度上也有很明显的提升。NSURLSessioniOS9.0开始,对http 2.0提供了支持。

NSURLSession由三部分构成:

  • NSURLSession:请求会话对象,可以用系统提供的单例对象,也可以自己创建。
  • NSURLSessionConfiguration:对session会话进行配置,一般都采用default
  • NSURLSessionTask:负责执行具体请求的task,由session创建。

NSURLSession有三种方式创建:

sharedSession

系统维护的一个单例对象,可以和其他使用这个sessiontask共享连接和请求信息。

sessionWithConfiguration:

在NSURLSession初始化时传入一个NSURLSessionConfiguration,这样可以自定义请求头、cookie等信息。

sessionWithConfiguration:delegate:delegateQueue:

如果想更好的控制请求过程以及回调线程,需要上面的方法进行初始化操作,并传入delegate来设置回调对象和回调的线程。

通过NSURLSession发起一个网络请求也比较简单。

  1. 创建一个NSURLSessionConfiguration配置请求。
  2. 通过Configuration创建NSURLSession对象。
  3. 通过session对象发起网络请求,并获取task对象。
  4. 调用[task resume]方法发起网络请求。

  1. NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];

  2. NSURLSession *session = [NSURLSession sessionWithConfiguration:config

  3. delegate:self

  4. delegateQueue:[NSOperationQueue mainQueue]];

  5. NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];

  6. [task resume];

NSURLSessionTask

通过NSURLSession发起的每个请求,都会被封装为一个NSURLSessionTask任务,但一般不会直接是NSURLSessionTask类,而是基于不同任务类型,被封装为其对应的子类。

  • NSURLSessionDataTask:处理普通的GetPost请求。
  • NSURLSessionUploadTask:处理上传请求,可以传入对应的上传文件或路径。
  • NSURLSessionDownloadTask:处理下载地址,提供断点续传功能的cancel方法。

主要方法都定义在父类NSURLSessionTask中,下面是一些关键方法或属性。

currentRequest
当前正在执行的任务,一般和originalRequest是一样的,除非发生重定向才会有所区别。
originalRequest
主要用于重定向操作,用来记录重定向前的请求。
taskIdentifier
当前session下,task的唯一标示,多个session之间可能存在相同的标识。
priority
task中可以设置优先级,但这个属性并不代表请求的优先级,而是一个标示。官方已经说明,NSURLSession并没有提供API可以改变请求的优先级。
state
当前任务的状态,可以通过KVO的方式监听状态的改变。
- resume
开始或继续请求,创建后的task默认是挂起的,需要手动调用resume才可以开始请求。
- suspend
挂起当前请求。主要是下载请求用的多一些,普通请求挂起后都会重新开始请求。下载请求挂起后,只要不超过NSURLRequest设置的timeout时间,调用resume就是继续请求。
- cancel
取消当前请求。任务会被标记为取消,并在未来某个时间调用URLSession:task:didCompleteWithError:方法。

NSURLSession提供有普通创建task的方式,创建后可以通过重写代理方法,获取对应的回调和参数。这种方式对于请求过程比较好控制。


  1. - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;

  2. - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;

  3. - (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;

除此之外,NSURLSession也提供了block的方式创建task,创建方式简单如AFN,直接传入URLNSURLRequest,即可直接在block中接收返回数据。和普通创建方式一样,block的创建方式创建后默认也是suspend的状态,需要调用resume开始任务。

completionHandlerdelegate是互斥的,completionHandler的优先级大于delegate。相对于普通创建方法,block方式更偏向于面向结果的创建,可以直接在completionHandler中获取返回结果,但不能控制请求过程。


  1. - (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

  2. - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(nullable NSData *)bodyData completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

  3. - (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

可以通过下面的两个方法,获取当前session对应的所有task,方法区别在于回调的参数不同。以getTasksWithCompletionHandler为例,在AFN中的应用是用来获取当前sessiontask,并将AFURLSessionManagerTaskDelegate的回调都置为nil,以防止崩溃。


  1. - (void)getTasksWithCompletionHandler:(void (^)(NSArray<NSURLSessionDataTask *> *dataTasks, NSArray<NSURLSessionUploadTask *> *uploadTasks, NSArray<NSURLSessionDownloadTask *> *downloadTasks))completionHandler;

  2. - (void)getAllTasksWithCompletionHandler:(void (^)(NSArray<__kindof NSURLSessionTask *> *tasks))completionHandler);

delegateQueue

在初始化NSURLSession时可以指定线程,如果不指定线程,则completionHandlerdelegate的回调方法,都会在子线程中执行。

如果初始化NSURLSession时指定了delegateQueue,则回调会在指定的队列中执行,如果指定的是mainQueue,则回调在主线程中执行,这样就避免了切换线程的问题。

[NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];

delegate

对于NSURLSession的代理方法这里就不详细列举了,方法命名遵循苹果一贯见名知意的原则,用起来很简单。这里介绍一下NSURLSession的代理继承结构。

代理继承关系

NSURLSession中定义了一系列代理,并遵循上面的继承关系。根据继承关系和代理方法的声明,如果执行某项任务,只需要遵守其中的某个代理即可。

例如执行上传或普通Post请求,则遵守NSURLSessionDataDelegate,执行下载任务则遵循NSURLSessionDownloadDelegate,父级代理定义的都是公共方法。

请求重定向

HTTP协议中定义了例如301等重定向状态码,通过下面的代理方法,可以处理重定向任务。发生重定向时可以根据response创建一个新的request,也可以直接用系统生成的request,并在completionHandler回调中传入,如果想终止这次重定向,在completionHandler传入nil即可。


  1. - (void)URLSession:(NSURLSession *)session

  2. task:(NSURLSessionTask *)task

  3. willPerformHTTPRedirection:(NSHTTPURLResponse *)response

  4. newRequest:(NSURLRequest *)request

  5. completionHandler:(void (^)(NSURLRequest *))completionHandler

  6. {

  7. NSURLRequest *redirectRequest = request;

  8. if (self.taskWillPerformHTTPRedirection) {

  9. redirectRequest = self.taskWillPerformHTTPRedirection(session, task, response, request);

  10. }

  11. if (completionHandler) {

  12. completionHandler(redirectRequest);

  13. }

  14. }

NSURLSessionConfiguration

创建方式

NSURLSessionConfiguration负责对NSURLSession初始化时进行配置,通过NSURLSessionConfiguration可以设置请求的Cookie、密钥、缓存、请求头等参数,将网络请求的一些配置参数从NSURLSession中分离出来。


  1. NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];

  2. NSURLSession *session = [NSURLSession sessionWithConfiguration:config

  3. delegate:self

  4. delegateQueue:[NSOperationQueue mainQueue]];

NSURLSessionConfiguration提供三种初始化方法,下面是请求的方法的一些解释。

@property (class, readonly, strong) NSURLSessionConfiguration *defaultSessionConfiguration;

NSURLSessionConfiguration提供defaultSessionConfiguration的方式创建,但这并不是单例方法,而是类方法,创建的是不同对象。通过这种方式创建的configuration,并不会共享cookiecache、密钥等,而是不同configuration都需要单独设置。

这块网上很多人理解都是错的,并没有真的在项目里使用或者没有留意过,如和其他人有出入,以我为准。

@property (class, readonly, strong) NSURLSessionConfiguration *ephemeralSessionConfiguration;

创建临时的configuration,通过这种方式创建的对象,和普通的对象主要区别在于URLCacheURLCredentialStorageHTTPCookieStorage上面。同样的,Ephemeral也不是单例方法,而只是类方法。


  1. URLCredentialStorage

  2. Ephemeral <__NSCFMemoryURLCredentialStorage: 0x600001bc8320>

  3. HTTPCookieStorage

  4. Ephemeral <NSHTTPCookieStorage cookies count:0>

如果对Ephemeral方式创建的config进行打印的话,可以看到变量类型明显区别于其他类型,并且在打印信息前面会有Ephemeral的标示。通过Ephemeral的方式创建的config,不会产生持久化信息,可以很好保护请求的数据安全性。

+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier;

identifier方式一般用于恢复之前的任务,主要用于下载。如果一个下载任务正在进行中,程序被kill调,可以在程序退出之前保存identifier。下次进入程序后通过identifier恢复之前的任务,系统会将NSURLSessionNSURLSessionConfiguration和之前的下载任务进行关联,并继续之前的任务。

timeout

timeoutIntervalForRequest

设置session请求间的超时时间,这个超时时间并不是请求从开始到结束的时间,而是两个数据包之间的时间间隔。当任意请求返回后这个值将会被重置,如果在超时时间内未返回则超时。单位为秒,默认为60秒。

timeoutIntervalForResource

资源超时时间,一般用于上传或下载任务,在上传或下载任务开始后计时,如果到达时间任务未结束,则删除资源文件。单位为秒,默认时间是七天。

资源共享

如果是相同的NSURLSessionConfiguration对象,会共享请求头、缓存、cookieCredential,通过Configuration创建的NSURLSession,也会拥有对应的请求信息。

@property (nullable, copy) NSDictionary *HTTPAdditionalHeaders;

公共请求头,默认是空的,设置后所有经Confuguration配置的NSURLSession,请求头都会带有设置的信息。

@property (nullable, retain) NSHTTPCookieStorage *HTTPCookieStorage;

HTTP请求的Cookie管理器。如果是通过sharedSessionbackgroundConfiguration创建的NSURLSession,默认使用sharedHTTPCookieStorageCookie数据。如果不想使用Cookie,则直接设置为nil即可,也可以手动设置为自己的CookieStorage

@property (nullable, retain) NSURLCredentialStorage *URLCredentialStorage;

证书管理器。如果是通过sharedSessionbackgroundConfiguration创建的NSURLSession,默认使用sharedCredentialStorage的证书。如果不想使用证书,可以直接设置为nil,也可以自己创建证书管理器。

@property (nullable, retain) NSURLCache *URLCache;

请求缓存,如果不手动设置的话为nil,NSURLCache是系统提供的对request-response的缓存,包括内存缓存+磁盘缓存,可以指定响应的大小,可以应对80%的网络请求缓存了, https://blog.csdn.net/u014600626/article/details/100159742。

缓存处理

NSURLRequest中可以设置cachePolicy请求缓存策略,这里不对具体值做详细描述,默认值为NSURLRequestUseProtocolCachePolicy使用缓存。

NSURLSessionConfiguration可以设置处理缓存的对象,我们可以手动设置自定义的缓存对象,如果不设置的话,默认使用系统的sharedURLCache单例缓存对象。经过configuration创建的NSURLSession发出的请求,NSURLRequest都会使用这个NSURLCache来处理缓存。

@property (nullable, retain) NSURLCache *URLCache;

NSURLCache提供了MemoryDisk的缓存,在创建时需要为其分别指定MemoryDisk的大小,以及存储的文件位置。使用NSURLCache不用考虑磁盘空间不够,或手动管理内存空间的问题,如果发生内存警告系统会自动清理内存空间。但是NSURLCache提供的功能非常有限,项目中一般很少直接使用它来处理缓存数据,还是用数据库比较多。


  1. [[NSURLCache alloc] initWithMemoryCapacity:30 * 1024 * 1024

  2. diskCapacity:30 * 1024 * 1024

  3. directoryURL:[NSURL URLWithString:filePath]];

使用NSURLCache还有一个好处,就是可以由服务端来设置资源过期时间,在请求服务端后,服务端会返回Cache-Control来说明文件的过期时间。NSURLCache会根据NSURLResponse来自动完成过期时间的设置。

最大连接数

限制NSURLSession的最大连接数,通过此方法创建的NSURLSession和服务端的最大连接数量不会超出这里设置的数量。苹果为我们设置的iOS端默认为4,Mac端默认为6。

@property NSInteger HTTPMaximumConnectionsPerHost;

连接复用

HTTP是基于传输层协议TCP的,通过TCP发送网络请求都需要先进行三次握手,建立网络请求后再发送数据,请求结束时再经历四次挥手。HTTP1.0开始支持keep-alivekeep-alive可以保持已经建立的链接,如果是相同的域名,在请求连接建立后,后面的请求不会立刻断开,而是复用现有的连接。从HTTP1.1开始默认开启keep-alive

请求是在请求头中设置下面的参数,服务器如果支持keep-alive的话,响应客户端请求时,也会在响应头中加上相同的字段。

Connection: Keep-Alive

如果想断开keep-alive,可以在请求头中加上下面的字段,但一般不推荐这么做。

Connection: Close

如果通过NSURLSession来进行网络请求的话,需要使用同一个NSURLSession对象,如果创建新的session对象则不能复用之前的链接。keep-alive可以保持请求的连接,苹果允许在iOS上最大保持有4个连接,Mac则是6个连接。

pipeline

pipeline

HTTP1.1中,基于keep-alive,还可以将请求进行管线化。和相同后端服务,TCP层建立的链接,一般都需要前一个请求返回后,后面的请求再发出。但pipeline就可以不依赖之前请求的响应,而发出后面的请求。

pipeline依赖客户端和服务器都有实现,服务端收到客户端的请求后,要按照先进先出的顺序进行任务处理和响应。pipeline依然存在之前非pipeline的问题,就是前面的请求如果出现问题,会阻塞当前连接影响后面的请求。

pipeline对于请求大文件并没有提升作用,只是对于普通请求速度有提升。在NSURLSessionConfiguration中可以设置HTTPShouldUsePipeliningYES,开启管线化,此属性默认为NO

NSURLSessionTaskMetrics

在日常开发过程中,经常遇到页面加载太慢的问题,这很大一部分原因都是因为网络导致的。所以,查找网络耗时的原因并解决,就是一个很重要的任务了。苹果对于网络检查提供了NSURLSessionTaskMetrics类来进行检查,NSURLSessionTaskMetrics是对应NSURLSessionTaskDelegate的,每个task结束时都会回调下面的方法,并且可以获得一个metrics对象。


  1. - (void)URLSession:(NSURLSession *)session

  2. task:(NSURLSessionTask *)task

  3. didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics;

NSURLSessionTaskMetrics可以很好的帮助我们分析网络请求的过程,以找到耗时原因。除了这个类之外,NSURLSessionTaskTransactionMetrics类中承载了更详细的数据。

@property (copy, readonly) NSArray<NSURLSessionTaskTransactionMetrics *> *transactionMetrics;

transactionMetrics数组中每一个元素都对应着当前task的一个请求,一般数组中只会有一个元素,如果发生重定向等情况,可能会存在多个元素。

@property (copy, readonly) NSDateInterval *taskInterval;

taskInterval记录了当前task从开始请求到最后完成的总耗时,NSDateInterval中包含了startDateendDateduration耗时时间。

@property (assign, readonly) NSUInteger redirectCount;

redirectCount记录了重定向次数,在进行下载请求时一般都会进行重定向,来保证下载任务能由后端最合适的节点来处理。

NSURLSessionTaskTransactionMetrics

NSURLSessionTaskTransactionMetrics中的属性都是用来做统计的,功能都是记录某个值,并没有逻辑上的意义。所以这里就对一些主要的属性做一下解释,基本涵盖了大部分属性,其他就不管了。

这张图是我从网上扒下来的,标示了NSURLSessionTaskTransactionMetrics的属性在请求过程中处于什么位置。

请求耗时细节


  1. // 请求对象

  2. @property (copy, readonly) NSURLRequest *request;

  3. // 响应对象,请求失败可能会为nil

  4. @property (nullable, copy, readonly) NSURLResponse *response;

  5. // 请求开始时间

  6. @property (nullable, copy, readonly) NSDate *fetchStartDate;

  7. // DNS解析开始时间

  8. @property (nullable, copy, readonly) NSDate *domainLookupStartDate;

  9. // DNS解析结束时间,如果解析失败可能为nil

  10. @property (nullable, copy, readonly) NSDate *domainLookupEndDate;

  11. // 开始建立TCP连接时间

  12. @property (nullable, copy, readonly) NSDate *connectStartDate;

  13. // 结束建立TCP连接时间

  14. @property (nullable, copy, readonly) NSDate *connectEndDate;

  15. // 开始TLS握手时间

  16. @property (nullable, copy, readonly) NSDate *secureConnectionStartDate;

  17. // 结束TLS握手时间

  18. @property (nullable, copy, readonly) NSDate *secureConnectionEndDate;

  19. // 开始传输请求数据时间

  20. @property (nullable, copy, readonly) NSDate *requestStartDate;

  21. // 结束传输请求数据时间

  22. @property (nullable, copy, readonly) NSDate *requestEndDate;

  23. // 接收到服务端响应数据时间

  24. @property (nullable, copy, readonly) NSDate *responseStartDate;

  25. // 服务端响应数据传输完成时间

  26. @property (nullable, copy, readonly) NSDate *responseEndDate;

  27. // 网络协议,例如http/1.1

  28. @property (nullable, copy, readonly) NSString *networkProtocolName;

  29. // 请求是否使用代理

  30. @property (assign, readonly, getter=isProxyConnection) BOOL proxyConnection;

  31. // 是否复用已有连接

  32. @property (assign, readonly, getter=isReusedConnection) BOOL reusedConnection;

  33. // 资源标识符,表示请求是从Cache、Push、Network哪种类型加载的

  34. @property (assign, readonly) NSURLSessionTaskMetricsResourceFetchType resourceFetchType;

  35. // 本地IP

  36. @property (nullable, copy, readonly) NSString *localAddress;

  37. // 本地端口号

  38. @property (nullable, copy, readonly) NSNumber *localPort;

  39. // 远端IP

  40. @property (nullable, copy, readonly) NSString *remoteAddress;

  41. // 远端端口号

  42. @property (nullable, copy, readonly) NSNumber *remotePort;

  43. // TLS协议版本,如果是http则是0x0000

  44. @property (nullable, copy, readonly) NSNumber *negotiatedTLSProtocolVersion;

  45. // 是否使用蜂窝数据

  46. @property (readonly, getter=isCellular) BOOL cellular;

下面是我发起一个http的下载请求,统计得到的数据。设备是Xcode模拟器,网络环境是WiFi


  1. (Request) <NSURLRequest: 0x600000c80380> { URL: http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4 }

  2. (Response) <NSHTTPURLResponse: 0x600000ed9420> { URL: http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4 } { Status Code: 200, Headers {

  3. "Accept-Ranges" = (

  4. bytes

  5. );

  6. Age = (

  7. 1063663

  8. );

  9. "Ali-Swift-Global-Savetime" = (

  10. 1575358696

  11. );

  12. Connection = (

  13. "keep-alive"

  14. );

  15. "Content-Length" = (

  16. 20472584

  17. );

  18. "Content-Md5" = (

  19. "YM+JxIH9oLH6l1+jHN9pmQ=="

  20. );

  21. "Content-Type" = (

  22. "video/mp4"

  23. );

  24. Date = (

  25. "Tue, 03 Dec 2019 07:38:16 GMT"

  26. );

  27. EagleId = (

  28. dbee142415764223598843838e

  29. );

  30. Etag = (

  31. "\"60CF89C481FDA0B1FA975FA31CDF6999\""

  32. );

  33. "Last-Modified" = (

  34. "Fri, 31 Mar 2017 01:41:36 GMT"

  35. );

  36. Server = (

  37. Tengine

  38. );

  39. "Timing-Allow-Origin" = (

  40. "*"

  41. );

  42. Via = (

  43. "cache39.l2et2[0,200-0,H], cache6.l2et2[3,0], cache16.cn548[0,200-0,H], cache16.cn548[1,0]"

  44. );

  45. "X-Cache" = (

  46. "HIT TCP_MEM_HIT dirn:-2:-2"

  47. );

  48. "X-M-Log" = (

  49. "QNM:xs451;QNM3:71"

  50. );

  51. "X-M-Reqid" = (

  52. "m0AAAP__UChjzNwV"

  53. );

  54. "X-Oss-Hash-Crc64ecma" = (

  55. 12355898484621380721

  56. );

  57. "X-Oss-Object-Type" = (

  58. Normal

  59. );

  60. "X-Oss-Request-Id" = (

  61. 5DE20106F3150D38305CE159

  62. );

  63. "X-Oss-Server-Time" = (

  64. 130

  65. );

  66. "X-Oss-Storage-Class" = (

  67. Standard

  68. );

  69. "X-Qnm-Cache" = (

  70. Hit

  71. );

  72. "X-Swift-CacheTime" = (

  73. 2592000

  74. );

  75. "X-Swift-SaveTime" = (

  76. "Sun, 15 Dec 2019 15:05:37 GMT"

  77. );

  78. } }

  79. (Fetch Start) 2019-12-15 15:05:59 +0000

  80. (Domain Lookup Start) 2019-12-15 15:05:59 +0000

  81. (Domain Lookup End) 2019-12-15 15:05:59 +0000

  82. (Connect Start) 2019-12-15 15:05:59 +0000

  83. (Secure Connection Start) (null)

  84. (Secure Connection End) (null)

  85. (Connect End) 2019-12-15 15:05:59 +0000

  86. (Request Start) 2019-12-15 15:05:59 +0000

  87. (Request End) 2019-12-15 15:05:59 +0000

  88. (Response Start) 2019-12-15 15:05:59 +0000

  89. (Response End) 2019-12-15 15:06:04 +0000

  90. (Protocol Name) http/1.1

  91. (Proxy Connection) NO

  92. (Reused Connection) NO

  93. (Fetch Type) Network Load

  94. (Request Header Bytes) 235

  95. (Request Body Transfer Bytes) 0

  96. (Request Body Bytes) 0

  97. (Response Header Bytes) 866

  98. (Response Body Transfer Bytes) 20472584

  99. (Response Body Bytes) 20472584

  100. (Local Address) 192.168.1.105

  101. (Local Port) 63379

  102. (Remote Address) 219.238.20.101

  103. (Remote Port) 80

  104. (TLS Protocol Version) 0x0000

  105. (TLS Cipher Suite) 0x0000

  106. (Cellular) NO

  107. (Expensive) NO

  108. (Constrained) NO

  109. (Multipath) NO

FAQ

NSURLSession的delegate为什么是强引用?

在初始化NSURLSession对象并设置代理后,代理对象将会被强引用。根据苹果官方的注释来看,这个强持有并不会一直存在,而是在调用URLSession:didBecomeInvalidWithError:方法后,会将delegate释放。

通过调用NSURLSessioninvalidateAndCancelfinishTasksAndInvalidate方法,即可将强引用断开并执行didBecomeInvalidWithError:代理方法,执行完成后session就会无效不可以使用。也就是只有在session无效时,才可以解除强引用的关系。

有时候为了保证连接复用等问题,一般不会轻易将session会话invalid,所以最好不要直接使用NSURLSession,而是要对其进行一次二次封装,使用AFN3.0的原因之一也在于此。

NSURLSession的上传下载

文件上传

表单上传

客户端有时候需要给服务端上传大文件,进行大文件肯定不能全都加载到内存里,一口气都传给服务器。进行大文件上传时,一般都会对需要上传的文件进行分片,分片后逐个文件进行上传。需要注意的是,分片上传和断点续传并不是同一个概念,上传并不支持断点续传。

进行分片上传时,需要对本地文件进行读取,我们使用NSFileHandle来进行文件读取。NSFileHandle提供了一个偏移量的功能,我们可以将handle的当前读取位置seek到上次读取的位置,并设置本次读取长度,读取的文件就是我们指定文件的字节。


  1. - (NSData *)readNextBuffer {

  2. if (self.maxSegment <= self.currentIndex) {

  3. return nil;

  4. }

  5. if(!self.fileHandler){

  6. NSString *filePath = [self uploadFile];

  7. NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];

  8. self.fileHandler = fileHandle;

  9. }

  10. [self.fileHandler seekToFileOffset:(self.currentIndex) * self.segmentSize];

  11. NSData *data = [self.fileHandler readDataOfLength:self.segmentSize];

  12. return data;

  13. }

上传文件现在主流的方式,都是采取表单上传的方式,也就是multipart/from-dataAFNetworking对表单上传也有很有的支持。表单上传需要遵循下面的格式进行上传,boundary是一个16进制字符串,可以是任何且唯一的。boundary的功能用来进行字段分割,区分开不同的参数部分。

multipart/from-data规范定义在rfc2388,详细字段可以看一下规范。


  1. --boundary

  2. Content-Disposition: form-data; name="参数名"

  3. 参数值

  4. --boundary

  5. Content-Disposition:form-data;name=”表单控件名”;filename=”上传文件名”

  6. Content-Type:mime type

  7. 要上传文件二进制数据

  8. --boundary--

拼接上传文件基本上可以分为下面三部分,上传参数、上传信息、上传文件。并且通过UTF-8格式进行编码,服务端也采用相同的解码方式,则可以获得上传文件和信息。需要注意的是,换行符数量是固定的,这都是固定的协议格式,不要多或者少,会导致服务端解析失败。


  1. - (NSData *)writeMultipartFormData:(NSData *)data

  2. parameters:(NSDictionary *)parameters {

  3. if (data.length == 0) {

  4. return nil;

  5. }

  6. NSMutableData *formData = [NSMutableData data];

  7. NSData *lineData = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding];

  8. NSData *boundary = [kBoundary dataUsingEncoding:NSUTF8StringEncoding];

  9. // 拼接上传参数

  10. [parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {

  11. [formData appendData:boundary];

  12. [formData appendData:lineData];

  13. NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n%@", key, obj];

  14. [formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding]];

  15. [formData appendData:lineData];

  16. }];

  17. // 拼接上传信息

  18. [formData appendData:boundary];

  19. [formData appendData:lineData];

  20. NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\nContent-Type: %@", @"name", @"filename", @"mimetype"];

  21. [formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding]];

  22. [formData appendData:lineData];

  23. [formData appendData:lineData];

  24. // 拼接上传文件

  25. [formData appendData:data];

  26. [formData appendData:lineData];

  27. [formData appendData: [[NSString stringWithFormat:@"--%@--\r\n", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];

  28. return formData;

  29. }

除此之外,表单提交还需要设置请求头的Content-TypeContent-Length,否则会导致请求失败。其中Content-Length并不是强制要求的,要看后端的具体支持情况。

设置请求头时,一定要加上boundary,这个boundary和拼接上传文件的boundary需要是同一个。服务端从请求头拿到boundary,来解析上传文件。


  1. NSString *headerField = [NSString stringWithFormat:@"multipart/form-data; charset=utf-8; boundary=%@", kBoundary];

  2. [request setValue:headerField forHTTPHeaderField:@"Content-Type"];

  3. NSUInteger size = [[[NSFileManager defaultManager] attributesOfItemAtPath:uploadPath error:nil] fileSize];

  4. headerField = [NSString stringWithFormat:@"%lu", size];

  5. [request setValue:headerField forHTTPHeaderField:@"Content-Length"];

随后我们通过下面的代码创建NSURLSessionUploadTask,并调用resume发起请求,实现对应的代理回调即可。


  1. // 发起网络请求

  2. NSURLSessionUploadTask *uploadTask = [self.backgroundSession uploadTaskWithRequest:request fromData:fromData];

  3. [uploadTask resume];

  4. // 请求完成后调用,无论成功还是失败

  5. - (void)URLSession:(NSURLSession *)session

  6. task:(NSURLSessionTask *)task

  7. didCompleteWithError:(NSError *)error {

  8. }

  9. // 更新上传进度,会回调多次

  10. - (void)URLSession:(NSURLSession *)session

  11. task:(NSURLSessionTask *)task

  12. didSendBodyData:(int64_t)bytesSent

  13. totalBytesSent:(int64_t)totalBytesSent

  14. totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {

  15. }

  16. // 数据接收完成回调

  17. - (void)URLSession:(NSURLSession *)session

  18. dataTask:(NSURLSessionDataTask *)dataTask

  19. didReceiveData:(NSData *)data {

  20. }

  21. // 处理后台上传任务,当前session的上传任务结束后会回调此方法。

  22. - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {

  23. }

但是,如果你认为这就完成一个上传功能了,too young too simple~

后台上传

如果通过fromData的方式进行上传,并不支持后台上传。如果想实现后台上传,需要通过fromFile的方式上传文件。不止如此,fromData还有其他坑。


  1. - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;

  2. - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;

内存占用

我们发现通过fromData:的方式上传文件,内存涨上去之后一直不能降下来,无论是直接使用NSURLSession还是AFNetworking,都是这样的。小文件还好,不是很明显,如果是几百MB的大文件很明显就会有一个内存峰值,而且涨上去就不会降下来。WTF?

上传有两种方式上传,如果我们把fromData:的上传改为fromFile:,就可以解决内存不下降的问题。所以,我们可以把fromData:的上传方式,理解为UIImageimageNamed的方法,上传后NSData文件会保存在内存中,不会被回收。而fromFile:的方式是从本地加载文件,并且上传完成后可以被回收。而且如果想支持后台上传,就必须用fromFile:的方式进行上传。

OK,那找到问题我们就开干,改变之前的上传逻辑,改为fromFile:的方式上传。


  1. // 将分片写入到本地

  2. NSString *filePath = [NSString stringWithFormat:@"%@/%ld", [self segmentDocumentPath], currentIndex];

  3. BOOL write = [formData writeToFile:filePath atomically:YES];

  4. // 创建分片文件夹

  5. - (NSString *)segmentDocumentPath {

  6. NSString *documentName = [fileName md5String];

  7. NSString *filePath = [[SVPUploadCompressor compressorPath] stringByAppendingPathComponent:documentName];

  8. BOOL needCreateDirectory = YES;

  9. BOOL isDirectory = NO;

  10. if ([[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]) {

  11. if (isDirectory) {

  12. needCreateDirectory = NO;

  13. } else {

  14. [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];

  15. }

  16. }

  17. if (needCreateDirectory) {

  18. [[NSFileManager defaultManager] createDirectoryAtPath:filePath

  19. withIntermediateDirectories:YES

  20. attributes:nil

  21. error:nil];

  22. }

  23. return filePath;

  24. }

因为要通过fromFile:方法传一个本地分片的路径进去,所以需要预先对文件进行分片,并保存在本地。在分片的同时,还需要拼接boundary信息。

所以我们在上传任务开始前,先对文件进行分片并拼接信息,然后将分片文件写入到本地。为了方便管理,我们基于具有唯一性的文件名进行MD5来创建分片文件夹,分片文件命名通过下标来命名,并写入到本地。文件上传完成后,直接删除整个文件夹即可。当然,这些文件操作都是在异步线程中完成的,防止影响UI线程。

内存占用

我们用一个400MB的视频测试上传,我们可以从上图看出,圈红部分是我们上传文件的时间。将上传方式改为fromFile:后,上传文件的峰值最高也就是在10MB左右徘徊,这对于iPhone6这样的低内存老年机来说,是相当友好的,不会导致低端设备崩溃或者卡顿。

动态分片

用户在上传时网络环境会有很多情况,WiFi、4G、弱网等很多情况。如果上传分片太大可能会导致失败率上升,分片文件太小会导致网络请求太多,产生太多无用的boundaryheader、数据链路等资源的浪费。

为了解决这个问题,我们采取的是动态分片大小的策略。根据特定的计算策略,预先使用第一个分片的上传速度当做测速分片,测速分片的大小是固定的。根据测速的结果,对其他分片大小进行动态分片,这样可以保证分片大小可以最大限度的利用当前网速。


  1. if ([Reachability reachableViaWiFi]) {

  2. self.segmentSize = 500 * 1024;

  3. } else if ([Reachability reachableViaWWAN]) {

  4. self.segmentSize = 300 * 1024;

  5. }

当然,如果觉得这种分片方式太过复杂,也可以采取一种阉割版的动态分片策略。即根据网络情况做判断,如果是WiFi就固定某个分片大小,如果是流量就固定某个分片大小。然而这种策略并不稳定,因为现在很多手机的网速比WiFi还快,我们也不能保证WiFi都是百兆光纤。

并行上传

上传的所有任务如果使用的都是同一个NSURLSession的话,是可以保持连接的,省去建立和断开连接的消耗。在iOS平台上,NSURLSession支持对一个Host保持4个连接,所以,如果我们采取并行上传,可以更好的利用当前的网络。

并行上传的数量在iOS平台上不要超过4个,最大连接数是可以通过NSURLSessionConfiguration设置的,而且数量最好不要写死。同样的,应该基于当前网络环境,在上传任务开始的时候就计算好最大连接数,并设置给Configuration

经过我们的线上用户数据分析,在线上环境使用并行任务的方式上传,上传速度相较于串行上传提升四倍左右。计算方式是每秒文件上传的大小。


  1. iPhone串行上传:715 kb/s

  2. iPhone并行上传:2909 kb/s

队列管理

分片上传过程中可能会因为网速等原因,导致上传失败。失败的任务应该由单独的队列进行管理,并且在合适的时机进行失败重传。

例如对一个500MB的文件进行分片,每片是300KB,就会产生1700多个分片文件,每一个分片文件就对应一个上传任务。如果在进行上传时,一口气创建1700多个uploadTask,尽管NSURLSession是可以承受的,也不会造成一个很大的内存峰值。但是我觉得这样并不太好,实际上并不会同时有这么多请求发出。


  1. /// 已上传成功片段数组

  2. @property (nonatomic, strong) NSMutableArray *successSegments;

  3. /// 待上传队列的数组

  4. @property (nonatomic, strong) NSMutableArray *uploadSegments;

所以在创建上传任务时,我设置了一个最大任务数,就是同时向NSURLSession发起的请求不会超过这个数量。需要注意的是,这个最大任务数是我创建uploadTask的任务数,并不是最大并发数,最大并发数由NSURLSession来控制,我不做干预。

我将待上传任务都放在uploadSegments中,上传成功后我会从待上传任务数组中取出一条或多条,并保证同时进行的任务始终不超过最大任务数。失败的任务理论上来说也是需要等待上传的,所以我把失败任务也放在uploadSegments中,插入到队列最下面,这样就保证了待上传任务完成后,继续重试失败任务。

成功的任务我放在successSegments中,并且始终保持和uploadSegments没有交集。两个队列中保存的并不是uploadTask,而是分片的索引,这也就是为什么我给分片命名的时候用索引当做名字的原因。当successSegments等于分片数量时,就表示所有任务上传完成。

文件下载

NSURLSession是在单独的进程中运行,所以通过此类发起的网络请求,是独立于应用程序运行的,即使App挂起、kill也不会停止请求。在下载任务时会比较明显,即便App被kill下载任务仍然会继续,并且允许下次启动App使用这次的下载结果或继续下载。

和上传代码一样,创建下载任务很简单,通过NSURLSession创建一个downloadTask,并调用resume即可开启一个下载任务。


  1. NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];

  2. NSURLSession *session = [NSURLSession sessionWithConfiguration:config

  3. delegate:self

  4. delegateQueue:[NSOperationQueue mainQueue]];

  5. NSURL *url = [NSURL URLWithString:@"http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4"];

  6. NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];

  7. NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];

  8. [downloadTask resume];

我们可以调用suspend将下载任务挂起,随后调用resume方法继续下载任务,suspendresume需要是成对的。但是suspend挂起任务是有超时的,默认为60s,如果超时系统会将TCP连接断开,我们再调用resume是失效的。可以通过NSURLSessionConfigurationtimeoutIntervalForResource来设置上传和下载的资源耗时。suspend只针对于下载任务,其他任务挂起后将会重新开始。

下面两个方法是下载比较基础的方法,分别用来接收下载进度和下载完的临时文件地址。didFinishDownloadingToURL:方法是required,当下载结束后下载文件被写入在Library/Caches下的一个临时文件,我们需要将此文件移动到自己的目录,临时目录在未来的一个时间会被删掉。


  1. // 从服务器接收数据,下载进度回调

  2. - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask

  3. didWriteData:(int64_t)bytesWritten

  4. totalBytesWritten:(int64_t)totalBytesWritten

  5. totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {

  6. CGFloat progress = (CGFloat)totalBytesWritten / (CGFloat)totalBytesExpectedToWrite;

  7. self.progressView.progress = progress;

  8. }

  9. // 下载完成后回调

  10. - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask

  11. didFinishDownloadingToURL:(NSURL *)location {

  12. }

断点续传

HTTP协议支持断点续传操作,在开始下载请求时通过请求头设置Range字段,标示从什么位置开始下载。

Range:bytes=512000-

服务端收到客户端请求后,开始从512kb的位置开始传输数据,并通过Content-Range字段告知客户端传输数据的起始位置。

Content-Range:bytes 512000-/1024000

downloadTask任务开始请求后,可以调用cancelByProducingResumeData:方法可以取消下载,并且可以获得一个resumeDataresumeData中存放一些断点下载的信息。可以将resumeData写到本地,后面通过这个文件可以进行断点续传。


  1. NSString *library = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;

  2. NSString *resumePath = [library stringByAppendingPathComponent:[self.downloadURL md5String]];

  3. [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {

  4. [resumeData writeToFile:resumePath atomically:YES];

  5. }];

在创建下载任务前,可以判断当前任务有没有之前待恢复的任务,如果有的话调用downloadTaskWithResumeData:方法并传入一个resumeData,可以恢复之前的下载,并重新创建一个downloadTask任务。


  1. NSString *library = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;

  2. NSString *resumePath = [library stringByAppendingPathComponent:[self.downloadURL md5String]];

  3. NSData *resumeData = [[NSData alloc] initWithContentsOfFile:resumePath];

  4. self.downloadTask = [self.session downloadTaskWithResumeData:resumeData];

  5. [self.downloadTask resume];

通过suspendresume这种方式挂起的任务,downloadTask是同一个对象,而通过cancel然后resumeData恢复的任务,会创建一个新的downloadTask任务。

当调用downloadTaskWithResumeData:方法恢复下载后,会回调下面的方法。回调参数fileOffset是上次文件的下载大小,expectedTotalBytes是预估的文件总大小。


  1. - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask

  2. didResumeAtOffset:(int64_t)fileOffset

  3. expectedTotalBytes:(int64_t)expectedTotalBytes;

后台下载

通过backgroundSessionConfigurationWithIdentifier方法创建后台上传或后台下载类型的NSURLSessionConfiguration,并且设置一个唯一标识,需要保证这个标识在不同的session之间的唯一性。后台任务只支持httphttps的任务,其他协议的任务并不支持。


  1. NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"identifier"];

  2. [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];

通过backgroundSessionConfigurationWithIdentifier方法创建的NSURLSession,请求任务将会在系统的单独进程中进行,因此即使App进程被kill也不受影响,依然可以继续执行请求任务。如果程序被系统kill调,下次启动并执行didFinishLaunchingWithOptions可以通过相同的identifier创建NSURLSessionNSURLSessionConfiguration,系统会将新创建的NSURLSession和单独进程中正在运行的NSURLSession进行关联。

在程序启动并执行didFinishLaunchingWithOptions方法时,按照下面方法创建NSURLSession即可将新创建的Session和之前的Session绑定,并自动开始执行之前的下载任务。恢复之前的任务后会继续执行NSURLSession的代理方法,并执行后面的任务。


  1. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  2. NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"identifier"];

  3. [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];

  4. return YES;

  5. }

当应用进入到后台时,可以继续下载,如果客户端没有开启Background Mode,则不会回调客户端进度。下次进入前台时,会继续回调新的进度。

如果在后台下载完成,则会通过AppDelegate的回调方法通知应用来刷新UI。由于下载是在一个单独的进程中完成的,即便业务层代码会停止执行,但下载的回调依然会被调用。在回调时,允许用户处理业务逻辑,以及刷新UI。

调用此方法后可以开始刷新UI,调用completionHandler表示刷新结束,所以上层业务要做一些控制逻辑。didFinishDownloadingToURL的调用时机会比此方法要晚,依然在那个方法里可以判断下载文件。由于项目中可能会存在多个下载任务,所以需要通过identifier对下载任务进行区分。


  1. - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {

  2. ViewController *vc = (ViewController *)self.window.rootViewController;

  3. vc.completionHandler = completionHandler;

  4. }

需要注意的是,如果存在多个相同名字的identifier任务,则创建的session会将同名的任务都继续执行。NSURLSessionConfiguration还提供下面的属性,在session下载任务完成时是否启动App,默认为YES,如果设置为NO则后台下载会受到影响。

@property BOOL sessionSendsLaunchEvents;

后台下载过程中会设计到一系列的代理方法调用,下面是调用顺序。

后台下载时序图

视频文件下载

现在很多视频类App都有视频下载的功能,视频下载肯定不会是单纯的把一个mp4下载下来就可以,这里就讲一下视频下载相关的知识。

  1. 视频地址一般都是从服务端获取的,所以需要先请求接口获取下载地址。这个地址可以是某个接口就已经请求下来的,也可以是某个固定格式拼接的。
  2. 现在有很多视频App都是有免流服务的,例如腾讯大王卡、蚂蚁宝卡之类的,免流服务的本质就是对m3u8tsmp4地址重新包一层,请求数据的时候直接请求运营商给的地址,运营商对数据做了一个中转操作。
  3. 以流视频m3u8为例,有了免流地址,先下载m3u8文件。这个文件一般都是加密的,下载完成后客户端会对m3u8文件进行decode,获取到真正的m3u8文件。
  4. m3u8文件本质上是ts片段的集合,视频播放播的还是ts片段。随后对m3u8文件进行解析,获取到ts片段地址,并将ts下载地址转成免流地址后逐个下载,也可以并行下载。
  5. m3u8文件下载后会以固定格式存在文件夹下,文件夹对应被缓存的视频。ts片命名以数字命名,例如0.ts,下标从0开始。
  6. 所有ts片段下载完成后,生成本地m3u8文件。
  7. m3u8文件分为远端和本地两种,远端的就是正常下载的地址,本地m3u8文件是在播放本地视频的时候传入。格式和普通m3u8文件差不多,区别在于ts地址是本地地址,例如下面的地址。

  1. #EXTM3U

  2. #EXT-X-TARGETDURATION:30

  3. #EXT-X-VERSION:3

  4. #EXTINF:9.28,

  5. 0.ts

  6. #EXTINF:33.04,

  7. 1.ts

  8. #EXTINF:30.159,

  9. 2.ts

  10. #EXTINF:23.841,

  11. 3.ts

  12. #EXT-X-ENDLIST

m3u8文件

HLS(Http Live Streaming)是苹果推出的流媒体协议,其中包含两部分,m3u8文件和ts文件。使用ts文件的原因是因为多个ts可以无缝拼接,并且单个ts可以单独播放。而mp4由于格式原因,被分割的mp4文件单独播放会导致画面撕裂或者音频缺失的问题。如果单独下载多个mp4文件,播放时会导致间断的问题。

m3u8Unicode版本的m3u,是苹果推出的一种视频格式,是一个基于HTTP的流媒体传输协议。m3u8协议将一个媒体文件切为多个小文件,并利用HTTP协议进行数据传输,小文件所在的资源服务器路径存储在.m3u8文件中。客户端拿到m3u8文件,即可根据文件中资源文件的路径,分别下载不同的文件。

m3u8文件必须是utf-8格式编码的,在文件中以#EXT开头的是标签,并且大小写敏感。以#开头的其他字符串则都会被认为是注释。m3u8分为点播和直播,点播在第一次请求.m3u8文件后,将下载下来的ts片段进行顺序播放即可。直播则需要过一段时间对.m3u8文件进行一个增量下载,并继续下载后续的ts文件。

m3u8中有很多标签,下面是项目中用到的一些标签或主要标签。将mp4或者flv文件进行切片很简单,直接用ffmpeg命令切片即可。

  • 起始标签,此标签必须在整个文件的开头。

#EXTM3U

  • 结束标签,此标签必须在整个文件的末尾。

#EXT-X-ENDLIST

  • 当前文件版本,如果不指定则默认为1

#EXT-X-VERSION

  • 所有ts片段最大时长。

#EXT-X-TARGETDURATION

  • 当前ts片段时长。

#EXTINF

如果没有#EXT或#开头的,一般都是ts片段下载地址。路径可以是绝对路径,也可以是相对路径,我们项目里使用的是绝对路径。但相对路径数据量会相对比较小,只不过看视频的人网速不会太差。

下面是相对路径地址,文件中只有segment1.ts,则表示相对于m3u8的路径,也就是下面的路径。


  1. https://data.vod.itc.cn/m3u8

  2. https://data.vod.itc.cn/segment1.ts

常见错误

A background URLSession with identifier backgroundSession already exists

如果重复后台已经存在的下载任务,会提示这个错误。需要在页面退出或程序退出时,调用finishTasksAndInvalidate方法将任务invalidate


  1. [[NSNotificationCenter defaultCenter] addObserver:self

  2. selector:@selector(willTerminateNotification)

  3. name:UIApplicationWillTerminateNotification

  4. object:nil];

  5. - (void)willTerminateNotification {

  6. [self.session getAllTasksWithCompletionHandler:^(NSArray<__kindof NSURLSessionTask *> * _Nonnull tasks) {

  7. if (tasks.count) {

  8. [self.session finishTasksAndInvalidate];

  9. }

  10. }];

  11. }

NSURLSession最全学习攻略笔记相关推荐

  1. 全网最全学习攻略【尚硅谷电影推荐系统】附视频代码链接

    简述 因为设计任务是开发一款图书推荐系统,但是没有现成的系统开发讲解,于是从网上找到了尚硅谷电影推荐系统的开发教程. 从配置虚拟机到开发各种推荐功能共耗时一个月左右,小破站里的视频教程很多但是有的是武 ...

  2. LINUX SHELL脚本攻略笔记[速查]

    LINUX SHELL脚本攻略笔记[速查] Linux Shell脚本攻略笔记[速查] 资源 shell script run shell script echo printf 环境变量和变量 pgr ...

  3. 别再找了!全网最全的数据分析全流程攻略在这

    试想这样一个场景: 领导说:"你去建材市场帮我买些配件."你顶着烈日跑遍大小市场,但领导问你:"为何选这家?"你却答不上来. 你没努力吗?努力了.但有成效吗?至 ...

  4. 传图识字java_Java:全面 清晰的 NIO 学习攻略

    Java:全面 & 清晰的 NIO 学习攻略 发布时间:2018-11-19 09:19, 浏览次数:202 , 标签: Java NIO <>前言 * JDK 1.4后,Java ...

  5. 变换例题_小学语文学习攻略9:句式变换知识点概述+例题讲练

    很多家长都会遇到这样的情况:孩子的语文考试卷发下来,作文上老师的评语或者做的记号上总会有几个是句意不通.在平常的交流中也是,支吾了半天也不能完整.通顺的表达自己的意思.这都是孩子在造句方面有障碍!孩子 ...

  6. Java 后端开发面试总结:25 个技术专题(最全面试攻略)

    另送福利: java 面试准备 准确的说这里又分为两部分: 1.Java 刷题 2.算法刷题 Java 刷题:此份文档详细记录了千道面试题与详解:  !     私信我回复[03]即可免费获取 很多人 ...

  7. 后端学习攻略,助你打怪升级

    大家好,我是辣条,好久不见,甚是想念,今天给大家带来一份后端的学习攻略,希望能帮助到你. 目录 一. 数据结构与算法 为什么数据结构很重要? 数据结构与算法相关数据推荐 视频推荐 二.计算机网络 为什 ...

  8. 中国式家长怎么学计算机,中国式家长前期怎么学习 中国式家长前期学习攻略...

    中国式家长是一款全新模拟养成手游,讲述的是中国传统式的教育,每个父母都希望自己子女能够成才,非常有趣的一款游戏哦,欢迎各位玩家前来下载体验哦!! 类型:休闲益智 大小:44.10 M 语言:简体中文 ...

  9. GoLang 学习攻略

    2019独角兽企业重金招聘Python工程师标准>>> GoLang 学习攻略  Daemon_Shell 关注 2018.06.02 11:04 字数 1180 阅读 1432评论 ...

最新文章

  1. Python 图像处理篇-利用opencv库和numpy库读取包含中文路径下的本地图片实例演示
  2. [转载] 你真的会用 Java 中的三目运算符吗
  3. 报告PPT|Python编程之美(45页)
  4. ASP.NET程序中 抛出Thread was being aborted. 异常(转)
  5. 【光学】基于matlab像面数字全息离轴干涉模拟【含Matlab源码 215期】
  6. 如何从Linux里面下载软件
  7. 使用element插件中Descriptions遇到的坑
  8. Screw一键生成数据库文档工具
  9. Microsoft VBScript 编译器错误 #x27;800a0408#x27; 无效字符 高手来来来,感激不尽
  10. deepin提取DSDT
  11. SSM项目实例——简易版图书管理系统
  12. ESP32 nvs 加密
  13. WebRTC--添加IOCP网络模型支持
  14. python123用户登录c_写代码: 实现用户输入用户名和密码,当用户名为seven且密码为123时,显示登录成功,否则登录失败。...
  15. ios 在window和mac上另类打包方式
  16. 汽车电子之Infineon车规级芯片
  17. nodejs+vue旅游网站设计
  18. 【从0到1搭建LoRa物联网】14、低成本单通道网关(二)
  19. 赋范线性空间上的有界线性泛函
  20. OpenGL教程翻译 第二十三课 阴影贴图(一)

热门文章

  1. 写给未毕业或已毕业的弟妹的寄语
  2. shell 删除simatic_“Simatic Shell”作为 WinCC 的一个组件,它的作用是什么?-工业支持中心-西门子中国...
  3. 神经网络环境python2.7+tensorflow(gpu)+keras+cuda toolkit8.0+cudnn5.1+anaconda(ubuntu 16.04desktop64位)
  4. Source Insight 4 使用 quicker.em
  5. 【linux】Ubuntu20.04使用apt安装tomcat9
  6. windows在命令行窗口如何进入指定盘符目录
  7. 学院集群安装pytorch环境
  8. 计算机跨界之科技金融
  9. CSS小球下落回弹动画效果
  10. bootstrap之按钮大小+图片大小调整