自己動手寫一個iOS 網絡請求庫的三部曲
代碼示例: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 函數內填充以下代碼:
- @IBAction func mainButtonBeTapped(sender: AnyObject) {
- let session = NSURLSession.sharedSession()
- let request = NSURLRequest(URL: NSURL(string: "http://baidu.com")!)
- let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
- let string = NSString(data: data, encoding: NSUTF8StringEncoding)
- println(string)
- })
- task.resume()
- }
使用成功!
感受異步
異步
改寫 mainButtonBeTapped 函數的代碼:
- @IBAction func mainButtonBeTapped(sender: AnyObject) {
- let session = NSURLSession.sharedSession()
- let request = NSURLRequest(URL: NSURL(string: "http://baidu.com")!)
- let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
- println("just wait for 5 seconds!")
- sleep(5)
- let string = NSString(data: data, encoding: NSUTF8StringEncoding)
- println(string)
- })
- task.resume()
- }
再次嘗試,兩次打印之間間隔了五秒,主線程未阻塞,證明 NSURLSession 為異步執行。
阻塞
嘗試多次點擊,我們能夠看到每五秒執行一次,直到全部執行完畢。
NSURLSession 采用的是 “異步阻塞” 模型,即所有請求在發出后都進入 2# 線程執行,在 2# 線程內部按照阻塞隊列模式執行。
#p#
開源項目:Pitaya,適合大文件上傳的 HTTP 請求庫:https://github.com/johnlui/Pitaya
本章中,我們將一起嘗試使用一個類來封裝我們之前的代碼,并嘗試加入動態增加 HTTP 參數(params)的功能,之后封裝出一個強大的接口。
基本封裝
基礎準備
新建一個 Swift 空文件,命名為 Network.swift,在里面寫一個 Network 類,之后寫一個靜態方法 request():
- class Network{
- static func request() {
- let session = NSURLSession.sharedSession()
- let request = NSURLRequest(URL: NSURL(string: "http://baidu.com")!)
- let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
- println("just wait for 5 seconds!")
- sleep(5)
- let string = NSString(data: data, encoding: NSUTF8StringEncoding)
- println(string)
- })
- task.resume()
- }
- }
修改 ViewController 中的按鈕函數:
- @IBAction func mainButtonBeTapped(sender: AnyObject) {
- Network.request()
- }
運行項目,點擊按鈕,效果和之前一致。
自定義 HTTP method 和 URL
修改 request() 方法,將 HTTP 方法和 URL 傳進去:
- static func request(method: String, url: String) {
- let session = NSURLSession.sharedSession()
- let request = NSMutableURLRequest(URL: NSURL(string: url)!)
- request.HTTPMethod = method
- let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
- println("just wait for 5 seconds!")
- sleep(5)
- let string = NSString(data: data, encoding: NSUTF8StringEncoding)
- println(string)
- })
- task.resume()
- }
修改前面的函數調用:
- @IBAction func mainButtonBeTapped(sender: AnyObject) {
- Network.request("GET", url: "http://baidu.com")
- }
運行項目,點擊按鈕,效果和之前一致。
使用閉包處理請求結果
函數是 Swift 中的一等公民,閉包可以作為函數參數和返回值,十分強大。下面我們就用閉包來處理網絡請求的返回值。修改 request() 方法,傳遞進去一個閉包:
- static func request(method: String, url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
- let session = NSURLSession.sharedSession()
- let request = NSMutableURLRequest(URL: NSURL(string: url)!)
- request.HTTPMethod = method
- let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
- callback(data: data, response: response , error: error)
- })
- task.resume()
- }
在前面函數調用處使用閉包進行結果處理:
- @IBAction func mainButtonBeTapped(sender: AnyObject) {
- Network.request("GET", url: "http://baidu.com") { (data, response, error) -> Void in
- println("just wait for 5 seconds!")
- sleep(5)
- let string = NSString(data: data, encoding: NSUTF8StringEncoding)
- println(string)
- }
- }
運行項目,點擊按鈕,效果和之前一致。
動態增加 Params
GET 方法
GET 方法下,params 在經過 url encode 之后直接附在 URL 末尾發送給服務器。修改 request() 方法,傳遞進去一個 params 的字典:
- static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
- ... ...
- }
為了處理 params,我們從 Alamofire 偷來他的 params 處理函數。如果是 GET 方法,那就把處理過的 params 增加到 URL 后面。Network 類的完整代碼如下:
- class Network{
- static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
- let session = NSURLSession.sharedSession()
- var newURL = url
- if method == "GET" {
- newURL += "?" + Network().buildParams(params)
- }
- let request = NSMutableURLRequest(URL: NSURL(string: newURL)!)
- request.HTTPMethod = method
- let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
- callback(data: data, response: response , error: error)
- })
- task.resume()
- }
- // 從 Alamofire 偷了三個函數
- func buildParams(parameters: [String: AnyObject]) -> String {
- var components: [(String, String)] = []
- for key in sorted(Array(parameters.keys), [(String, String)] {
- var components: [(String, String)] = []
- if let dictionary = value as? [String: AnyObject] {
- for (nestedKey, value) in dictionary {
- components += queryComponents("\(key)[\(nestedKey)]", value)
- }
- } else if let array = value as? [AnyObject] {
- for value in array {
- components += queryComponents("\(key)", value)
- }
- } else {
- components.extend([(escape(key), escape("\(value)"))])
- }
- return components
- }
- func escape(string: String) -> String {
- let legalURLCharactersToBeEscaped: CFStringRef = ":&=;+!@#$()',*"
- return CFURLCreateStringByAddingPercentEscapes(nil, string, nil, legalURLCharactersToBeEscaped, CFStringBuiltInEncodings.UTF8.rawValue) as String
- }
- }
修改前面的函數調用:
- @IBAction func mainButtonBeTapped(sender: AnyObject) {
- Network.request("GET", url: "http://pitayaswift.sinaapp.com/pitaya.php", params: ["get": "Network"]) { (data, response, error) -> Void in
- let string = NSString(data: data, encoding: NSUTF8StringEncoding)
- println(string)
- }
- }
http://pitayaswift.sinaapp.com/pitaya.php 是我部署的用于測試的服務端代碼,會直接返回 ?get=ooxx 中的 ooxx。運行項目,點擊按鈕,查看效果:
POST 方法
POST 方法下有幾個協議可供選擇,此處沒有文件上傳,我們采用較簡單的 application/x-www-form-urlencoded 方式發送請求。request() 方法增加一些代碼:
- static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
- let session = NSURLSession.sharedSession()
- var newURL = url
- if method == "GET" {
- newURL += "?" + Network().buildParams(params)
- }
- let request = NSMutableURLRequest(URL: NSURL(string: newURL)!)
- request.HTTPMethod = method
- if method == "POST" {
- request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
- request.HTTPBody = Network().buildParams(params).dataUsingEncoding(NSUTF8StringEncoding)
- }
- let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
- callback(data: data, response: response , error: error)
- })
- task.resume()
- }
修改前面的函數調用:
- @IBAction func mainButtonBeTapped(sender: AnyObject) {
- Network.request("POST", url: "http://pitayaswift.sinaapp.com/pitaya.php", params: ["post": "Network"]) { (data, response, error) -> Void in
- let string = NSString(data: data, encoding: NSUTF8StringEncoding)
- 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 等設為成員變量,讓他們在構造函數中初始化:
- class NetworkManager {
- let method: String!
- let params: Dictionary let callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void
- let session = NSURLSession.sharedSession()
- let url: String!
- var request: NSMutableURLRequest!
- var task: NSURLSessionTask!
- init(url: String, method: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
- self.url = url
- self.request = NSMutableURLRequest(URL: NSURL(string: url)!)
- self.method = method
- self.params = params
- self.callback = callback
- }
- }
之后,將上面分析的
1. 確定 URL
2. 確定 HTTP 方法(GET、POST 等)
3. 添加特定的 HTTP 頭
4. 填充 HTTP Body
前三步封裝到一個 function 中,***一步封裝到一個 function 中,然后把驅動 session.dataTaskWithRequest 的代碼封裝到一個 function 中:
- func buildRequest() {
- if self.method == "GET" && self.params.count > 0 {
- self.request = NSMutableURLRequest(URL: NSURL(string: url + "?" + buildParams(self.params))!)
- }
- request.HTTPMethod = self.method
- if self.params.count > 0 {
- request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
- }
- }
- func buildBody() {
- if self.params.count > 0 && self.method != "GET" {
- request.HTTPBody = buildParams(self.params).nsdata
- }
- }
- func fireTask() {
- task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
- self.callback(data: data, response: response, error: error)
- })
- task.resume()
- }
之后使用一個統一的方法來驅動上面三個 function,完成請求:
- func fire() {
- buildRequest()
- buildBody()
- fireTask()
- }
同時,不要忘了那三個 parse params 的從 Alamofire 偷來的函數哦,也要放到這個類里面。至此,降低耦合的工作基本完成,接下來我們開始封裝“網絡API”。
使用適配器模式封裝“網絡API”
理解適配器模式
適配器模式是設計模式中的一種,很容易理解:我的 APP 需要一個獲取某一個 URL 返回的字符串的功能,我現在選擇的是 Alamofire,但是正在發展的 Pitaya 看起來不錯,我以后想替換成 Pitaya,所以我封裝了一層我自己的網絡接口,用來屏蔽底層細節,到時候只需要修改這個類,不需要再深入項目中改那么多接口調用了。
適配器模式聽起來高大上,其實這是我們在日常編碼中非常常用的設計模式。
Do it!
修改 Network 類的代碼為:
- class Network{
- static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
- let manager = NetworkManager(url: url, method: method, params: params, callback: callback)
- manager.fire()
- }
- }
搞定!
封裝多級接口
不帶 params 的接口:
- static func request(method: String, url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
- let manager = NetworkManager(url: url, method: method, callback: callback)
- manager.fire()
- }
兩個 get 接口(帶與不帶 params):
- static func get(url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
- let manager = NetworkManager(url: url, method: "GET", callback: callback)
- manager.fire()
- }
- static func get(url: String, params: Dictionary, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
- let manager = NetworkManager(url: url, method: "GET", params: params, callback: callback)
- manager.fire()
- }
兩個 post 接口(帶與不帶 params):
- static func post(url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
- let manager = NetworkManager(url: url, method: "POST", callback: callback)
- manager.fire()
- }
- static func post(url: String, params: Dictionary, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
- let manager = NetworkManager(url: url, method: "POST", params: params, callback: callback)
- manager.fire()
- }
測試接口
修改 ViewController 中的調用代碼,測試多級 API:
- @IBAction func mainButtonBeTapped(sender: AnyObject) {
- let url = "http://pitayaswift.sinaapp.com/pitaya.php"
- Network.post(url, callback: { (data, response, error) -> Void in
- println("POST 1 請求成功")
- })
- Network.post(url, params: ["post": "POST Network"], callback: { (data, response, error) -> Void in
- let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String
- println("POST 2 請求成功 " + string)
- })
- Network.get(url, callback: { (data, response, error) -> Void in
- println("GET 1 請求成功")
- })
- Network.get(url, params: ["get": "POST Network"], callback: { (data, response, error) -> Void in
- let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String
- println("GET 2 請求成功 " + string)
- })
- Network.request("GET", url: url, params: ["get": "Request Network"]) { (data, response, error) -> Void in
- let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String
- println("Request 請求成功 " + string)
- }
- }
運行項目,點擊按鈕,查看效果:
多級 API 封裝成功!
【責任編輯:chenqingxiang TEL:(010)68476606】




























