iOS网络-NSURLSession简单使用

一、前言

NSURLSession是iOS7以后提出的网络请求API,这个API通过一系列的代理方法支持认证,让你的app能够实现后台下载。

NSURLSession原生地支持 data,file,ftp,http和https等URL schemes。苹果提供了五种方案来更改URL的加载。


一览图

二、NSURLSession

NSURLSession提供了通过HTTP下载内容的API,主要是通过代理方法来实现。要使用这个API必须创建多个session,每个session都协调处理一组相关的数据转移任务。在浏览器中,每一个tab或者窗口都包含一个session对象。

NSURLSession API是高度异步的,如果使用系统提供的代理方法,开发者必须指定一个completion block 用来将数据转换到app中。

NSULRSession支持取消,重启,暂停和继续任务。

2.1 session的task

task的行为只要取决于:

  • session类型:
    session类型主要由创建session时使用的配置对象--NSURLSessionConfiguration对象决定

default session将用户证书存储到keychain 中,使用磁盘永久存储缓存。

+(NSURLSessionConfiguration *)defaultSessionConfiguration;

Ephemeral session不存储任何缓存和数据

+(NSURLSessionConfiguration *)ephemeralSessionConfiguration;

Background session与default session类似,只是多了些局限性

// NS_AVAILABLE(10_10, 8_0);
+(NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier
  • 任务类型
    NSURLSession支持三种类型的任务;

Data task使用NSData对象上传接收数据。当你的app和服务器发生短暂的经常性的交互时使用Data task比较合适。它能在所有数据都被接收后一次返回一点数据给你的app,或者通过completion handler全部返回。

Download task支持后台下载,检索数据。

Upload task以file的类型传递数据,支持后台上传

  • 任务是否是在前台被创建
    NSURLSession支持后台下载上传数据,在后台运行的session有以下限制:

1、必须提供session 的代理
2、支持HTTP和HTTPS协议
3、只有从文件中上传任务才会成功,如果是以data或者stream流形式会失败

note:iOS8之前,data task不支持后台session

当app不再运行而且后台传输结束或者请求证书的时候,iOS会调用application:handleEventsForBackgroundURLSession:completionHandler:方法在后台自动重启应用。这个方法提供了session标识来重启app,app应该存储的comoletionHandler,用这个标识来创建一个后台configuration对象,然后用这个configuration对象创建一个session.这个新的session会自动与后台的活动关联起来。之后当session晚餐最后一个后台任务的适合,会调用代理的URLSessionDidFinishEventsForBackgroundURLSession:方法,在这个代理方法中回到主线程调用之前存储的completionHandler,以便于让系统知道你的app又可以再次安全的处于挂起状态了。

Note: You must create exactly one session per identifier (specified when you create the configuration object). The behavior of multiple sessions sharing the same identifier is undefined.

创建session的时候要精确的使用标识。

当前任务完成的时候app处于挂起状态,代理的URLSession:downloadTask:didFinishDownloadingToURL:放会被调用。

相似地,如果task请求证书,session对象会调用代理的URLSession:task:didReceiveChallenge:completionHandler:或者URLSession:didReceiveChallenge:completionHandler:方法。

后台的上传下载任务会在网络错误的时候自动重连,不需要通过其他API去决定什么时候重新连接。

2.2 URL Session的生命周期

apple提供了两种使用NSURLSession API的方法:
使用自定义的delegate和系统提供的delegate,主要区别在于是否给代理赋值,通俗点就是:

使用系统代理方法:session.delegate = nil
使用自己定义的代理方法:session.delegate = aDelegate

如果你有以下需求的时候就需要用自己指定的代理来实现:

  • app不在运行的时候需要在后台上传下功能
  • 实现自定义的身份认证
  • 实现自定义的 SSL 证书验证
  • 从一个主题流中下载数据
  • 禁止缓存
  • 禁止HTTP重定向

2.2.1 系统代理URLSession生命周期

步骤:
1、创建一个session configuration对象,如果是background session ,必须指定明确的identify,存储这个identity,当app退出,崩溃,挂起的时候可以通过这个identity找到关联的session。

let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration()
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("com.nuclear.bgsession")

2、创建一个session对象,设置configuration对象,代理为nil

let session = NSURLSession(configuration:configuration)
//或者
let session = NSURLSession(configuration:configuration, delegate:nil, delegateQueue:nil)

3、通过session对象创建一个包含源请求的task对象。
每个task对象期初都是出于挂起状态,直到你调用了task的resume()方法,它才会开始下载特定的资源。
task对象都是NSURLSessionTask的子类:

  • NSURLSessionDataTask:请求资源,以一个或多个NSData对象返回服务器响应数据。支持 default,ephemeral,shared类型的session,不支持background session。
  • NSULRSessionUploadTask:与data task类似,但是upload task使得创建请求体更加容易,它还支持background session。
  • NSURLSessionDownloadTask:将资源直接下载到磁盘中,支持所有类型的session
  • NSURLSessionStreamTask:建立一个TCP / IP连接的主机名和端口或一个网络服务对象。

Important: If you are using the NSURLSession class without providing delegates, your app must create tasks using a call that takes a completionHandler parameter, because otherwise it cannot obtain data from the class.

如果不提供delegate,那么开发者需要使用comoletionHandler参数创建task,否则无法接收数据。

4、对于一个下载任务,如果用户暂停了下载,你应该这么做:调用cancelByProducingResumeData:方法取消任务,当用户点击继续下载的时候将返回的剩余数据传递给downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:方法,创建一个新的下载任务继续下载。

5、任务结束,session对象会调用 task的completion handler

note: NSURLSession does not report server errors through the error parameter. The only errors your app receives through the error parameter are client-side errors, such as being unable to resolve the hostname or connect to the host.Server-side errors are reported through the HTTP status code in the NSHTTPURLResponse object

error参数只会报告客户端的错误,比如:无法解析主机名或者无法连接主机,不会报告服务器的错误。服务器端的错误通过在NSHTTPURLResponse里面的HTTP的状态码来判定。error code详细内容参见 URL Loading System Error

6、不再需要session的时候调用invalidateAndCancel方法停止所有任务并使session失效;或者调用finishTasksAndInvalidate方法让任务继续运行,在运行结束时取消session。

2.2.2 自定义delegate 的URL Session生命周期

自定义delegate,并结合各个delegate方法能够实现更多的功能,也更加灵活,当然生命周期也更复杂。

apple提供了几点最基本的session 使用步骤:
前三步同默认delegate一样:
1、创建season configuration;

2、根据创建好的configuration创建session;

3、定义task类型,用创建好的session创建task

4、当远程服务器需要authentication并且此authentication需要challenge(例如:SSL客户端证书),这个时候就需要用到 authentication challenge 代理方法:

  • session-level challenge:
let NSURLAuthenticationMethodNTLM: String

let NSURLAuthenticationMethodNegotiate: String

let NSURLAuthenticationMethodClientCertificate: String

let NSURLAuthenticationMethodServerTrust: String

session会调用代理方法:URLSession:didReceiveChallenge:completionHandler:,如果没有实现session的代理方法,那么会调task 的代理方法:URLSession:task:didReceiveChallenge:completionHandler:来处理challenge。

  • non-session-level challenge
    session对象调用task delegate方法URLSession:task:didRecevieChallenge:completionHandler:方法处理,不调用session的delegate方法。

5、接收到HTTP重定向请求,session对象调用URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:方法。

6、使用downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:方法来创建继续下载任务,session调用URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:方法响应新的task对象。

7、对于data task,对应session的URLSession:dataTask:didBecomeDownloadTask:。在决定是否将data task转变为download task之后,会调用completionHandler方法来继续接收数据或者下载数据。

如果app将data task 转变成为 download task ,session会调用 URLSession:dataTask:didBecomeDownloadTask:,这个之后,session不会再接收data task的回调,转而接收download task的。

8、如果task是通过uploadTaskWithStreamRqeust:方法创建的,session会通过URLSession:task:needNewBodyStream提供一个新的数据主体。

9、在上传内容主体到服务器期间,delegate会周期性收到URLSession:downloadTask:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:,可以通过这个方法获知上传的进度

10、

  • 下载任务中:
    URLSession:dowloadTask:didiWriteData:totalBytesWriteen:totalBytesExpectedToWrite:获取下载进度;
    cancelByProducingResumeData:暂停下载;
    用户继续下载任务时,将接收到的data传递给downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:来创建一个新的下载任务。

  • data task中
    通过URLSession:dataTask:didReceiveData:方法获取接收进度;
    URLSession:dataTask:willCacheResponse:completionHandler:决定是否使用缓存,

11、下载任务完成之后,URLSession:downloadTask:didFinishDownloadingToURL:会被调用,它会返回下载的数据存放的临时路径,应该及时处理数据(读取数据或移入沙盒做持久化存储等)。

12、不管什么类型的task结束,URLSession:task:didCompleteWithError:都会被调用,根据error是否为空判断成功失败

在任务失败的情况下,大多数app应当尝试重新请求直到用户取消任务或者服务端返回error code预示这个任务不会成功。

NSError对象的userInfo字典包含一个key值为NSURLSessionDownloadTaskResumeData的value,应该将这个值传递给dowloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:并创建一个新的下载任务以继续执行之前的下载。

如果该下载任务不能继续下载,那应该创建一个新的下载任务重新开始下载。

13、如果response是多重编码的,session会多次调用didReceiveResponse方法,紧跟随的是didReceiveData的调用

14、如果要停止一个session,可以invalidateAndCancelfinishTasksAndInvalidate实现。session失效之后会给URLSession:didBecomeInvalideWithError:方法发送消息,当这个方法返回之后,session不再对delegate持有强引用。

Important: The session object keeps a strong reference to the delegate until your app explicitly invalidates the session. If you do not invalidate the session, your app leaks memory.

session会对delegate持续强引用知道失效为止,如果不调用invalid方法会造成内存泄露。

除此之外,如果你取消正在执行的下载任务,URLSession:task:didCompleteWithError:方法会被触发

三、基本用法

上面的内容都是与session的基本生命周期有关的,接下来来看看session的基本用法

3.1 配置session

session有三种类型,其中background类型需要知道identifier以便于开发者辨认和调试。

func configSession() {
    /*
        创建configuration
     */
    let defaultConfig = NSURLSessionConfiguration.defaultSessionConfiguration()
    let ephemeralConfig = NSURLSessionConfiguration.ephemeralSessionConfiguration()
    let bgConfig = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("com.nuclear.bgsession")
    
    /*
        获取缓存路径
    */
    let path = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true).first
    let fullPath = path! + NSBundle.mainBundle().bundleIdentifier! + "MyCacheDir"
    //设置cache路径和policy
    let cache = NSURLCache(memoryCapacity: 16*1024, diskCapacity: 256*1024*1024, diskPath: fullPath)
    defaultConfig.URLCache = cache
    defaultConfig.requestCachePolicy = .UseProtocolCachePolicy
    
    /*
        配置session
    */
    let defaultSession = NSURLSession(configuration: defaultConfig, delegate: self, delegateQueue: nil)
    let ephemeralSession = NSURLSession(configuration: ephemeralConfig, delegate: self, delegateQueue: nil)
    let bgSession = NSURLSession(configuration: bgConfig, delegate: self, delegateQueue: nil)

session的创建方法中,如果delegateQueue为nil的时候,session会创建一个串行队列,并在该队列中执行操作。

configuration对象可以随时安全地被更改,因为session会对configuration对象执行深复制,因此你的更改只会对新创建的session有效,因此需要创建一个新的session

ephemeralConfig.allowsCellularAccess = false
let epheMeralSession = NSSession(configuration:ehpemeralConfig, delegate: self, delegateQueue:nil)

3.2 不实现代理方法的情况下发起网络请求:

func createSession() {
    let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
    
    let request = NSURLRequest(URL: NSURL(string:"https://www.baidu.com")!)
    
    session.dataTaskWithRequest(request) { (data, response, error) -> Void in
        NSLog("Got response \(response) with error \(error)\n");
        NSLog("DATA:\n%@\nEND DATA\n",NSString(data: data!, encoding: NSUTF8StringEncoding)!)
    }.resume()
}

3.3 文件下载

实现此功能需要实现的代理方法有:

//提供获取到的数据的临时存储地址
URLSession:downloadTask:didFinishDownloadingToURL:

//获取下载进度
URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:

//先前失败的下载被重启了
URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:

//下载完成失败
URLSession:didCompleteWithError:

tip:

  • 暂停任务步骤:
    调用cancelByproducingResumeData:方得到还未下载的NSData对象,继续下载的时候将这个对象传递个dowloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:方法来创建一个新的download task继续下载。

2、下载失败:
代理方法URLSession:task:didCompleteWithError:会被调用,同时会得到一个NSEerror对象error,error有一个userInfo的字典,包含一个NSURLSessionDownloadTskResumeData的key,这个key值对应的value为NSData对象,如果需要重新下载,应当将这value值传递给dowloadTaskWithResumeData:或者dowloadTaskWithReusmeData:completionHandler:方法,创建一个新的download task继续下载。

实现下载功能关键步骤的代码:

/*
    init sesion and download task
*/
lazy var session:NSURLSession = {
    let config  = NSURLSessionConfiguration.defaultSessionConfiguration()
    let session = NSURLSession(configuration: config, delegate: self, delegateQueue: NSOperationQueue.currentQueue())
    return session
    }()

lazy var downloadTask:NSURLSessionDownloadTask = {
    let URLString = "http://yourDownloadFileURL.com"
    let URL = NSURL(string: URLString)
    let request = NSURLRequest(URL: URL!)
    let downloadTask = self.session.downloadTaskWithRequest(request)
    return downloadTask
    }()

/* 
    download task delegate
*/
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
    let p = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
    NSLog("progress:\(p)")
}

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
    NSLog("resume succeed")
}

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
    
    let path = savePathForDownloadData(location, task: downloadTask)
    NSLog("download completed in path:\(path)")
}

func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
    NSLog("download failed ")
    let resumeData = error?.userInfo[NSURLSessionDownloadTaskResumeData]
    if let rd = resumeData {
        self.downloadTask = self.session.downloadTaskWithResumeData(rd as! NSData)
        NSLog("task restart ")
        self.downloadTask.resume()
    }
}

//MARK: save downloaded data then return save path
func savePathForDownloadData(location:NSURL, task:NSURLSessionDownloadTask) -> NSURL {
    let manager = NSFileManager.defaultManager()
    let docDict = manager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first
    let originURL = task.originalRequest?.URL
    let distinationURL = docDict?.URLByAppendingPathComponent((originURL?.lastPathComponent)!)
    
    try! manager.copyItemAtURL(location, toURL: distinationURL!)
    
    return distinationURL!
}

/*
    start,pause,resuem download task
*/
@IBAction func startDownload(sender: AnyObject) {
    if downloadTask.state == .Running{return}
    NSLog("task start")
    downloadTask.resume()
}

@IBAction func pauseDownload(sender: AnyObject) {
    NSLog("task pause")
    downloadTask.cancelByProducingResumeData({ (data) -> Void in
        if let d = data{
            self.resumeData = d
        }
    })
}

@IBAction func resumeDownload(sender: AnyObject) {
    if downloadTask.state == .Running {return}
    
    downloadTask = session.downloadTaskWithResumeData(resumeData)
    NSLog("task resume")
    downloadTask.resume()
}

3.4 上传文件

有三种上传方式,分别对应三种情况:

  • NSData obj
    已经有data在内存中时。
    使用uploadTskWithRequest:fromData:或者uploadTaskWithRequest:fromData:completionHandler:创建一个新的upload task,在fromData参数中配置HTTP的请求体数据。
  • file
    上传磁盘中的文件,或者使用后台上传,或者你想通过这个方式使得占用的内存较少时。

    相似地,上传文件时使用uploadTskWithRequest:fromFile:或者uploadTskWithRequest:fromFile:completionHandler:创建upload task。

    session会根据待上传数据的大小来计算Content-Length header;如果没有提供Content-Type头信息,session会自动创建一个。如果需要提供其他的头信息,通过request对象来实现。

  • stream
    上传通过网络获取的数据(可能是边获取边上传)或者是将含有steram请求体的NSURLConnection转换为NSURLSession的时候。
    创建:uploadTaskWithSreamedRequest:

不管通过哪种方式,都必须实现URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:方法获取上传进度信息,

除此之外,如果使用stream上传还必须实现URLSession:task:needNewBodyStream:方法

3.5 后台下载

运行后台下载的任务会涉及到一个比较重要的appd elegate方法:

func application(application: UIApplication, 
      handleEventsForBackgroundURLSession identifier: String,
      completionHandler: () -> Void)

the block passed to the completionHandler
parameter is an opaque callback. The background transfer service needs to know when your application is done handling events for the session. Invoking that block informs the service that the application has completely processing of this set of events and the daemon can move on.

completionHandler:这个block是一个系统的回调,不能将一个具体的操作赋值给它,用于通知服务器app已经完成了这一系列的事件,后台的程序可以继续往下运行。

completionHandler是系统创建的,开发者不能更改它,但是可以复制,同时提供一个自定义的回调并将completionHandler传递给自定义回调,在适当的时候通知系统以进行下一步操作。

/*
    app delegate 中实现方法
*/
func application(application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: () -> Void) {
        let bgSessionVC = BGSessionViewController.bgDownload()
        let _ = NSURLSession(configuration: NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(identifier), delegate: bgSessionVC, delegateQueue: NSOperationQueue.currentQueue())
        bgSessionVC.addCompletionHandler(completionHandler, session: identifier)
    }


/*
    background session 实现方法
*/
typealias completionHandler = ()->()
var completionHandlerDic = [String:completionHandler]()

func URLSessionDidFinishEventsForBackgroundURLSession(session: NSURLSession) {
    if let ID = session.configuration.identifier {
        self.callHandlerForSession(ID)
    }
}
    
func callHandlerForSession(identify:String) {
    if let hander = self.completionHandlerDic[identify] {
        NSLog("handler execute")
        hander()
    }
}

func addCompletionHandler(handler:completionHandler,session:String) {
    if let _ = completionHandlerDic[session] {
        NSLog("compleHandler has exited")
        return
    }
    completionHandlerDic[session] = handler
}
```

更详细的步骤可以看看这个[小demo](https://github.com/hah1992/NSURLSessionDemo)。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容