1. 程式人生 > >iOS開發之Alamofire原始碼解析前奏--NSURLSession全家桶

iOS開發之Alamofire原始碼解析前奏--NSURLSession全家桶

今天部落格的主題不是Alamofire, 而是iOS網路程式設計中經常使用的NSURLSession。如果你想看權威的NSURLSession的東西,那麼就得去蘋果官方的開發中心去看了,雖然是英文的,但是結合程式碼理解應該不難。更詳細的資訊請移步於蘋果官方介紹,網上好多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     }
View Code

在上述程式碼中,功能並不算複雜。就是遞迴將字典中的所有鍵值對轉變成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認證,NTLMNT 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都繼承在NSURLSessionDelegateDownloadDelegate、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時受到伺服器響應時所回撥的方法,在該方