今天博客的主題不是Alamofire, 而是iOS網絡編程中經常使用的NSURLSession。如果你想看權威的NSURLSession的東西,那么就得去蘋果官方的開發中心去看了,雖然是英文的,但是結合代碼理解應該不難。更詳細的信息請移步於蘋果官方介紹URL Loading System,網上好多iOS網絡編程的博客都翻譯於此。因為目前iOS開發中,網絡請求大部分使用NSURLSession,所以今天的博客我們就以NSURLSession展開。關於之前使用的NSURLConnection在此就不做過多贅述了。今天博客的主要內容是系統的介紹NSURLSession及其相關的Delegate,當然每個知識點都依托於實例,如果你仔細的閱讀本篇博客還是收獲不少的。
下方這個截圖中所涵蓋的所有功能就是本篇博客中所涉及的所有知識點,幾乎涵蓋了NSURLSession的所有的東西。接下來我們就一個一個的功能點來詳述一下NSURLSession。
一、NSURLSession概覽
NSURLSession對於iOS開發來說並不是什么新的內容,它是Apple在iOS7中引入的,其主要功能是發起網絡請求獲取網絡數據,這與iOS7之前使用的NSURLConnection功能類似,但是NSURLSession更為強大。如果在你開發的App中沒有使用第三方網絡庫,那么NSURLSession無異於是最佳的選擇。雖然網上的關於NSURLSession的東西一抓一大把,但是每個人都有每個人的見解,今天的博客就系統的整理一下NSURLSession相關的知識點,算是為下篇博客做准備吧。因為下篇博客是對Alamofire框架進行的解析,Alamofire就是對NSURLSession的封裝,還是那句話,如果你對NSURLSession不熟悉的話,那么Alamofire源碼看起來會比較費勁的。在本篇博客的第一部分我們先整體的概覽一下NSURLSession,以便后面一步步的展開。
廢話少說,進入本篇博客的主題。從NSURLSession這個名字中我們不難看出,主要是URL + Session。顧名思義,NSURLSession是用來URL會話的。當然如果你做過服務器端的開發,比如PHP,也會有Session的概念,不過此Session非彼Session,兩者的區別還是不小的。iOS的NSURLSession的主要功能是通過URL與服務器簡歷會話的。“會話”進一步說就是交流唄,一句話總結:也就是我們的iOS客戶端可以使用NSURLSession這個東西通過相應的URL與我們的服務器建立會話,然后通過此會話來完成一些交互任務(NSURLSessionTask)。Session有着不同的類型,每種類型的Session又可以執行不同類型的任務(Task)。接下來就來介紹一下Session的類型以及所執行的任務等。
1.NSURLSession的類型
在使用NSURLSession時你得知道你使用的是那種類型的Session對吧。從官方的NSURLSession API中不難看出,公有三種類型的Session:Default sessions,Ephemeral sessions,Background sessions。這三種Session我們可以通過NSURLSessionConfiguration來指定。
- 默認會話(Default Sessions)使用了持久的磁盤緩存,並且將證書存入用戶的鑰匙串中。
- 臨時會話(Ephemeral Session)沒有像磁盤中存入任何數據,與該會話相關的證書、緩存等都會存在RAM中。因此當你的App臨時會話無效時,證書以及緩存等數據就會被清除掉。
- 后台會話(Background sessions)除了使用一個單獨的線程來處理會話之外,與默認會話類似。不過要使用后台會話要有一些限制條件,比如會話必須提供事件交付的代理方法、只有HTTP和HTTPS協議支持后台會話、總是伴隨着重定向。僅僅在上傳文件時才支持后台會話,當你上傳二進制對象或者數據流時是不支持后台會話的。當App進入后台時,后台傳輸就會被初始化。(需要注意的是iOS8和OS X 10.10之前的版本中后台會話是不支持數據任務(data task)的)。
下方的截圖就是我們使用Swift語言創建了上述三種類型的會話配置,Session在初始化時可以指定下方的任意一種SessionConfiguration。具體入校所示:
2. NSURLSession的各種任務
在一個Session會話中可以發起的任務可分為三種:數據任務(Data Task)、下載任務(Download Task)、上傳任務(Upload Task)。上面也提到了,在iOS8和OS X 10.10之前的版本中后台會話是不支持Data Task。下面來簡述一下這三種任務。
- Data Task(數據任務)負責使用NSData對象來發送和接收數據。Data Task是為了那些簡短的並且經常從服務器請求的數據而准備的。該任務可以沒請求一次就對返回的數據進行一次處理。
- Download task(下載任務)以表單的形式接收一個文件的數據,該任務支持后台下載。
- Upload task(上傳任務)以表單的形式上傳一個文件的數據,該任務同樣支持后台下載。
上面所介紹的所有類型的Session以及Session中的Task會在下方的實例中進行一一的介紹,本部分就做一個概述。
二、URL編碼
1.URL編碼概述
無論是GET、POST還是其他的請求,與服務器交互的URL是需要進行編碼的。因為進行URL編碼的參數服務器那邊才能進行解析,為了能和服務器正常的交互,我們需要對我們的參數進行轉義和編碼。先簡單的聊一下什么是URL吧,其實URL是URI(Uniform Resource Identifier ---- 統一資源定位符)的一種。URL就是互聯網上資源的地址,用戶可以通過URL來找到其想訪問的資源。RFC3986文檔規定,Url中只允許包含英文字母(a-zA-Z)、數字(0-9)、-_.~4個特殊字符以及所有保留字符,如果你的URL中含有漢字,那么就需要對其進行轉碼了。RFC3986中指定了以下字符為保留字符:! * ' ( ) ; : @ & = + $ , / ? # [ ]。
在URL編碼時有一定的規則,下方是我們今天主要使用的URL格式的一個規則的一個圖解。其他的我們先不說,今天博客中所涉及的主要是下圖中Query的部分。從下面我們不難看出,Path和Query之間使用的是?號進行分隔的,問好后邊就是我們要傳給服務武器的參數了,該參數就是下方的Query的部分。在Get請求中Query是存放在URL后邊,而在POST中是放在Request的Body中。如果你的參數只是一個key-Value, 那么Query的形式就是key = value。如果你的參數是一個數組比如key = [itme1, item2, item3,……],那么你的Query的格式就是key[]=item1&key[itme2]&key[item3]……。如果你的參數是一個字典比如key = ["subKey1":"item1", "subKey2":"item2"], 那么Query對應的形式就是key[subKey1]=item1&key[subKey2]=item2.接下來我們要做的就是將字典進行URL編碼。
2.將Dictionary進行URL編碼
在iOS開發中,有時候我們從VC層或者VM層獲取到的數據是一個字典,字典中存儲的就是要發給服務器的數據參數。直接將字典轉成二進制數據發送給服務器,服務器那邊是沒法解析iOS這邊的字典的,得有一個統一的交互標准,這個標准就是URL編碼。我們要做的就是講字典進行URL編碼,然后將編碼后的東西在傳給服務器,這樣一來服務器那邊就能解析到我們請求的參數了。下方折疊的這段代碼就是從AlamoFire框架中摘抄出來的三個方法,位於ParameterEncoding.swift文件中。該段代碼就是負責將字典類型的參數進行URL編碼的,在編碼過程中進行轉義是少不了的。

1 // - MARK - Alamofire中的三個方法該方法將字典轉換成URL編碼的字符 2 func query(parameters: [String: AnyObject]) -> String { 3 4 var components: [(String, String)] = [] //存有元組的數組,元組由ULR中的(key, value)組成 5 6 for key in parameters.keys.sort(<) { //遍歷參數字典 7 let value = parameters[key]! 8 components += queryComponents(key, value) 9 } 10 11 return (components.map { "\($0)=\($1)" } as [String]).joinWithSeparator("&") 12 } 13 14 15 func queryComponents(key: String, _ value: AnyObject) -> [(String, String)] { 16 var components: [(String, String)] = [] 17 18 19 if let dictionary = value as? [String: AnyObject] { //value為字典的情況, 遞歸調用 20 for (nestedKey, value) in dictionary { 21 components += queryComponents("\(key)[\(nestedKey)]", value) 22 } 23 } else if let array = value as? [AnyObject] { //value為數組的情況, 遞歸調用 24 for value in array { 25 components += queryComponents("\(key)[]", value) 26 } 27 } else { //vlalue為字符串的情況,進行轉義,上面兩種情況最終會遞歸到此情況而結束 28 components.append((escape(key), escape("\(value)"))) 29 } 30 31 return components 32 } 33 34 /** 35 36 - parameter string: 要轉義的字符串 37 38 - returns: 轉義后的字符串 39 */ 40 func escape(string: String) -> String { 41 /* 42 :用於分隔協議和主機,/用於分隔主機和路徑,?用於分隔路徑和查詢參數, #用於分隔查詢與碎片 43 */ 44 let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 45 46 //組件中的分隔符:如=用於表示查詢參數中的鍵值對,&符號用於分隔查詢多個鍵值對 47 let subDelimitersToEncode = "!$&'()*+,;=" 48 49 let allowedCharacterSet = NSCharacterSet.URLQueryAllowedCharacterSet().mutableCopy() as! NSMutableCharacterSet 50 allowedCharacterSet.removeCharactersInString(generalDelimitersToEncode + subDelimitersToEncode) 51 52 53 var escaped = "" 54 55 //========================================================================================================== 56 // 57 // Batching is required for escaping due to an internal bug in iOS 8.1 and 8.2. Encoding more than a few 58 // hundred Chinese characters causes various malloc error crashes. To avoid this issue until iOS 8 is no 59 // longer supported, batching MUST be used for encoding. This introduces roughly a 20% overhead. For more 60 // info, please refer to: 61 // 62 // - https://github.com/Alamofire/Alamofire/issues/206 63 // 64 //========================================================================================================== 65 66 if #available(iOS 8.3, OSX 10.10, *) { 67 escaped = string.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacterSet) ?? string 68 } else { 69 let batchSize = 50 //一次轉義的字符數 70 var index = string.startIndex 71 72 while index != string.endIndex { 73 let startIndex = index 74 let endIndex = index.advancedBy(batchSize, limit: string.endIndex) 75 let range = startIndex..<endIndex 76 77 let substring = string.substringWithRange(range) 78 79 escaped += substring.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacterSet) ?? substring 80 81 index = endIndex 82 } 83 } 84 85 return escaped 86 }
在上述代碼中,功能並不算復雜。就是遞歸將字典中的所有鍵值對轉變成key=value、key[]=value、key[subkey]=value這三種形式。之所以進行遞歸,因為字典中有可能含有字典或者數組,數組中又可能嵌套着數組或者字典。所有要進行遞歸,直到找到key=value這種形式為止。上述的三個函數中queryComponents()方法就負責進行遞歸調用的。從下方的截圖中我們不難看出,字典、數組以及鍵值對的處理方式是不同的。
調用上述代碼段的query方法就可以對字典進行轉義。query()方法的參數是一個[String, AnyObject]類型的字典,返回參數是一個字符串。這個返回的字符串就是將該字典進行編碼后的結果。接下來我們對其進行測試。點擊“URL編碼”按鈕就會執行下方的方法,在該方法中我們定義了一個字典,該字典的key是String類型的,Value中存儲的有String、Array以及Dictionary。將該字典作為參數傳入query()中,然后query()函數返回的字符串進行數據。緊跟着的就是輸出結果,從結果中我們能看出將中文字符進行了百分號編碼,也就是URL編碼。
我們可以將上述輸出的字符串使用站上工具進行URL解碼,解碼后的URL如下所示:
三、數據任務--NSURLSessionDataTask
解決完了URL編碼的問題,我們就具體的來看一下NSURLSessionDataTask了,也就是我們上面所提到的Data Task。因為會話中會執行不同的任務,所以任務的對象來自於Session對象,也就是說我們需要使用已經存在的Session對象來創建我們的任務對象。接下來我們來看一下Data Task的使用。本部分主要給出了Data Task的工作方式。
1.對Data task代碼的封裝
下方截圖中的sessionDataTaskRequest()方法,該方法的第一個參數是會話請求的方式“POST”、"GET"等。第二個參數就發送到服務器的參數,該參數是一個[String:AnyObject]類型的字典。下面就是NSURLSessionDataTask的使用步驟
- 首先我們先創建會話使用的URL,在創建URL是我們要對parameters字典參數進行URL編碼。如果是GET方式的請求的話就使用?號將我們編碼后的字符串拼接到URL后方即可。
- 然后創建我們會話使用的請求(NSURLMutableRequest),在創建請求時我們要指定請求方式是POST還是GET。如果是POST方式,我們就將編碼后的URL字符串放入request的HTTPBody中即可,有一點需要注意的是我們傳輸的數據都是二進制的,所以在將字符串存入HTTPBody之前要將其轉換成二進制,在轉換成二進制的同時我們使用的是UTF8這種編碼格式。
- 創建完Request后,我們就該創建URLSession了,此處我們為了簡單就獲取了全局的Session單例。我們使用這個Session單例創建了含有Request對象的一個DataTask。在這個DataTask創建時,有一個尾隨閉包,這個尾隨閉包用來接收服務器返回來的數據。當然此處可以指定代理,使用代理來接收和解析數據的,稍后會介紹到。
- 最后切記創建好的Data Task是處於掛起狀態的,需要你去喚醒它,所以我們要調用dataTask的resume方法進行喚醒。具體如下所示。
2. 測試
上述Data Task的核心代碼已經完成,接下來我們要對其進行Get和Post測試。也就是給上述方法傳入“GET”或者"POST"請求方式和相應的參數。下方代碼截圖是對DataTask進行GET測試。傳入相應的參數,控制台中輸出的是服務器接收到參數后返回的數據。當然下方輸出的數據是我們通過JSON解析后的數據了。
緊接着我們進行POST測試,也就是傳入"POST"已經相應的參數,具體如下所示。下方的輸出是服務器返回的數據。
四、上傳任務---Upload Task
接下來我們來搞一下Upload Task,顧名思義Upload Task就是用來往服務器上上傳東西的嘛。下方這個代碼段就是用來往服務器上傳二進制數據的,當然我們使用的是POST方式進行表單提交的。下方的代碼步驟與上述DataTask的使用方式大為相似,具體步驟如下所示。
- 先創建URL和request並為request指定請求方式為POST。
- 然后創建Session,此處我們使用SessionConfiguration為Session的類型指定為default session類型。並為該Session指定代理對象為self。
- 最后使用Session來創建upload task,在創建upload task時為上傳任務指定NSURLRequest對象,並且傳入要上傳的表單數據formData,當然不要忘了將任務進行喚醒。
接下來我們要將上述代碼進行測試,上面有兩測試地址,第一個是你可以使用的,第二個是我在我本地服務器自己使用php寫的一個文件上傳的腳本,當然你是使用不了的。如果你要運行上述代碼的話,你就要使用第一個地址進行測試了。下方代碼段就是我們的測試用例,首先我們先通過網絡獲取圖片,並NSData加載到本地,獲取到圖片的二進制數據imageData。等待圖片數據獲取完畢后,在調用上述上傳數據的方法。為了請求完圖片的二進制數據后在調用上述方法,我們使用了GCD中dispatch group的相關東西。關於GCD更為詳細的內容請參見之前的博客《GCD詳解》。下方的代碼會在點擊“UploadTask”按鈕時會被觸發。
在上傳文件時,如果你想時刻的監聽上傳的進度,你可以去實現NSURLSessionTaskDelegate中的didSendBodyData方法,該方法會實時的監聽文件上傳的速度。bytesSent回調參數表示本次上傳的字節數,totalBytesSend回調參數表示已經上傳的數據大小,
totalBytesExpectedToSend表示文件公有的大小。該回調方法具體實現方式如下,在下方回調方法中我們根據每次上傳的數據情況對進度條進行更新,當然在更新UI時我們要在主線程中進行更新。具體代碼如下。
五、下載任務--Download Task
Download Task這種類型的任務就稍微有些復雜了,接下來我們來一一的進行介紹。接下來我們要實現一個支持后台下載並且支持暫停和繼續的任務。在下載時我們也要實現相應的回調代理來監聽下載進度,后台下載以及下載任務的暫停和繼續在開發中用的還是比較多的,本部分就好好的探討一下Download task。下方的實例是從網絡下載一個比較大的圖片,下載完畢后就從存儲到Document中。
1.創建后台會話
在創建Download Task 之前我們要先創建一個支持后台下載的會話,也就是Background Session。因為我們要暫停和續傳,所以在此Background Session的對象和Download Task的對象都是使用的類屬性。下方代碼段就創建了一個background session的對象。首先我們先創建一個background類型的Session Configuration,然后在創建downloadSession對象時配置為background即可。在創建Session對象時要為downloadSession對象指定代理對象,因為我們要在相應的代理對象中獲取下載進度更新我們的ProgressView。創建DownloadSession對象的代碼如下:
1 //創建BackgroundDownloadSession 2 let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(keyBackgroundDownload) 3 self.downloadSession = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
2.開始下載
創建好downloadSession對象后,我們就該創建downloadTask開始進行文件的下載了。下方代碼段就是點擊“開始下載”按鈕所觸發的方法。首先我們先獲取ResumeData,這個ResumeData就是我們暫停下載任務是所保存的信息,通過該ResumeData我們可以接着上次的文件進行下載。ResumeData中存儲的並不是我們上次下載的數據Data,而是存儲了下載地址和上次下載的位置等相關的信息,稍后會將ResumeData進行打印。我們從UserDefault中獲取ResumeData,如果存在ResumeData我們就調用下載會話的downloadTaskWithResumeData()方法傳入ResumeData接着上次的下載。如果ResumeData為nil,那么我們就創建下載請求,調用下載會話的downloadTaskWithRequest()方法創建下載任務。創建完下載任務后不要忘記將任務進行resume()呢。點擊“開始下載”的代碼如下所示。
3.暫停下載
上面是開始下載,接下來讓我們來實現暫停下載。下方代碼段就是點擊“暫停下載”按鈕所觸發的方法。在該方法中我們主要調用了downloadTask中的cancelByProducingResumeData()方法來進行任務的暫停的。在調用上述方法時會通過Closure回調的形式返回一個ResumeData,此處的ResumeData就是上面我們使用到的ResumeData。拿到該ResumeData后你要講它進行磁盤的持久化存儲,便於下次繼續下載。此處為了方便就將ResumeData存到了UserDefault中,其實也就是plist文件中。
下方就是我們在暫停下載任務時所打印的ResumeData中的內容。從下方的內容不難看出ResumeData就是一個xml格式的文本信息其中存儲着相應的下載信息。比如下載文件的URL(NSURLSessionDownloadURL),已經接受的數據字節(NSURLSessionResumeBytesReceived)等等相關的下載信息,具體請看下圖。我們可以通過下方的xml存儲的信息重新接着上次的下載任務進行下載。
上面有一項存儲的就是所下載文件的臨時文件的名稱,就位於temp目錄中。在下載過程中正在下載的任務會在temp目錄中創建一個.tmp的臨時文件用來存儲下載的臨時數據,也就是說這個臨時文件就是邊下邊存的地方。下載完成后我們要對該臨時文件進行轉存的,因為下載完成后該臨時文件會被自動刪除的。
4.下載任務的回調--NSURLSessionDownloadDelegate
上面兩段代碼主要是用於下載任務的開始和暫停的,如果你要想對下載完成后的文件進行處理,以及要監聽下載進度的話,就得實現NSURLSessionDownloadDelegate代理中相應的方法了。NSURLSessionDownloadDelegate中有3個代理方法,分別負責處理文件下載完成,監測下載進度以及文件暫停時的處理工作。接下來將要結合上述下載任務的開始和暫停的代碼來探討一下這三個代理方法。
(1)、文件下載完成后的回調----didFinishDownloadingToURL
下方代碼段中的代理方法就是在文件下載完成后要執行的回調方法。在下方的委托回調方法中有三個回調參數,第一個就是我們的downloadSession對象,第二個參數就是我們的downloadTask對象,第三個參數就是臨時文件的下載目錄。臨時文件在下載完成之后如果你不做任何處理的話,那么就會被自動刪除。下方代碼段在獲取臨時文件路徑后將臨時文件使用FileManager將臨時文件存儲到相應的文件夾中,新文件的名字此處取的是當前時間的時間戳,如下所示。
(2)、監聽下載任務----didWriteData
下方代碼片段是用來實時監聽下載進度的回調方法,該方法中有5個回調參數。前兩個就不說了,重點在后三個。didWriteData參數是本次下載的數據,單位為字節,totalBytesWritter參數代表着已經下載的數據總量,totalBytesExpectedToWrite是文件的總量。通過上述三個參數我們不難計算出當前的下載進度,可以在該委托回調方法中進行ProgressiView的更新。具體代碼如下所示
(3)暫停后再次啟動下載任務的代理方法----didResumeAtOffset
下方回調方法會在暫停的下載任務重啟后會被調用。該代理回調方法中有四個回調參數,前兩個就不多說了,我們來看后兩個。fileOffset代表中已經下載文件的大小,expectedTotalBytes表示文件的總大小,如下所示:
至此,NSURLSessionDownloadDelegate中的三個代理方法已介紹完畢。在你做文件下載時上述回調大部分情況下會被使用到。
六、網絡緩存
網絡緩存在網絡請求中使用的還是蠻多的,尤其是加載一些H5頁面時經常會加一些緩存來提高用戶體驗。有時的一些數據也會進行緩存,你可將數據緩存到你的SQLite數據庫、PList文件,或者直接使用NSURLSession相關的東西進行緩存。接下來要介紹的緩存方式就是網絡緩存,就是利用NSURLSession相關的類來實現網絡緩存。該緩存的過程不需要你去操作數據庫或者plist文件,下方給出了四種不同的網絡緩存方式,無論是哪一種網絡緩存的方式只是用法不一樣,本質上是一樣的,都是利用NSURLSession進行的網絡緩存。廢話少說,進入該部分的主題。
1.緩存策略概述
在配置網絡請求緩存時,有着不同的請求緩存策略。下方就是所有支持的網絡緩存策略:
-
UseProtocolCachePolicy -- 緩存存在就讀緩存,若不存在就請求服務器
-
ReloadIgnoringLocalCacheData -- 忽略緩存,直接請求服務器數據
-
ReturnCacheDataElseLoad -- 本地如有緩存就使用,忽略其有效性,無則請求服務器
-
ReturnCacheDataDontLoad -- 直接加載本地緩存,沒有也不請求網絡
-
ReloadIgnoringLocalAndRemoteCacheData -- 尚未實現
-
ReloadRevalidatingCacheData -- 尚未實現
上述緩存策略在Foundation框架中是以枚舉的形式來提現的,該緩存策略的枚舉類型是NSURLRequestCachePolicy,具體定義如下所示:
2.使用NSMutableURLRequest指定緩存策略
接下來我們使用NSMutableURLRequest來指定緩存策略,在NSMutableURLRequest類的對象中有一個參數cachePolicy用來指定緩存策略的,只需要將上述枚舉的緩存策略的枚舉值賦值給cachePolicy即可。下方代碼段就是點擊“Request設置緩存”按鈕所觸發的代碼,在下方代碼中我們使用DataTask對百度的網頁進行請求,將請求的數據使用.ReturnCacheDataElseLoad的緩存策略進行緩存。下方紅框的部分就是使用NSMutableURLRequest對象來設置緩存策略的代碼,具體如下所示:
下方就是點擊“Request設置緩存”按鈕后所呈現的效果,緩存目錄默認為~/Library/Caches/[Boundle ID]/fsCachedData/緩存文件,緩存文件名是按照一定的規則生成的,當然同一個URL所生成的緩存文件名是相同的。下方就是我們所緩存的文件,使用Sublime打開后里邊就是百度的HTML頁面。如下所示。當緩存完畢后,如果你再次發起請求的話就會從緩存文件中進行數據的加載。
3. 使用NSURLSessionConfiguration指定緩存策略
除了直接使用Request對象來指定請求緩存策略,我們還可以使用NSURLSessionConfiguration的對象來指定緩存策略。在NSURLSessionConfiguration類中有一個用來設置請求緩存策略的requestCachePolicy屬性。使用該屬性設置的緩存策略時,同樣的緩存策略所表現的效果與上面直接使用NSURLMutableRequest設置的緩存策略表現是一致的。下方代碼段就是使用NSURLSessionConfiguration對象來設置緩存策略,如下所示:
由於此處的緩存文件與上述一致,如果該請求連接以被上面緩存就會被直接加載。
4.使用URLCache + request進行緩存
上面是使用URLRequest自帶的緩存策略,可定制性和靈活度比較低。如果要對網絡緩存有着較高的定制性的話,我們就得使用NSURLCache這個東西了。雖然NSURLURLCache任然依賴於NSURLRequst對象,不過可以設置一些緩存的參,比如緩存路徑、緩存的最大磁盤容量和內存容量等等。接下來我們就要使用URLCache來進行網絡緩存了。下面的代碼就是對“博客園”首頁的HTML進行的緩存,當然我們在此使用的是URLCache。
在下方代碼中我們先創建了三個常量:memoryCapacity--緩存最大內存容量、diskCapacity--緩存最大磁盤容量、cacheFilePath--緩存路徑。上面這三個常量用來作為初始化NSURLCache對象的參數,創建完NSURLCache對象后我們將其設置成全局的URLCache。緩存策略仍然使用NSURLMutableRequest來指定。具體代碼如下所示。
有一點需要注意的是此處設置的緩存路徑是相對於/Library/Caches/[Boundle ID]/的,會在這個相當路徑下創建相應的文件夾來存放緩存文件。下方就是我們使用NSURLCache緩存的文件路徑已經內容,從內容不難看出就是博客園首頁的HTML代碼。效果如下所示:
5、使用URLCache + NSURLSessionConfiguration進行緩存
你也可以在NSURLSessionConfigurationzhon中指定URLCache對象,當然此處我們使用NSURLSessionConfiguration的對象來指定緩存策略。NSURLSessionConfiguration對象中有一個屬性是URLCache, 我們可以用它來配置URLCache對象。下方代碼就是使用NSURLSessionConfiguration結合着URLCache進行緩存的。緩存效果與上面的一致。
6、清除緩存
誰污染誰治理呢,創建完緩存,如果在不用時我們要對相應的緩存數據進行清理的。清理緩存就是找到緩存所在的文件夾將緩存的文件進行刪除即可。下方代碼段就是對我們上面創建的所有緩存進行清理。因為下方的每行代碼基本上都有注釋,在此就對其做過多的解釋了。主要還是NSFileManager的使用。如下所示:
七、請求認證
有時為了網絡請求的安全性,服務器與客戶端之間要進行身份的驗證。根據安全性的不同要求可以是單向驗證,也可以是雙向驗證。本部分我們就來聊一下NSURLSession發起網絡請求遇到驗證時的處理方案,就以HTTPS證書驗證為例。下方會先介紹認證方式與認證策略,然后結合實例來進一步認識NSURLSession中的請求認證。
1.認證方式
首先我們先來大體的了解一下所有的認證方式
- NSURLAuthenticationMethodHTTPBasic: HTTP基本認證,需要提供用戶名和密碼
- NSURLAuthenticationMethodHTTPDigest: HTTP數字認證,與基本認證相似需要用戶名和密碼
- NSURLAuthenticationMethodHTMLForm: HTML表單認證,需要提供用戶名和密碼
- NSURLAuthenticationMethodNTLM: NTLM認證,NTLM(NT LAN Manager)是一系列旨向用戶提供認證,完整性和機密性的微軟安全協議
- NSURLAuthenticationMethodNegotiate: 協商認證
- NSURLAuthenticationMethodClientCertificate: 客戶端認證,需要客戶端提供認證所需的證書
- NSURLAuthenticationMethodServerTrust: 服務端認證,由認證請求的保護空間提供信任
上面后兩個就是我們在請求HTTPS時會遇到的認證,需要服務器或者客戶端來提供認證的,這個證書就是我們平時常說的CA證書。當然你也可以使用自簽名證書了,這就不在本篇博客的討論范圍內了。
2.認證處理策略
當我們進行網絡求時,會對相應的認證做出響應。在NSURLSession進行網絡請求時支持四種證書處理策略,這些認證處理策略以枚舉的形式來存儲,枚舉的類型為NSURLSessionAuthChallengeDisposition。下方就是認證的所有處理策略:
- UseCredential: 使用證書
- PerformDefaultHandling: 執行默認處理, 類似於該代理沒有被實現一樣,credential參數會被忽略
- CancelAuthenticationChallenge: 取消請求,credential參數同樣會被忽略
- RejectProtectionSpace: 拒絕保護空間,重試下一次認證,credential參數同樣會被忽略
3.HTTPS請求證書處理
接下來我們就根據實例來感受一下上述的認證方式以及認證處理策略,在此我們就以HTTPS的證書認證為例。點擊“NSURLAuthenticationChallenge”按鈕就會執行下方代碼段,在下方代碼段中我們以請求宜信--星火金服的首頁的HTML數據為例。由下方的代碼段我們可以看出星火金服的首頁是https,我們在請求該頁面數據時,肯定會進行證書認證的處理的。下方我們使用的默認會話中的Data Task發起的https請求。
發起上述https請求后,就會執行下方的代理方法。下方的委托代理方法屬於NSURLSessionDelegate中處理認證的方法,也就是如果服務器需要認證時就會執行下方的回調方法。下方代碼首先從授權質疑的保護空間中取出認證方式,然后根據不同的認證方式進行不同的處理。下方給出了兩種認證方式的處理,上面的if語句塊賦值服務端認證,下面的if語句塊負責HTTP的基本認證。具體處理方式如下所示。有一點需要注意的是如果在該委托回調方法中如果不執行completionHandler閉包,那么認證就會失效,是請求不到數據的。
八、NSURLSession相關代理
在AlamoFire框架中用到了好多的NSURLSession的相關代理,AlamoFire框架對NSURLSession的相關代理進行了封裝,使用Closure的形式進行了替換,所以在閱讀AlamoFire源碼之前了解NSURLSession的相關代理方法的功能比較重要的。接下來將要對NSURLSession所有相關的代理方法進行介紹,當然上面已經用到的代理方法在該部分就不重述了。下面的內容首先會整體的介紹一些這些代理的關系,然后各個擊破。
1.SessionDelegate類圖
下方類圖是SessionDelegate相關協議已經SessionTask相關類之間的繼承和依賴關系。上面已經介紹了各種Session Task的使用,當然除了Stream Task之外。Stream Task是iOS9之后添加的東西,用來進行數據流的請求與交互的,在此就不多說了。該部分是對下方類圖中上半部分進行介紹。Session相關的Delegate都繼承在NSURLSessionDelegate,DownloadDelegate、DataDelegate、StreamDelegate則繼承自SessionTaskDelegate。詳細的請看下方類圖。
2.Delegate測試用例
為了進行各種代理的測試,我們創建了下方專門用於代理測試的請求。網絡請求的地址使用的是“https://www.xinghuo365.com”,后面沒有加index.shtml。因為直接請求域名星火金服會進行重定向,正好在我們相應的代理方法中進行請求重定向的處理。點擊“SessionDelegate”按鈕就會執行下方的方法。
3.NSURLSessionDelegate
上面我們在證書認證時實現了一個didReceiveChallenge代理方法,該方法就位於NSURLSessionDelegate代理中。在NSURLSessionDelegate代理中除了didReceiveChallenge代理方法外還有兩個方法。下方截圖中就是這兩個代理方法,
didBecomeInvalidWithError代理方法會在Session無效后被調用,URLSessionDidFinishEventsForBackgroundURLSession該代理方法會在后台Session在執行完后台任務后所執行的方法。
4.NSURLSessionTaskDelegate
接下來我們來介紹NSURLSessionDelegate的子協議NSURLSessionTaskDelegate,當然父協議中的代理方法同樣適用於所有的子協議的。關於NSURLSessionTaskDelegate的代理方法,上面我們在介紹UploadTask時用到了NSURLSessionTaskDelegate協議中的
didSendBodyData代理方法來監聽上傳速度。接下來我們來介紹該代理方法中的其他代理方法。
(1).請求的重定向
當我們請求的地址進行重定向時會執行NSURLSessionTaskDelegate中的willPerformHTTPRedirection方法,我們可以在此代理方法中對重定向的請求進一步的進行處理,甚至在此進行重定向。下方代碼段的截圖就是該URL重定向后要執行的方法,我們在此方法中將重定向的內容再次進行重定向,我們此處是重定向到的百度。具體做法如下所示。
(2)、其他代理方法
下方代碼片段中的三個代理方法是NSURLSessionTaskDelegate中其他的代理方法,下方第一個方法是用來處理認證策略的,與NSURLSessionDelegate中的認證代理使用方式一致,如果你已經實現了NSURLSessionDelegate中的相應的方法,那么此處的認證方法不會被調用。第二個是關於流操作的,因為至今沒有真正用過流試的請求方式再次就不做過多的贅述了。第三個是Session Task執行完畢后會調用的方法,具體如下所示。
5.NSURLSessionDataDelegate
NSURLSessionDataDelegate中的方法主要是用來處理Data Task任務相應的事件的。在介紹NSURLSessionDataDelegate中具體的代理方法之前我們先了解一下NSURLSession中對Data Task相應數據的處理策略。了解完處理策略以后,我們再來一個接一個的介紹NSURLSessionDataDelegate中所對應的回調方法。
(1)、相應處理策略
在Data Task收到相應后,我們可以通過相應的代理方法指定處理策略,所有的處理策略同樣是以枚舉的形式存在的。枚舉類型NSURLSessionResponseDisposition中存儲的就是Data Task的響應處理策略,共有四種處理策略,下方是每種響應處理策略的詳細介紹:
- Cancel :取消數據的加載,默認為 .Cancel。此處理方式就是忽略數據的加載,取消對響應數據的進一步解析。
-
Allow :允許繼續操作, 會執行 NSURLSessionDataDelegate中的dataTaskDidReceiveData回調方法
-
BecomeDownload : 將Data Task的響應轉變為DownloadTask,會執行NSURLSessionDownloadDelegate代理中相應的方法
-
BecomeStream : 將Data Task的響應轉變為DownloadTask,會執行NSURLSessionStreamDelegate代理中相應的方法
(2)、Data Task接收到響應后執行的方法--didReceiveResponse
下方的回調方法會在我們執行Data Task時受到服務器響應時所回調的方法,在該方法中我們就可以指定上述相應的處理策略。下方我們指定的處理策略是Allow,就是允許繼續執行數據的請求和處理。
(3)、受到數據后執行的代理方法--didReceiveData
下方的代理方法就是在執行Data Task時,收到服務器的數據后所執行的方法。也就是上面的處理策略設置成Allow后會執行下方的方法,如果響應處理策略不是Allow那么就不會接收到服務器的Data,從而也不會執行下面的方法。在該方法中我們收到了服務器所返回的二進制數據,下方我們將二進制數據轉成UTF8的字符串編碼。具體代碼如下所示:
(4)、任務轉變所執行的代理方法--didBecomeDownloadTask與didBecomeStreamTask
如果你處理響應的策略是BecomeDownload,那么就會執行下方的第一個回調方法。如果處理策略是BecomeStream那么就會執行下方第二個回調方法。
(5)、將要進行緩存響應----willCacheResponse
如果你在執行Data Task時,如果指定了響應的緩存策略,那么在請求數據完畢會會執行下方的willCacheResponse代理方法。顧名思義,willCacheResponse就是在將要進行緩存的使用多調用的,具體做法如下:
NSURLSessionStreamDelegate是iOS9上提供的,如果Data被轉換成流數據就會執行NSURLSessionStreamDelegate中相應的方法,在streamTask中有一個readDataOfMinLength方法可以讀取流中的數據。因為至今還用過NSURLSessionStreamDelegate,所以關於NSURLSessionStreamDelegate的東西就不做過多贅述了。不過在Github上分享的demo中有NSURLSessionStreamDelegate的相關內容,在此就不做過多的贅述了。
九、監測網絡連接狀態
本部分不屬於NSSession范疇,不過網絡開發怎么能少的了監測網絡狀態的模塊呢。接下來我們將要使用SystemConfiguration來實現reachability。在AlamoFire中也是使用的SystemConfiguration相關的內容來實現的reachability。
下方這個代碼段就是使用SystemConfiguration相關的內容來進行網絡狀態的監測。首先我們先使用SCNetworkReachabilityCreateWithName來創建一個reachability對象,然后創建reachability的上下文,之后在設置網絡狀態改變后的回調,隨后將reachability對象放到隊列中進行執行,具體步驟具體代碼如下所示:
下方代碼段是我們設置的網絡狀態改變后的回調方法,在其中對網絡的狀態進行了處理,具體代碼如下所示,因這部分比較簡單所以在此就不做過多贅述了。
篇幅有限今天博客算是長篇大論了,就先到此,下篇博客會對AlamoFire源碼進行解析。
上述所以代碼Github分享地址為:https://github.com/lizelu/NSURLSessionDemo