??????Alamofire專題目錄傅寡,歡迎及時反饋交流 ??????
Alamofire 目錄直通車 --- 和諧學(xué)習诬乞,不急不躁埋哟!
非常高興,這個
Alamofire
篇章馬上也結(jié)束了略水!那么這也作為Alamofire
的終章价卤,給大家介紹整個Alamofire
剩余的內(nèi)容,以及下載器封裝渊涝,最后總結(jié)一下慎璧!
一、NetworkReachabilityManager
這個類主要對 SystemConfiguration.framework
中的 SCNetworkReachability
相關(guān)的東西進行封裝的跨释,主要用來管理和監(jiān)聽網(wǎng)絡(luò)狀態(tài)的變化
1??:首先我們來使用監(jiān)聽網(wǎng)絡(luò)狀態(tài)
let networkManager = NetworkReachabilityManager(host: "www.apple.com")
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
/// 網(wǎng)絡(luò)監(jiān)控
networkManager!.listener = {
status in
var message = ""
switch status {
case .unknown:
message = "未知網(wǎng)絡(luò),請檢查..."
case .notReachable:
message = "無法連接網(wǎng)絡(luò),請檢查..."
case .reachable(.wwan):
message = "蜂窩移動網(wǎng)絡(luò),注意節(jié)省流量..."
case .reachable(.ethernetOrWiFi):
message = "WIFI-網(wǎng)絡(luò),使勁造吧..."
}
print("***********\(message)*********")
let alertVC = UIAlertController(title: "網(wǎng)絡(luò)狀況提示", message: message, preferredStyle: .alert)
alertVC.addAction(UIAlertAction(title: "我知道了", style: .default, handler: nil))
self.window?.rootViewController?.present(alertVC, animated: true, completion: nil)
}
networkManager!.startListening()
return true
}
- 用法非常簡單胸私,因為考慮到全局監(jiān)聽,一般都會寫在
didFinishLaunchingWithOptions
- 創(chuàng)建
NetworkReachabilityManager
對象 - 設(shè)置回調(diào)鳖谈,通過回調(diào)的
status
來處理事務(wù) - 最后一定要記得開啟監(jiān)聽(內(nèi)部重點封裝)
2??:底層源碼分析
1:我們首先來看看 NetworkReachabilityManager
的初始化
public convenience init?(host: String) {
guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
self.init(reachability: reachability)
}
private init(reachability: SCNetworkReachability) {
self.reachability = reachability
// 將前面的標志設(shè)置為無保留值岁疼,以表示未知狀態(tài)
self.previousFlags = SCNetworkReachabilityFlags(rawValue: 1 << 30)
}
- 底層源碼里面調(diào)用
SCNetworkReachabilityCreateWithName
創(chuàng)建了reachability
對象,這也是我們SystemConfiguration
下非常非常重要的類! - 保存在這個
reachability
對象缆娃,方便后面持續(xù)使用 - 將前面的標志設(shè)置為無保留值捷绒,以表示未知狀態(tài)
- 其中初始化方法中,也提供了默認創(chuàng)建贯要,該實例監(jiān)視地址
0.0.0.0
- 可達性將
0.0.0.0地址
視為一個特殊的token
暖侨,它可以監(jiān)視設(shè)備的一般路由狀態(tài),包括IPv4和IPv6崇渗。
2:open var listener: Listener?
- 這里也就是對外提供的狀態(tài)回調(diào)閉包
3:networkManager!.startListening()
開啟監(jiān)聽
這里也是這個內(nèi)容點的重點所在
open func startListening() -> Bool {
// 獲取上下文結(jié)構(gòu)信息
var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
context.info = Unmanaged.passUnretained(self).toOpaque()
// 將客戶端分配給目標字逗,當目標的可達性發(fā)生更改時,目標將接收回調(diào)
let callbackEnabled = SCNetworkReachabilitySetCallback(
reachability,
{ (_, flags, info) in
let reachability = Unmanaged<NetworkReachabilityManager>.fromOpaque(info!).takeUnretainedValue()
reachability.notifyListener(flags)
},
&context
)
// 在給定分派隊列上為給定目標調(diào)度或取消調(diào)度回調(diào)
let queueEnabled = SCNetworkReachabilitySetDispatchQueue(reachability, listenerQueue)
// 異步執(zhí)行狀態(tài)宅广,以及通知
listenerQueue.async {
guard let flags = self.flags else { return }
self.notifyListener(flags)
}
return callbackEnabled && queueEnabled
}
- 調(diào)用
SCNetworkReachabilityContext
的初始化扳肛,這個結(jié)構(gòu)體包含用戶指定的數(shù)據(jù)和回調(diào)函數(shù). -
Unmanaged.passUnretained(self).toOpaque()
就是將非托管類引用轉(zhuǎn)換為指針 -
SCNetworkReachabilitySetCallback
:將客戶端分配給目標,當目標的可達性發(fā)生更改時乘碑,目標將接收回調(diào)挖息。(這也是只要我們的網(wǎng)絡(luò)狀態(tài)發(fā)生改變時,就會響應(yīng)的原因) - 在給定分派隊列上為給定目標調(diào)度或取消調(diào)度回調(diào)
- 異步執(zhí)行狀態(tài)信息處理兽肤,并發(fā)出通知
4:self.notifyListener(flags)
我們看看狀態(tài)處理以及回調(diào)
- 調(diào)用了
listener?(networkReachabilityStatusForFlags(flags))
在回調(diào)的時候還內(nèi)部處理了flags
- 這也是可以理解的套腹,我們需要不是一個標志位,而是蜂窩網(wǎng)絡(luò)资铡、WIFI电禀、無網(wǎng)絡(luò)!
func networkReachabilityStatusForFlags(_ flags: SCNetworkReachabilityFlags) -> NetworkReachabilityStatus {
guard isNetworkReachable(with: flags) else { return .notReachable }
var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi)
#if os(iOS)
if flags.contains(.isWWAN) { networkStatus = .reachable(.wwan) }
#endif
return networkStatus
}
- 通過
isNetworkReachable
判斷有無網(wǎng)絡(luò) - 通過
.reachable(.ethernetOrWiFi)
是否存在 WIFI 網(wǎng)絡(luò) - iOS端 還增加了
.reachable(.wwan)
判斷蜂窩網(wǎng)絡(luò)
3??:小結(jié)
網(wǎng)絡(luò)監(jiān)聽處理笤休,還是非常簡單的尖飞!代碼的思路也沒有太惡心,就是通過 SCNetworkReachabilityRef
這個一個內(nèi)部類去處理網(wǎng)絡(luò)狀態(tài),然后通過對 flags
分情況處理政基,確定是無網(wǎng)絡(luò)贞铣、還是WIFI、還是蜂窩
三沮明、AFError錯誤處理
AFError
中將錯誤定義成了五個大類型
// 當“URLConvertible”類型無法創(chuàng)建有效的“URL”時返回辕坝。
case invalidURL(url: URLConvertible)
// 當參數(shù)編碼對象在編碼過程中拋出錯誤時返回。
case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
// 當多部分編碼過程中的某個步驟失敗時返回荐健。
case multipartEncodingFailed(reason: MultipartEncodingFailureReason)
// 當“validate()”調(diào)用失敗時返回酱畅。
case responseValidationFailed(reason: ResponseValidationFailureReason)
// 當響應(yīng)序列化程序在序列化過程中遇到錯誤時返回。
case responseSerializationFailed(reason: ResponseSerializationFailureReason)
這里通過對枚舉拓展了計算屬性江场,來直接對錯誤類型進行 if判斷
纺酸,不用在 switch
一個一個判斷了
extension AFError {
// 返回AFError是否為無效URL錯誤
public var isInvalidURLError: Bool {
if case .invalidURL = self { return true }
return false
}
// 返回AFError是否是參數(shù)編碼錯誤。
// 當“true”時址否,“underlyingError”屬性將包含關(guān)聯(lián)的值餐蔬。
public var isParameterEncodingError: Bool {
if case .parameterEncodingFailed = self { return true }
return false
}
// 返回AFError是否是多部分編碼錯誤。
// 當“true”時在张,“url”和“underlyingError”屬性將包含相關(guān)的值。
public var isMultipartEncodingError: Bool {
if case .multipartEncodingFailed = self { return true }
return false
}
// 返回“AFError”是否為響應(yīng)驗證錯誤矮慕。
// 當“true”時帮匾,“acceptableContentTypes”、“responseContentType”和“responseCode”屬性將包含相關(guān)的值痴鳄。
public var isResponseValidationError: Bool {
if case .responseValidationFailed = self { return true }
return false
}
// 返回“AFError”是否為響應(yīng)序列化錯誤瘟斜。
// 當“true”時,“failedStringEncoding”和“underlyingError”屬性將包含相關(guān)的值痪寻。
public var isResponseSerializationError: Bool {
if case .responseSerializationFailed = self { return true }
return false
}
}
小結(jié)
AFError
錯誤處理螺句,這個類的代碼也是非常簡單的!大家自行閱讀以下應(yīng)該沒有太多疑問,這里也就不花篇幅去啰嗦了橡类!
四蛇尚、Notifications & Validation
Notifications 核心重點
extension Notification.Name {
/// Used as a namespace for all `URLSessionTask` related notifications.
public struct Task {
/// Posted when a `URLSessionTask` is resumed. The notification `object` contains the resumed `URLSessionTask`.
public static let DidResume = Notification.Name(rawValue: "org.alamofire.notification.name.task.didResume")
/// Posted when a `URLSessionTask` is suspended. The notification `object` contains the suspended `URLSessionTask`.
public static let DidSuspend = Notification.Name(rawValue: "org.alamofire.notification.name.task.didSuspend")
/// Posted when a `URLSessionTask` is cancelled. The notification `object` contains the cancelled `URLSessionTask`.
public static let DidCancel = Notification.Name(rawValue: "org.alamofire.notification.name.task.didCancel")
/// Posted when a `URLSessionTask` is completed. The notification `object` contains the completed `URLSessionTask`.
public static let DidComplete = Notification.Name(rawValue: "org.alamofire.notification.name.task.didComplete")
}
}
-
Notification.Name
通過擴展了一個Task
這樣的結(jié)構(gòu)體,把跟task
相關(guān)的通知都綁定在這個Task
上顾画,因此取劫,在代碼中就可以這么使用:
NotificationCenter.default.post(
name: Notification.Name.Task.DidComplete,
object: strongSelf,
userInfo: [Notification.Key.Task: task]
)
-
Notification.Name.Task.DidComplete
表達的非常清晰,一般都能知道是task
請求完成之后的通知研侣。再也不需要惡心的字符串谱邪,需要匹配,萬一寫錯了庶诡,那么也是一種隱藏的危機惦银!
Notification userinfo&key 拓展
extension Notification {
/// Used as a namespace for all `Notification` user info dictionary keys.
public struct Key {
/// User info dictionary key representing the `URLSessionTask` associated with the notification.
public static let Task = "org.alamofire.notification.key.task"
/// User info dictionary key representing the responseData associated with the notification.
public static let ResponseData = "org.alamofire.notification.key.responseData"
}
}
- 擴展了
Notification
,新增了一個Key結(jié)構(gòu)體
,這個結(jié)構(gòu)體用于取出通知中的userInfo。
- 使用
userInfo[Notification.Key.ResponseData] = data
NotificationCenter.default.post(
name: Notification.Name.Task.DidResume,
object: self,
userInfo: [Notification.Key.Task: task]
)
- 設(shè)計的本質(zhì)就是為了更加簡潔扯俱!大家也可以從這種思維得出一些想法運用到實際開發(fā)中: 按照自己的業(yè)務(wù)創(chuàng)建不同的結(jié)構(gòu)體就可以了书蚪。
小結(jié)
-
Notifications
其實是一個Task結(jié)構(gòu)體
,該結(jié)構(gòu)體中定義了一些字符串蘸吓,這些字符串就是所需通知的key
善炫,當網(wǎng)絡(luò)請求DidResume、DIdSuspend库继、DIdCancel箩艺、DidComplete
都會發(fā)出通知。 -
Validation
主要是用來驗證請求是否成功宪萄,如果出錯了就做相應(yīng)的處理
五艺谆、下載器
這里的下載器筆者是基于 Alamofire(2)— 后臺下載 繼續(xù)給大家分析幾個關(guān)鍵點
1??:暫停&繼續(xù)&取消
//MARK: - 暫停/繼續(xù)/取消
func suspend() {
self.currentDownloadRequest?.suspend()
}
func resume() {
self.currentDownloadRequest?.resume()
}
func cancel() {
self.currentDownloadRequest?.cancel()
}
- 通過我們的下載事務(wù)管理者:
Request
管理task
任務(wù)的生命周期 - 其中task事務(wù)就是通過調(diào)用
suspend
和resume
方法 -
cancel
里面調(diào)用:downloadDelegate.downloadTask.cancel { self.downloadDelegate.resumeData = $0 }
保存了取消時候的resumeData
2??:斷點續(xù)傳
斷點續(xù)傳的重點:就是保存響應(yīng) resumeData
,然后調(diào)用:manager.download(resumingWith: resumeData)
if let resumeData = currentDownloadRequest?.resumeData {
let documentUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
let fileUrl = documentUrl?.appendingPathComponent("resumeData.tmp")
try! resumeData.write(to: fileUrl!)
currentDownloadRequest = LGDowloadManager.shared.manager.download(resumingWith: resumeData)
}
- 看到這里大家也就能感受到其實斷點續(xù)傳最重要的是保存
resumeData
- 然后處理文件路徑拜英,保存
- 最后調(diào)用
download(resumingWith: resumeData)
就可以輕松實現(xiàn)斷點續(xù)傳
3??:應(yīng)用程序被用戶kill的時候
1:準備條件
我們們在前面Alamofire(2)— 后臺下載處理的時候静汤,針對 URLSession
是由要求的
- 必須使用
background(withIdentifier:)
方法創(chuàng)建URLSessionConfiguration
,其中這個identifier
必須是固定的居凶,而且為了避免跟其他App
沖突虫给,建議這個identifier
跟應(yīng)用程序的Bundle ID
相關(guān),保證唯一 - 創(chuàng)建URLSession的時候侠碧,必須傳入delegate
- 必須在App啟動的時候創(chuàng)建
Background Sessions
抹估,即它的生命周期跟App幾乎一致,為方便使用弄兜,最好是作為AppDelegate
的屬性药蜻,或者是全局變量。
2:測試反饋
OK替饿,準備好了條件语泽,我們開始測試!當應(yīng)用程序被用戶殺死的時候,再回來视卢!
?? 我們驚人的發(fā)現(xiàn)踱卵,會報錯:load failed with error Error Domain=NSURLErrorDomain Code=-999
, 這個BUG 我可是經(jīng)常看見据过,于是飛快定位:
urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
?? 果然應(yīng)用程序會回到完成代理颊埃,大家如果細心想一想也是可以理解的:應(yīng)用程序被用戶kill,也是舒服用戶取消,這個任務(wù)執(zhí)行失敗暗恪班利! ??
3:處理事務(wù)
if let error = error {
if let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
LGDowloadManager.shared.resumeData = resumeData
print("保存完畢,你可以斷點續(xù)傳!")
}
}
- 錯誤獲取,然后轉(zhuǎn)成相應(yīng)
NSError
- 通過
error
獲取里面inifo
, 再通過key
拿到相應(yīng)的resumeData
- 因為前面這個已經(jīng)保證了生命周期的單利,就可以啟動應(yīng)用程序的時候保存
- 下次點擊同一個
URL
下載的時候榨呆,只要取出對應(yīng)的task
保存的resumeData
- 執(zhí)行
download(resumingWith: resumeData)
完美罗标!
當然如果你有特殊封裝也可以執(zhí)行調(diào)用 Alamofire
封裝的閉包
manager.delegate.taskDidComplete = { (session, task, error) in
print("**************")
if let error = error {
if let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
LGDowloadManager.shared.resumeData = resumeData
print("保存完畢,你可以斷點續(xù)傳!")
}
}
print("**************")
}
4??:APP Crash或者被系統(tǒng)關(guān)閉時候
問題
這里我們在實際開發(fā)過程中庸队,也會遇到各種各樣的BUG,那么在下載的時候 APP Crash
也是完全可能的闯割!問題在于:我們這個時候怎么辦彻消?
思考
我們通過上面的條件,發(fā)現(xiàn)其實 apple
針對下載任務(wù)是有特殊處理的宙拉!我把它理解是在另一進程處理的宾尚!下載程序的代理方法還是會繼續(xù)執(zhí)行!那么我在直接把所有下載相關(guān)代理方法全部斷點
測試結(jié)果
// 告訴委托下載任務(wù)已完成下載
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL)
// 下載進度也會不斷執(zhí)行
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64)
- 我們的程序回來谢澈,會在后臺默默執(zhí)行
-
urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
完成也會調(diào)用
問題一:OK煌贴,看似感覺一切都完美(不需要處理),但是錯了:我們用戶不知道你已經(jīng)在后臺執(zhí)行了锥忿,他有可能下次進來有點擊下載(還有UI頁面牛郑,也沒有顯示的進度)
問題二:因為 Alamofire
的 request
沒有創(chuàng)建,所以沒有對應(yīng)的 task
思路:重重壓力敬鬓,我找到了一個非常重要的閉包(URLSession
的屬性)-- getTasksWithCompletionHandler
于是有下面這么一段代碼
manager.session.getTasksWithCompletionHandler({ (dataTasks, uploadTasks, downloadTasks) in
print(dataTasks)
print(uploadTasks)
print(downloadTasks)
})
- 這個閉包能夠監(jiān)聽到當前
session
里正在執(zhí)行的任務(wù),我們只需要便利找到響應(yīng)的Task
- 然后利用緩存把
task
對應(yīng)url
保存起來 - 下次用戶再點擊相同
url
的時候淹朋,就判斷讀取就OK,如果存在就不需要開啟新的任務(wù)钉答,只要告訴用戶已經(jīng)開始下載就OK础芍,UI頁面處理而已 - 進度呢?也很簡單畢竟代理在后臺持續(xù)進行数尿,我們只需要在
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
代理里面匹配downloadTask
保存進度仑性,然后更新界面就OK! - 細節(jié):
didFinishDownloadingTo
記得對下載回來的文件進行路徑轉(zhuǎn)移砌创!
5??:如果應(yīng)用程序creash,但是下載完成
首先這里非常感謝 iOS原生級別后臺下載詳解 提供的測試總結(jié)虏缸!Tiercel2 框架一個非常強大的下載框架鲫懒,推薦大家使用
- 在前臺:跟普通的
downloadTask
一樣嫩实,調(diào)用相關(guān)的session代理方法
- 在后臺:當
Background Sessions
里面所有的任務(wù)(注意是所有任務(wù),不單單是下載任務(wù))都完成后窥岩,會調(diào)用AppDelegate
的application(_:handleEventsForBackgroundURLSession:completionHandler:)
方法甲献,激活App
,然后跟在前臺時一樣颂翼,調(diào)用相關(guān)的session代理方法
晃洒,最后再調(diào)用urlSessionDidFinishEvents(forBackgroundURLSession:)
方法 -
crash
或者App被系統(tǒng)關(guān)閉
:當Background Sessions
里面所有的任務(wù)(注意是所有任務(wù),不單單是下載任務(wù))都完成后朦乏,會自動啟動App
球及,調(diào)用AppDelegate的application(_:didFinishLaunchingWithOptions:)
方法,然后調(diào)用application(_:handleEventsForBackgroundURLSession:completionHandler:)
方法呻疹,當創(chuàng)建了對應(yīng)的Background Sessions
后吃引,才會跟在前臺時一樣,調(diào)用相關(guān)的session
代理方法,最后再調(diào)用urlSessionDidFinishEvents(forBackgroundURLSession:)
方法 -
crash
或者App被系統(tǒng)關(guān)閉
镊尺,打開App
保持前臺朦佩,當所有的任務(wù)都完成后才創(chuàng)建對應(yīng)的Background Sessions:
沒有創(chuàng)建session
時,只會調(diào)用AppDelegate的application(_:handleEventsForBackgroundURLSession:completionHandler:)
方法庐氮,當創(chuàng)建了對應(yīng)的Background Sessions
后语稠,才會跟在前臺時一樣,調(diào)用相關(guān)的session
代理方法弄砍,最后再調(diào)用urlSessionDidFinishEvents(forBackgroundURLSession:)
方法 -
crash
或者App被系統(tǒng)關(guān)閉
仙畦,打開App
,創(chuàng)建對應(yīng)的Background Sessions
后所有任務(wù)才完成:跟在前臺的時候一樣
到這里输枯,這個篇章就分析完畢了议泵!看到這里估計你也對
Alamofire
有了一定的了解。這個篇章完畢桃熄,我還是會繼續(xù)更新(盡管現(xiàn)在掘進iOS人群不多先口,閱讀量不多)但這是我的執(zhí)著!希望還在iOS行業(yè)奮斗的小伙伴瞳收,繼續(xù)加油碉京,守的云開見日出!??????就問此時此刻還有誰螟深?45度仰望天空谐宙,該死!我這無處安放的魅力界弧!