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度仰望天空,该死!我这无处安放的魅力!

相关文章
相关标签/搜索