Alamofire(8)— 終章(網絡監控&通知&下載器封裝)

😊😊😊Alamofire專題目錄,歡迎及時反饋交流 😊😊😊git


Alamofire 目錄直通車 --- 和諧學習,不急不躁!github


很是高興,這個 Alamofire 篇章立刻也結束了!那麼這也做爲 Alamofire 的終章,給你們介紹整個 Alamofire 剩餘的內容,以及下載器封裝,最後總結一下!swift

1、NetworkReachabilityManager

這個類主要對 SystemConfiguration.framework 中的 SCNetworkReachability 相關的東西進行封裝的,主要用來管理和監聽網絡狀態的變化緩存

1️⃣:首先咱們來使用監聽網絡狀態

let networkManager = NetworkReachabilityManager(host: "www.apple.com")

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    /// 網絡監控
    networkManager!.listener = {
        status in
        var message = ""
        switch status {
        case .unknown:
            message = "未知網絡,請檢查..."
        case .notReachable:
            message = "沒法鏈接網絡,請檢查..."
        case .reachable(.wwan):
            message = "蜂窩移動網絡,注意節省流量..."
        case .reachable(.ethernetOrWiFi):
            message = "WIFI-網絡,使勁造吧..."
        }
        print("***********\(message)*********")
        let alertVC = UIAlertController(title: "網絡情況提示", 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
}
複製代碼
  • 用法很是簡單,由於考慮到全局監聽,通常都會寫在didFinishLaunchingWithOptions
  • 建立 NetworkReachabilityManager 對象
  • 設置回調,經過回調的 status 來處理事務
  • 最後必定要記得開啓監聽(內部重點封裝)

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
    // 將前面的標誌設置爲無保留值,以表示未知狀態
    self.previousFlags = SCNetworkReachabilityFlags(rawValue: 1 << 30)
}
複製代碼
  • 底層源碼裏面調用 SCNetworkReachabilityCreateWithName 建立了 reachability 對象,這也是咱們 SystemConfiguration 下很是很是重要的類!
  • 保存在這個 reachability 對象,方便後面持續使用
  • 將前面的標誌設置爲無保留值,以表示未知狀態
  • 其中初始化方法中,也提供了默認建立,該實例監視地址 0.0.0.0
  • 可達性將 0.0.0.0地址 視爲一個特殊的 token,它能夠監視設備的通常路由狀態,包括 IPv4和IPv6。

2:open var listener: Listener?網絡

  • 這裏也就是對外提供的狀態回調閉包

3:networkManager!.startListening() 開啓監聽session

這裏也是這個內容點的重點所在閉包

open func startListening() -> Bool {
    // 獲取上下文結構信息
    var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
    context.info = Unmanaged.passUnretained(self).toOpaque()
    // 將客戶端分配給目標,當目標的可達性發生更改時,目標將接收回調
    let callbackEnabled = SCNetworkReachabilitySetCallback(
        reachability,
        { (_, flags, info) in
            let reachability = Unmanaged<NetworkReachabilityManager>.fromOpaque(info!).takeUnretainedValue()
            reachability.notifyListener(flags)
        },
        &context
    )
    // 在給定分派隊列上爲給定目標調度或取消調度回調
    let queueEnabled = SCNetworkReachabilitySetDispatchQueue(reachability, listenerQueue)
    // 異步執行狀態,以及通知
    listenerQueue.async {
        guard let flags = self.flags else { return }
        self.notifyListener(flags)
    }
    return callbackEnabled && queueEnabled
}
複製代碼
  • 調用SCNetworkReachabilityContext的初始化,這個結構體包含用戶指定的數據和回調函數.
  • Unmanaged.passUnretained(self).toOpaque()就是將非託管類引用轉換爲指針
  • SCNetworkReachabilitySetCallback:將客戶端分配給目標,當目標的可達性發生更改時,目標將接收回調。(這也是隻要咱們的網絡狀態發生改變時,就會響應的緣由)
  • 在給定分派隊列上爲給定目標調度或取消調度回調
  • 異步執行狀態信息處理,併發出通知

4:self.notifyListener(flags) 咱們看看狀態處理以及回調併發

  • 調用了listener?(networkReachabilityStatusForFlags(flags)) 在回調的時候還內部處理了 flags
  • 這也是能夠理解的,咱們須要不是一個標誌位,而是蜂窩網絡、WIFI、無網絡!
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 判斷有無網絡
  • 經過 .reachable(.ethernetOrWiFi) 是否存在 WIFI 網絡
  • iOS端 還增長了 .reachable(.wwan) 判斷蜂窩網絡

3️⃣:小結

網絡監聽處理,仍是很是簡單的!代碼的思路也沒有太噁心,就是經過 SCNetworkReachabilityRef 這個一個內部類去處理網絡狀態,而後經過對 flags 分狀況處理,肯定是無網絡、仍是WIFI、仍是蜂窩app

3、AFError錯誤處理

AFError中將錯誤定義成了五個大類型

// 當「URLConvertible」類型沒法建立有效的「URL」時返回。
case invalidURL(url: URLConvertible)
// 當參數編碼對象在編碼過程當中拋出錯誤時返回。
case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
// 當多部分編碼過程當中的某個步驟失敗時返回。
case multipartEncodingFailed(reason: MultipartEncodingFailureReason)
// 當「validate()」調用失敗時返回。
case responseValidationFailed(reason: ResponseValidationFailureReason)
// 當響應序列化程序在序列化過程當中遇到錯誤時返回。
case responseSerializationFailed(reason: ResponseSerializationFailureReason)
複製代碼

這裏經過對枚舉拓展了計算屬性,來直接對錯誤類型進行 if判斷,不用在 switch 一個一個判斷了

extension AFError {
    // 返回AFError是否爲無效URL錯誤
    public var isInvalidURLError: Bool {
        if case .invalidURL = self { return true }
        return false
    }
    // 返回AFError是不是參數編碼錯誤。
    // 當「true」時,「underlyingError」屬性將包含關聯的值。
    public var isParameterEncodingError: Bool {
        if case .parameterEncodingFailed = self { return true }
        return false
    }
    // 返回AFError是不是多部分編碼錯誤。
    // 當「true」時,「url」和「underlyingError」屬性將包含相關的值。
    public var isMultipartEncodingError: Bool {
        if case .multipartEncodingFailed = self { return true }
        return false
    }
    // 返回「AFError」是否爲響應驗證錯誤。
    // 當「true」時,「acceptableContentTypes」、「responseContentType」和「responseCode」屬性將包含相關的值。
    public var isResponseValidationError: Bool {
        if case .responseValidationFailed = self { return true }
        return false
    }
    // 返回「AFError」是否爲響應序列化錯誤。
    // 當「true」時,「failedStringEncoding」和「underlyingError」屬性將包含相關的值。
    public var isResponseSerializationError: Bool {
        if case .responseSerializationFailed = self { return true }
        return false
    }
}
複製代碼

小結

AFError 錯誤處理,這個類的代碼也是很是簡單的!你們自行閱讀如下應該沒有太多疑問,這裏也就不花篇幅去囉嗦了!

4、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 這樣的結構體,把跟 task 相關的通知都綁定在這個 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結構體,這個結構體用於取出通知中的 userInfo。
  • 使用 userInfo[Notification.Key.ResponseData] = data
NotificationCenter.default.post(
    name: Notification.Name.Task.DidResume,
    object: self,
    userInfo: [Notification.Key.Task: task]
)
複製代碼
  • 設計的本質就是爲了更加簡潔!你們也能夠從這種思惟得出一些想法運用到實際開發中: 按照本身的業務建立不一樣的結構體就能夠了。

小結

  • Notifications 實際上是一個 Task結構體,該結構體中定義了一些字符串,這些字符串就是所需通知的 key,當網絡請求 DidResume、DIdSuspend、DIdCancel、DidComplete 都會發出通知。
  • Validation 主要是用來驗證請求是否成功,若是出錯了就作相應的處理

5、下載器

這裏的下載器筆者是基於 Alamofire(2)— 後臺下載 繼續給你們分析幾個關鍵點

1️⃣:暫停&繼續&取消

//MARK: - 暫停/繼續/取消
func suspend() {
    self.currentDownloadRequest?.suspend()
}
func resume() {
    self.currentDownloadRequest?.resume()
}
func cancel() {
    self.currentDownloadRequest?.cancel()
}
複製代碼
  • 經過咱們的下載事務管理者:Request 管理 task 任務的生命週期
  • 其中task事務就是經過調用 suspendresume 方法
  • cancel 裏面調用:downloadDelegate.downloadTask.cancel { self.downloadDelegate.resumeData = $0 } 保存了取消時候的 resumeData

2️⃣:斷點續傳

斷點續傳的重點:就是保存響應 resumeData,而後調用: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)
}
複製代碼
  • 看到這裏你們也就能感覺到其實斷點續傳最重要的是保存resumeData
  • 而後處理文件路徑,保存
  • 最後調用 download(resumingWith: resumeData) 就能夠輕鬆實現斷點續傳

3️⃣:應用程序被用戶kill的時候

1:準備條件

咱們們在前面Alamofire(2)— 後臺下載處理的時候,針對 URLSession 是由要求的

  • 必須使用 background(withIdentifier:) 方法建立 URLSessionConfiguration,其中這個 identifier 必須是固定的,並且爲了不跟 其餘App 衝突,建議這個identifier 跟應用程序的 Bundle ID相關,保證惟一
  • 建立URLSession的時候,必須傳入delegate
  • 必須在App啓動的時候建立 Background Sessions,即它的生命週期跟App幾乎一致,爲方便使用,最好是做爲 AppDelegate 的屬性,或者是全局變量。

2:測試反饋

OK,準備好了條件,咱們開始測試!當應用程序被用戶殺死的時候,再回來!

⚠️ 咱們驚人的發現,會報錯:load failed with error Error Domain=NSURLErrorDomain Code=-999, 這個BUG 我但是常常看見,因而飛快定位:

urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)

😲 果真應用程序會回到完成代理,你們若是細心想想也是能夠理解的:應用程序被用戶kill,也是舒服用戶取消,這個任務執行失敗啊! 😲

3:處理事務

if let error = error {
    if let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
        LGDowloadManager.shared.resumeData = resumeData
        print("保存完畢,你能夠斷點續傳!")
    }
}
複製代碼
  • 錯誤獲取,而後轉成相應 NSError
  • 經過 error 獲取裏面 inifo , 再經過 key 拿到相應的 resumeData
  • 由於前面這個已經保證了生命週期的單利,就能夠啓動應用程序的時候保存
  • 下次點擊同一個URL下載的時候,只要取出對應的 task 保存的 resumeData
  • 執行download(resumingWith: resumeData) 完美!

固然若是你有特殊封裝也能夠執行調用 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("保存完畢,你能夠斷點續傳!")
        }
    }
    print("**************")
}
複製代碼

4️⃣:APP Crash或者被系統關閉時候

問題

這裏咱們在實際開發過程當中,也會遇到各類各樣的BUG,那麼在下載的時候 APP Crash 也是徹底可能的!問題在於:咱們這個時候怎麼辦?

思考

咱們經過上面的條件,發現其實 apple 針對下載任務是有特殊處理的!我把它理解是在另外一進程處理的!下載程序的代理方法仍是會繼續執行!那麼我在直接把全部下載相關代理方法所有斷點

測試結果

// 告訴委託下載任務已完成下載
func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
// 下載進度也會不斷執行
func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
複製代碼
  • 咱們的程序回來,會在後臺默默執行
  • urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) 完成也會調用

問題一:OK,看似感受一切都完美(不須要處理),可是錯了:咱們用戶不知道你已經在後臺執行了,他有可能下次進來有點擊下載(還有UI頁面,也沒有顯示的進度)

問題二:由於 Alamofirerequest 沒有建立,因此沒有對應的 task

思路:重重壓力,我找到了一個很是重要的閉包(URLSession 的屬性)-- getTasksWithCompletionHandler 因而有下面這麼一段代碼

manager.session.getTasksWithCompletionHandler({ (dataTasks, uploadTasks, downloadTasks) in
    print(dataTasks)
    print(uploadTasks)
    print(downloadTasks)
})
複製代碼
  • 這個閉包可以監聽到當前session里正在執行的任務,咱們只須要便利找到響應的 Task
  • 而後利用緩存把 task 對應 url 保存起來
  • 下次用戶再點擊相同 url 的時候,就判斷讀取就OK,若是存在就不須要開啓新的任務,只要告訴用戶已經開始下載就OK,UI頁面處理而已
  • 進度呢?也很簡單畢竟代理在後臺持續進行,咱們只須要在 func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) 代理裏面匹配 downloadTask 保存進度,而後更新界面就OK!
  • 細節:didFinishDownloadingTo 記得對下載回來的文件進行路徑轉移!

5️⃣:若是應用程序creash,可是下載完成

首先這裏很是感謝 iOS原生級別後臺下載詳解 提供的測試總結!Tiercel2 框架一個很是強大的下載框架,推薦你們使用

  • 在前臺:跟普通的 downloadTask 同樣,調用相關的 session代理方法
  • 在後臺:當 Background Sessions 裏面全部的任務(注意是全部任務,不僅僅是下載任務)都完成後,會調用 AppDelegateapplication(_:handleEventsForBackgroundURLSession:completionHandler:) 方法,激活 App,而後跟在前臺時同樣,調用相關的session代理方法,最後再調用 urlSessionDidFinishEvents(forBackgroundURLSession:) 方法
  • crash 或者 App被系統關閉:當 Background Sessions 裏面全部的任務(注意是全部任務,不僅僅是下載任務)都完成後,會自動啓動App,調用 AppDelegate的application(_:didFinishLaunchingWithOptions:) 方法,而後調用 application(_:handleEventsForBackgroundURLSession:completionHandler:) 方法,當建立了對應的Background Sessions 後,纔會跟在前臺時同樣,調用相關的 session 代理方法,最後再調用 urlSessionDidFinishEvents(forBackgroundURLSession:) 方法
  • crash 或者 App被系統關閉,打開 App 保持前臺,當全部的任務都完成後才建立對應的 Background Sessions:沒有建立 session 時,只會調用 AppDelegate的application(_:handleEventsForBackgroundURLSession:completionHandler:) 方法,當建立了對應的 Background Sessions 後,纔會跟在前臺時同樣,調用相關的 session 代理方法,最後再調用 urlSessionDidFinishEvents(forBackgroundURLSession:) 方法
  • crash 或者 App被系統關閉,打開 App,建立對應的 Background Sessions 後全部任務才完成:跟在前臺的時候同樣

到這裏,這個篇章就分析完畢了!看到這裏估計你也對 Alamofire 有了必定的瞭解。這個篇章完畢,我仍是會繼續更新(儘管如今掘進iOS人羣很少,閱讀量很少)但這是個人執着!但願還在iOS行業奮鬥的小夥伴,繼續加油,守的雲開見日出!💪💪💪

就問此時此刻還有誰?45度仰望天空,該死!我這無處安放的魅力!

相關文章
相關標籤/搜索