zoukankan      html  css  js  c++  java
  • iOS之网络请求NSURLSession剖析

    2013年的WWDC大会上,苹果推出了NSURLSession,对Foundation URL加载系统进行了彻底的重构,提供了更丰富的API来处理网络请求,如:支持http2.0协议、直接把数据下载到磁盘、同一session发送多个请求、下载是多线程异步处理和提供全局的session并可以统一配置等等,提高了NSURLSession的易用性、灵活性,更加地适合移动开发的需求。


    NSURLSession的介绍

    1. session类型

    Default session

    +defaultSessionConfiguration 返回一个标准的 configuration,这个配置实际上与 NSURLConnection 的网络堆栈(networking stack)是一样的,具有相同的共享 NSHTTPCookieStorage,共享 NSURLCache 和共享 NSURLCredentialStorage

    Ephemeral session

    +ephemeralSessionConfiguration 返回一个预设配置,这个配置中不会对缓存Cookie 和证书进行持久性的存储,这对于实现像秘密浏览这种功能来说是很理想的。

    Background session

    +backgroundSessionConfiguration:(NSString *)identifier 的独特之处在于,它会创建一个后台 session。后台 session 不同于常规的,普通的 session,它甚至可以在应用程序挂起,退出或者崩溃的情况下进行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程。

    2. 配置属性

    基本配置

    HTTPAdditionalHeaders 指定了一组默认的可以设置请求(outbound request)的数据头。这对于跨 session 共享信息,如内容类型、语言、用户代理和身份认证,是很有用的。

    	// 设置请求的header
    	NSString *userPasswordString = [NSString stringWithFormat:@"%@:%@", user, password];
    	NSData * userPasswordData = [userPasswordString dataUsingEncoding:NSUTF8StringEncoding];
    	NSString *base64EncodedCredential = [userPasswordData base64EncodedStringWithOptions:0];
    	NSString *authString = [NSString stringWithFormat:@"Basic %@", base64EncodedCredential];
    	NSString *userAgentString = @"AppName/com.example.app (iPhone 5s; iOS 7.0.2; Scale/2.0)";
    
    	configuration.HTTPAdditionalHeaders = @{@"Accept": @"application/json",
                                            	@"Accept-Language": @"en",
                                            	@"Authorization": authString,
                                            	@"User-Agent": userAgentString};
    
    • networkServiceType 对标准的网络流量、网络电话、语音、视频,以及由一个后台进程使用的流量进行了区分。大多数应用程序都不需要设置这个。
    • allowsCellularAccessdiscretionary 被用于节省通过蜂窝网络连接的带宽。对于后台传输的情况,推荐大家使用 discretionary 这个属性,而不是 allowsCellularAccess,因为前者会把 WiFi 和电源的可用性考虑在内。
    • timeoutIntervalForRequesttimeoutIntervalForResource 分别指定了对于请求和资源的超时间隔。许多开发人员试图使用 timeoutInterval 去限制发送请求的总时间,但其实它真正的含义是:分组(packet)之间的时间。实际上我们应该使用 timeoutIntervalForResource 来规定整体超时的总时间,但应该只将其用于后台传输,而不是用户实际上可能想要去等待的任何东西。
    • HTTPMaximumConnectionsPerHostFoundation 框架中 URL 加载系统的一个新的配置选项。它曾经被 NSURLConnection 用于管理私有的连接池。现在有了 NSURLSession,开发者可以在需要时限制连接到特定主机的数量。
    • HTTPShouldUsePipelining 这个属性在 NSMutableURLRequest 下也有,它可以被用于开启 HTTP 管线化(HTTP pipelining),这可以显着降低请求的加载时间,但是由于没有被服务器广泛支持,默认是禁用的。
    • sessionSendsLaunchEvents 是另一个新的属性,该属性指定该 session 是否应该从后台启动。
    • connectionProxyDictionary 指定了 session 连接中的代理服务器。同样地,大多数面向消费者的应用程序都不需要代理,所以基本上不需要配置这个属性。

    Cookie 策略

    • HTTPCookieStorage 存储了 session 所使用的 cookie。默认情况下会使用 NSHTTPCookieShorage+sharedHTTPCookieStorage 这个单例对象,这与 NSURLConnection 是相同的。
    • HTTPCookieAcceptPolicy 决定了什么情况下 session 应该接受从服务器发出的 cookie
    • HTTPShouldSetCookies 指定了请求是否应该使用 session 存储的 cookie,即 HTTPCookieSorage 属性的值。

    安全策略

    • URLCredentialStorage 存储了 session 所使用的证书。默认情况下会使用 NSURLCredentialStorage+sharedCredentialStorage 这个单例对象,这与 NSURLConnection 是相同的。
    • TLSMaximumSupportedProtocolTLSMinimumSupportedProtocol 确定 `session 是否支持 SSL 协议。

    缓存策略

    • URLCachesession 使用的缓存。默认情况下会使用 NSURLCache+sharedURLCache 这个单例对象,这与 NSURLConnection 是相同的。
    • requestCachePolicy 指定了一个请求的缓存响应应该在什么时候返回。这相当于 NSURLRequest-cachePolicy 方法。

    自定义协议

    protocolClasses 用来配置特定某个 session 所使用的自定义协议(该协议是 NSURLProtocol 的子类)的数组。

    3. NSURLSessionTask

    NSURLsessionTask 是一个抽象类,其下有 3 个实体子类可以直接使用:NSURLSessionDataTaskNSURLSessionUploadTaskNSURLSessionDownloadTask。这 3 个子类封装了现代程序三个最基本的网络任务:获取数据,比如 JSON 或者 XML,上传文件和下载文件。

    图1

    不同于直接使用 alloc-init 初始化方法,task 是由一个 NSURLSession 创建的。每个 task 的构造方法都对应有或者没有 completionHandler 这个 block 的两个版本。

    4. 代理

    针对NSURLsessionTask的代理,根代理为NSURLSessionDelegate,其它的代理直接或者间接继承自改代理,如:NSURLSessionTaskDelegateNSURLSessionDataDelegateNSURLSessionDownloadDelegate。其中根代理NSURLSessionDelegate主要处理鉴权、后台下载任务完成通知等等,NSURLSessionTaskDelegate主要处理收到鉴权响应、任务结束(无论是正常还是异常),NSURLSessionDataDelegate处理数据的接收、dataTaskdownloadTask、缓存等,NSURLSessionDownloadDelegate主要处理数据下载、数据进度通知等。

    图2

    NSURLSession应用

    1. NSURLSessionDataTask 发送 GET 请求

    	//确定请求路径
    	NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login?username=520&pwd=520&type=JSON"];
    	//创建 NSURLSession 对象
     	NSURLSession *session = [NSURLSession sharedSession];
    
     /**
      根据对象创建 Task 请求,默认在子线程中解析数据
    
      url  方法内部会自动将 URL 包装成一个请求对象(默认是 GET 请求)
      completionHandler  完成之后的回调(成功或失败)
    
      param data     返回的数据(响应体)
      param response 响应头
      param error    错误信息
      */
     	NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:
                 ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
         	//解析服务器返回的数据
         	NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
     	}];
    	//发送请求(执行Task)
    	[dataTask resume];
    

    2. NSURLSessionDataTask 发送 POST 请求

    	//确定请求路径
     	NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login"];
     	//创建可变请求对象
     	NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];
     	//修改请求方法
     	requestM.HTTPMethod = @"POST";
     	//设置请求体
     	requestM.HTTPBody = [@"username=520&pwd=520&type=JSON" dataUsingEncoding:NSUTF8StringEncoding];
     	//创建会话对象
     	NSURLSession *session = [NSURLSession sharedSession];
     	//创建请求 Task
     	NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:requestM completionHandler:
                 ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
         	//解析返回的数据
         	NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
     	}];
     	//发送请求
     	[dataTask resume];
    

    3. NSURLSessionDataTask 设置代理发送请求

    	 //确定请求路径
    	 NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login"];
    	 //创建可变请求对象
    	 NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];
    	 //设置请求方法
    	 requestM.HTTPMethod = @"POST";
    	 //设置请求体
    	 requestM.HTTPBody = [@"username=520&pwd=520&type=JSON" dataUsingEncoding:NSUTF8StringEncoding];
    	 //创建会话对象,设置代理
     /**
      第一个参数:配置信息
      第二个参数:设置代理
      第三个参数:队列,如果该参数传递nil 那么默认在子线程中执行
      */
    	 NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
                                  delegate:self delegateQueue:nil];
    	 //创建请求 Task
    	 NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:requestM];
    	 //发送请求
    	 [dataTask resume];
    

    代理方法:

    -(void)URLSession:(NSURLSession *)session dataTask:(nonnull NSURLSessionDataTask *)dataTask 
    didReceiveResponse:(nonnull NSURLResponse *)response 
    completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler {
         //子线程中执行
         NSLog(@"接收到服务器响应的时候调用 -- %@", [NSThread currentThread]);
    
         self.dataM = [NSMutableData data];
         //默认情况下不接收数据
         //必须告诉系统是否接收服务器返回的数据
         completionHandler(NSURLSessionResponseAllow);
    }
    -(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    
         NSLog(@"接受到服务器返回数据的时候调用,可能被调用多次");
         //拼接服务器返回的数据
         [self.dataM appendData:data];
    }
    -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
         NSLog(@"请求完成或者是失败的时候调用");
         //解析服务器返回数据
         NSLog(@"%@", [[NSString alloc] initWithData:self.dataM encoding:NSUTF8StringEncoding]);
    }
    

    设置代理之后的强引用问题

    • NSURLSession 对象在使用的时候,如果设置了代理,那么 session 会对代理对象保持一个强引用,在合适的时候应该主动进行释放
    • 可以在控制器调用 viewDidDisappear 方法的时候来进行处理,通过调用 invalidateAndCancel 方法或者是 finishTasksAndInvalidate 方法来释放对代理对象的强引用。

    其中,invalidateAndCancel是直接取消请求然后释放代理对象,而finishTasksAndInvalidate是等请求完成之后释放代理对象。

    4. NSURLSessionDownloadTask 简单下载

    	//确定请求路径
     	NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/images/minion_02.png"];
     	//创建请求对象
     	NSURLRequest *request = [NSURLRequest requestWithURL:url];
     	//创建会话对象
     	NSURLSession *session = [NSURLSession sharedSession];
     	//创建会话请求
     	//优点:该方法内部已经完成了边接收数据边写沙盒的操作,解决了内存飙升的问题
     	NSURLSessionDownloadTask *downTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
         	//默认存储到临时文件夹 tmp 中,需要剪切文件到 cache
         	NSLog(@"%@", location);//目标位置
         	NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]  
                             stringByAppendingPathComponent:response.suggestedFilename];
    
         /**
          fileURLWithPath:有协议头
          URLWithString:无协议头
          */
         	[[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
    
     	}];
     	//发送请求
     	[downTask resume];
    

    以上方法无法监听下载进度,如要获取下载进度,可以使用代理的方式进行下载。

    5. NSURLSessionDownloadTask 代理方式

    	NSURL * url = [NSURL URLWithString:@"http://e.hiphotos.baidu.com/image/pic/item/63d0f703918fa0ec14b94082249759ee3c6ddbc6.jpg"];
        NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
        NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate:self delegateQueue: [NSOperationQueue mainQueue]];
    
        NSURLSessionDownloadTask * downloadTask =[ defaultSession downloadTaskWithURL:url];
        [downloadTask resume];
    

    代理方法:

    // 接收数据,可能多次被调用
    -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
    {
        float progress = totalBytesWritten * 1.0/totalBytesExpectedToWrite;
        
        // 主线程更新UI
        dispatch_async(dispatch_get_main_queue(),^ {
            [self.process setProgress:progress animated:YES];
        });
    }
    
    // 3.下载完成之后调用该方法
    - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
    {
        NSString *catchDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
        NSString *filePath = [catchDir stringByAppendingPathComponent:@"app.dmg"];
        
        NSError *fileError = nil;
        NSURL *fileURL = [NSURL fileURLWithPath:filePath];
        [[NSFileManager defaultManager] moveItemAtURL:location toURL:fileURL error:&fileError];
        
        if (fileError) {
            NSLog(@"保存下载文件出错:%@", fileError);
        } else {
            NSLog(@"保存成功:%@", filePath);
        }
    }
    

    暂停和恢复下载:

    方式一:

    // 暂停下载
    - (IBAction)suspendDownload {
        if (self.session) {
            __weak typeof(self) weakSelf = self;
            [self.task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
                weakSelf.receivedData = resumeData;
            }];
        }
    }
    
    // 恢复下载
    - (IBAction)resumeDownload {
        if (self.session) {
            self.task = [self.session downloadTaskWithResumeData:self.receivedData];
        }
        
        [self.task resume];
    }
    

    方式二:

    //暂停
    [self.downloadTask suspend];
    //恢复
    [self.downloadTask resume];
    

    6. NSURLSessionDownloadTask 后台下载

    // 后台session
    - (NSURLSession* ) backgroundURLSession {
        static NSURLSession * session = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSString * identifier = @"com.yourcompany.appId.BackgroundSession";
            NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:[NSOperationQueue mainQueue]];
        });
        
        return session;
    }
    
    // 创建并启动任务
    - (void)beginDownloadWithUrl:(NSString *)downloadURLString {
     	NSURL *downloadURL = [NSURL URLWithString:downloadURLString];
     	NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
     	NSURLSession *session = [self backgroundURLSession];
     	NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
     	
     	[downloadTask resume];
    }
    

    appDelegate中实现application:handleEventsForBackgroundURLSession:completionHandler:方法,在后台所有的任务完成后会调用给方法,但是我一直没有调用成功,原因未知,高手可以告知一下

    - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
    
        NSURLSession *backgroundSession = [self backgroundURLSession];
        
        NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession);
        
        // 保存 completion handler 以在处理 session 事件后更新 UI
        [self addCompletionHandler:completionHandler forSession:identifier];
    }
    

    handleEventsForBackgroundURLSession 方法是在后台下载的所有任务完成后才会调用。如果后台任务完成且应用被杀掉,启动应用程序后,该方法会在 application:didFinishLaunchingWithOptions:方法被调用之后被调用。

    //NSURLSessionDelegate委托方法,会在NSURLSessionDownloadDelegate委托方法后执行
    - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
         NSLog(@"Background URL session %@ finished events.
    ", session);
    }
    

    之后会调用接收完成的方法:

    /*
     * 在该方法结束前,需要处理location指向的文件,因为方法结束后,临时文件会被销毁
     * 如果用模拟器保存,会出错,因为模拟器上app退出后再启动是,路径会不一样,导致找不到后台下载的文件;而用真机调试则无此问题 !!!
     */
    - (void)URLSession:(NSURLSession *)session
          downloadTask:(NSURLSessionDownloadTask *)downloadTask
    didFinishDownloadingToURL:(NSURL *)location 
    {}
    
    /*
     * 该方法下载成功和失败都会回调,只是失败的是error是有值的,
     * 在下载失败时,error的userinfo属性可以通过NSURLSessionDownloadTaskResumeData
     * 这个key来取到resumeData(和上面的resumeData是一样的),再通过resumeData恢复下载
     */
    - (void)URLSession:(NSURLSession *)session
                  task:(NSURLSessionTask *)task
    didCompleteWithError:(NSError *)error 
    {}
    

    7. NSURLSessionUploadTask上传任务

    	NSURL*URL = [NSURLURLWithString:@"http://example.com/upload"];
    
    	NSURLRequest*request = [NSURLRequestrequestWithURL:URL];
    
    	NSData*data = ...;
    
    	NSURLSession*session = [NSURLSessionsharedSession];
    
    	NSURLSessionUploadTask*uploadTask = [session uploadTaskWithRequest:request fromData:datacompletionHandler:^(NSData*data, NSURLResponse *response,NSError*error) {
    
            // ...
    	}];
    
    [uploadTask resume];
    

    注意事项

    1. 后台下载的配置和限制

    作为一个必须实现的委托,您不能对NSURLSession使用简单的基于 block的回调方法。后台启动应用程序,是相对耗费较多资源的,所以总是采用HTTP重定向。后台传输服务只支持HTTPHTTPS,你不能使用自定义的协议。系统会根据可用的资源进行优化,在任何时候你都不能强制传输任务在后台进行。

    另外,要注意的是在后台会话中,NSURLSessionDataTasks 是完全不支持的,你应该只出于短期的、小请求等使用这些任务,而不是用来下载或上传。

    2. 后台启动新的下载

    苹果会对后台的下载任务进行限制,大致流程如下:

    • 苹果的NSURLSession这个类会维护一个Delay值(即延时执行时间),用于后台启动任务延时执行时使用;
    • 当在后台启动一个新任务时,苹果会对这个任务进行延时执行,延时时间苹果那边是有一个默认的延时时间,当后台启动的任务数越多,这个值就会成2N-1幂倍增长;
    • 比如:假设苹果设定的延时时间为Delay。当在后台启动了第一个任务时,这个任务的延时时间为Delay,这个任务会在Delay时间后开始执行;当启动在后台启动第二个任务时,这个任务的延时时间为:2 * Delay,当启动第三个任务是,该任务的延时执行时间即为:2 * 2 * Delay;以此类推,在后台启动第N个任务是,该任务的延时执行时间为:2^(N-1)次方 * Delay
    • 但是在应用从后台切到前台或者重新启动时,这个延时时间会重置。

    参考示例:

    https://github.com/BirdandLion/NSURLSessionDemo

    参考资料:
    Life Cycle of a URL Session

    http://www.jianshu.com/p/63e2ad28459f

    http://www.jianshu.com/p/b0ddadd34037

    http://www.jianshu.com/p/1211cf99dfc3

    http://www.jianshu.com/p/02a5a896c9ed

  • 相关阅读:
    react native( rn) 中关于navigationOptions中headerRight 获取navigation的问题 rn
    string与number转换
    基础面试资料
    vim快捷键大全
    Vim中的自定义快捷键
    去掉ubuntu命令行模式提示声
    模板非类型形参的详细阐述
    C++中为什么构造函数不能定义为虚函数
    C++ 深入理解 虚继承、多重继承和直接继承
    C++ 虚函数表解析
  • 原文地址:https://www.cnblogs.com/fishbay/p/7209595.html
Copyright © 2011-2022 走看看