自己动手写一个iOS 网络请求库的三部曲

移动开发 iOS
NSURLSession 是 iOS7 引入的新网络请求接口,在 WWDC2013 中有详细介绍,下面是描述其结构的一页 slides:

代码示例:https://github.com/johnlui/Swift-On-iOS/blob/master/BuildYourHTTPRequestLibrary

开源项目:Pitaya,适合大文件上传的 HTTP 请求库:https://github.com/johnlui/Pitaya

本系列文章中,我们将尝试使用 NSURLSession 技术构建一个自己的网络请求库。

NSURLSession 简介

NSURLSession 是 iOS7 引入的新网络请求接口,在 WWDC2013 中有详细介绍,下面是描述其结构的一页 slides:

当应用在前台时,NSURLSession 跟 NSURLConnection 没有什么区别,但是在程序切换到后台之后 Background Session 就会更加灵活。

尝试 NSURLSession

准备工作

新建一个名为 BuildYourHTTPRequestLibrary 的单页面应用,在页面上居中放置一个按钮,名为 Request:

拖动绑定 Touch Up Inside 事件:

使用 NSURLSession

在 mainButtonBeTapped 函数内填充以下代码:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) { 
  2. let session = NSURLSession.sharedSession() 
  3. let request = NSURLRequest(URL: NSURL(string: "http://baidu.com")!) 
  4. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in 
  5. let string = NSString(data: data, encoding: NSUTF8StringEncoding) 
  6. println(string) 
  7. }) 
  8. task.resume() 
  9. }

     

 

使用成功!

感受异步

异步

改写 mainButtonBeTapped 函数的代码:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) { 
  2. let session = NSURLSession.sharedSession() 
  3. let request = NSURLRequest(URL: NSURL(string: "http://baidu.com")!) 
  4. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in 
  5. println("just wait for 5 seconds!"
  6. sleep(5
  7. let string = NSString(data: data, encoding: NSUTF8StringEncoding) 
  8. println(string) 
  9. }) 
  10. task.resume() 

再次尝试,两次打印之间间隔了五秒,主线程未阻塞,证明 NSURLSession 为异步执行。

阻塞

尝试多次点击,我们能够看到每五秒执行一次,直到全部执行完毕。

NSURLSession 采用的是 “异步阻塞” 模型,即所有请求在发出后都进入 2# 线程执行,在 2# 线程内部按照阻塞队列模式执行。

#p#

 

开源项目:Pitaya,适合大文件上传的 HTTP 请求库:https://github.com/johnlui/Pitaya

本章中,我们将一起尝试使用一个类来封装我们之前的代码,并尝试加入动态增加 HTTP 参数(params)的功能,之后封装出一个强大的接口。

 

[[135519]]

基本封装

基础准备

新建一个 Swift 空文件,命名为 Network.swift,在里面写一个 Network 类,之后写一个静态方法 request():

  1. class Network{ 
  2. static func request() { 
  3. let session = NSURLSession.sharedSession() 
  4. let request = NSURLRequest(URL: NSURL(string: "http://baidu.com")!) 
  5. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in 
  6. println("just wait for 5 seconds!") 
  7. sleep(5) 
  8. let string = NSString(data: data, encoding: NSUTF8StringEncoding) 
  9. println(string) 
  10. }) 
  11. task.resume() 

修改 ViewController 中的按钮函数:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) { 
  2. Network.request() 

运行项目,点击按钮,效果和之前一致。

自定义 HTTP method 和 URL

修改 request() 方法,将 HTTP 方法和 URL 传进去:

  1. static func request(method: String, url: String) { 
  2. let session = NSURLSession.sharedSession() 
  3. let request = NSMutableURLRequest(URL: NSURL(string: url)!) 
  4. request.HTTPMethod = method 
  5. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in 
  6. println("just wait for 5 seconds!"
  7. sleep(5
  8. let string = NSString(data: data, encoding: NSUTF8StringEncoding) 
  9. println(string) 
  10. }) 
  11. task.resume() 

修改前面的函数调用:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) { 
  2. Network.request("GET", url: "http://baidu.com"

运行项目,点击按钮,效果和之前一致。

使用闭包处理请求结果

函数是 Swift 中的一等公民,闭包可以作为函数参数和返回值,十分强大。下面我们就用闭包来处理网络请求的返回值。修改 request() 方法,传递进去一个闭包:

  1. static func request(method: String, url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) { 
  2. let session = NSURLSession.sharedSession() 
  3. let request = NSMutableURLRequest(URL: NSURL(string: url)!) 
  4. request.HTTPMethod = method 
  5. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in 
  6. callback(data: data, response: response , error: error) 
  7. }) 
  8. task.resume() 

在前面函数调用处使用闭包进行结果处理:

 

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) { 
  2. Network.request("GET", url: "http://baidu.com") { (data, response, error) -> Void in 
  3. println("just wait for 5 seconds!"
  4. sleep(5
  5. let string = NSString(data: data, encoding: NSUTF8StringEncoding) 
  6. println(string) 

运行项目,点击按钮,效果和之前一致。

动态增加 Params

GET 方法

GET 方法下,params 在经过 url encode 之后直接附在 URL 末尾发送给服务器。修改 request() 方法,传递进去一个 params 的字典:

  1. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) { 
  2. ... ... 

为了处理 params,我们从 Alamofire 偷来他的 params 处理函数。如果是 GET 方法,那就把处理过的 params 增加到 URL 后面。Network 类的完整代码如下:

 

  1. class Network{ 
  2. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) { 
  3. let session = NSURLSession.sharedSession() 
  4.  
  5. var newURL = url 
  6. if method == "GET" { 
  7. newURL += "?" + Network().buildParams(params) 
  8.  
  9. let request = NSMutableURLRequest(URL: NSURL(string: newURL)!) 
  10. request.HTTPMethod = method 
  11.  
  12. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in 
  13. callback(data: data, response: response , error: error) 
  14. }) 
  15. task.resume() 
  16.  
  17. // 从 Alamofire 偷了三个函数 
  18. func buildParams(parameters: [String: AnyObject]) -> String { 
  19. var components: [(String, String)] = [] 
  20. for key in sorted(Array(parameters.keys), [(String, String)] { 
  21. var components: [(String, String)] = [] 
  22. if let dictionary = value as? [String: AnyObject] { 
  23. for (nestedKey, value) in dictionary { 
  24. components += queryComponents("\(key)[\(nestedKey)]", value) 
  25. else if let array = value as? [AnyObject] { 
  26. for value in array { 
  27. components += queryComponents("\(key)", value) 
  28. else { 
  29. components.extend([(escape(key), escape("\(value)"))]) 
  30.  
  31. return components 
  32. func escape(string: String) -> String { 
  33. let legalURLCharactersToBeEscaped: CFStringRef = ":&=;+!@#$()',*" 
  34. return CFURLCreateStringByAddingPercentEscapes(nil, string, nil, legalURLCharactersToBeEscaped, CFStringBuiltInEncodings.UTF8.rawValue) as String 

修改前面的函数调用:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) { 
  2. Network.request("GET", url: "http://pitayaswift.sinaapp.com/pitaya.php", params: ["get""Network"]) { (data, response, error) -> Void in 
  3. let string = NSString(data: data, encoding: NSUTF8StringEncoding) 
  4. println(string) 

http://pitayaswift.sinaapp.com/pitaya.php 是我部署的用于测试的服务端代码,会直接返回 ?get=ooxx 中的 ooxx。运行项目,点击按钮,查看效果:

POST 方法

POST 方法下有几个协议可供选择,此处没有文件上传,我们采用较简单的 application/x-www-form-urlencoded 方式发送请求。request() 方法增加一些代码:

  1. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) { 
  2. let session = NSURLSession.sharedSession() 
  3.  
  4. var newURL = url 
  5. if method == "GET" { 
  6. newURL += "?" + Network().buildParams(params) 
  7.  
  8. let request = NSMutableURLRequest(URL: NSURL(string: newURL)!) 
  9. request.HTTPMethod = method 
  10.  
  11. if method == "POST" { 
  12. request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type"
  13. request.HTTPBody = Network().buildParams(params).dataUsingEncoding(NSUTF8StringEncoding) 
  14.  
  15. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in 
  16. callback(data: data, response: response , error: error) 
  17. }) 
  18. task.resume() 

修改前面的函数调用:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) { 
  2. Network.request("POST", url: "http://pitayaswift.sinaapp.com/pitaya.php", params: ["post""Network"]) { (data, response, error) -> Void in 
  3. let string = NSString(data: data, encoding: NSUTF8StringEncoding) 
  4. println(string) 

使用 POST 方式发送请求,同样服务端会返回 key 为 post 的 value 的值。运行项目,点击按钮,结果和前面 GET 方法的结果一致。

至此,接口封装完成!

 

#p#

开源项目:Pitaya,适合大文件上传的 HTTP 请求库:https://github.com/johnlui/Pitaya

本文中,我们将一起降低之前代码的耦合度,并使用适配器模式实现一层独立于底层结构的网络 API,造一个真正的网络请求“库”。

降低耦合度

如何降低耦合度

现在的清汤挂面式的代码虽然便于理解,但是功能单一,代码杂乱。我们一起来分析 NSURLSession 的使用过程:

构造 NSURLRequest

确定 URL

确定 HTTP 方法(GET、POST 等)

添加特定的 HTTP 头

填充 HTTP Body

驱动 session.dataTaskWithRequest 方法,开始请求

具体实施

在 Network 下另外新建一个 NetworkManager 类,将 URL、params、files 等设为成员变量,让他们在构造函数中初始化:

  1. class NetworkManager { 
  2.  
  3. let method: String! 
  4. let params: Dictionary let callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void 
  5.  
  6. let session = NSURLSession.sharedSession() 
  7. let url: String! 
  8. var request: NSMutableURLRequest! 
  9. var task: NSURLSessionTask! 
  10.  
  11. init(url: String, method: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) { 
  12. self.url = url 
  13. self.request = NSMutableURLRequest(URL: NSURL(string: url)!) 
  14. self.method = method 
  15. self.params = params 
  16. self.callback = callback 

之后,将上面分析的

1. 确定 URL

2. 确定 HTTP 方法(GET、POST 等)

3. 添加特定的 HTTP 头

4. 填充 HTTP Body

前三步封装到一个 function 中,***一步封装到一个 function 中,然后把驱动 session.dataTaskWithRequest 的代码封装到一个 function 中:

 

  1. func buildRequest() { 
  2. if self.method == "GET" && self.params.count > 0 { 
  3. self.request = NSMutableURLRequest(URL: NSURL(string: url + "?" + buildParams(self.params))!) 
  4.  
  5. request.HTTPMethod = self.method 
  6.  
  7. if self.params.count > 0 { 
  8. request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type"
  9. func buildBody() { 
  10. if self.params.count > 0 && self.method != "GET" { 
  11. request.HTTPBody = buildParams(self.params).nsdata 
  12. func fireTask() { 
  13. task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in 
  14. self.callback(data: data, response: response, error: error) 
  15. }) 
  16. task.resume() 

之后使用一个统一的方法来驱动上面三个 function,完成请求:

  1. func fire() { 
  2. buildRequest() 
  3. buildBody() 
  4. fireTask() 

同时,不要忘了那三个 parse params 的从 Alamofire 偷来的函数哦,也要放到这个类里面。至此,降低耦合的工作基本完成,接下来我们开始封装“网络API”。

使用适配器模式封装“网络API”

理解适配器模式

适配器模式是设计模式中的一种,很容易理解:我的 APP 需要一个获取某一个 URL 返回的字符串的功能,我现在选择的是 Alamofire,但是正在发展的 Pitaya 看起来不错,我以后想替换成 Pitaya,所以我封装了一层我自己的网络接口,用来屏蔽底层细节,到时候只需要修改这个类,不需要再深入项目中改那么多接口调用了。

适配器模式听起来高大上,其实这是我们在日常编码中非常常用的设计模式。

Do it!

修改 Network 类的代码为:

  1. class Network{ 
  2. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) { 
  3. let manager = NetworkManager(url: url, method: method, params: params, callback: callback) 
  4. manager.fire() 

搞定!

封装多级接口

不带 params 的接口:

  1. static func request(method: String, url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) { 
  2. let manager = NetworkManager(url: url, method: method, callback: callback) 
  3. manager.fire() 

两个 get 接口(带与不带 params):

 

  1. static func get(url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) { 
  2. let manager = NetworkManager(url: url, method: "GET", callback: callback) 
  3. manager.fire() 
  4. static func get(url: String, params: Dictionary, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) { 
  5. let manager = NetworkManager(url: url, method: "GET", params: params, callback: callback) 
  6. manager.fire() 

两个 post 接口(带与不带 params):

 

  1. static func post(url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) { 
  2. let manager = NetworkManager(url: url, method: "POST", callback: callback) 
  3. manager.fire() 
  4. static func post(url: String, params: Dictionary, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) { 
  5. let manager = NetworkManager(url: url, method: "POST", params: params, callback: callback) 
  6. manager.fire() 

测试接口

修改 ViewController 中的调用代码,测试多级 API:

 

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) { 
  2. let url = "http://pitayaswift.sinaapp.com/pitaya.php" 
  3.  
  4. Network.post(url, callback: { (data, response, error) -> Void in 
  5. println("POST 1 请求成功"
  6. }) 
  7. Network.post(url, params: ["post""POST Network"], callback: { (data, response, error) -> Void in 
  8. let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String 
  9. println("POST 2 请求成功 " + string) 
  10. }) 
  11.  
  12. Network.get(url, callback: { (data, response, error) -> Void in 
  13. println("GET 1 请求成功"
  14. }) 
  15. Network.get(url, params: ["get""POST Network"], callback: { (data, response, error) -> Void in 
  16. let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String 
  17. println("GET 2 请求成功 " + string) 
  18. }) 
  19.  
  20. Network.request("GET", url: url, params: ["get""Request Network"]) { (data, response, error) -> Void in 
  21. let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String 
  22. println("Request 请求成功 " + string) 

运行项目,点击按钮,查看效果:

 

多级 API 封装成功!

 

【责任编辑:chenqingxiang TEL:(010)68476606】

责任编辑:chenqingxiang 来源: CocoaChina
相关推荐

2015-06-02 10:24:43

iOS网络请求降低耦合

2015-06-02 09:51:40

iOS网络请求封装接口

2009-09-14 09:04:17

CCNA考试CCNA

2011-03-25 09:56:40

Nagios 安装

2022-10-10 09:10:51

家庭网络网络

2011-03-09 09:30:52

Mina

2011-08-03 10:33:05

网络管理网络拓扑管理

2010-05-12 10:51:06

2010-07-17 01:12:31

Telnet服务

2010-09-26 14:39:40

DHCP故障分析

2013-06-28 09:35:04

Hypervisor虚拟化成本

2010-09-06 09:22:26

CSS语法

2011-03-21 09:22:46

Tomcat

2017-04-11 09:07:20

互联网

2017-02-07 14:50:39

华为

2018-03-18 15:51:59

人工智能潜力首席数据官

2018-03-19 09:35:37

人工智能

2015-05-12 10:42:53

程序员代码

2012-09-10 16:19:00

云计算公共云

2009-04-07 11:07:16

网络营销
点赞
收藏

51CTO技术栈公众号