Swift开发进阶AlamofireiOS 进阶开发

Alamofire(8)— 终章(网络监控&通知&下载器封装)

2019-09-21  本文已影响0人  Cooci_和谐学习_不急不躁

😊😊😊Alamofire专题目录,欢迎及时反馈交流 😊😊😊


Alamofire 目录直通车 --- 和谐学习,不急不躁!


非常高兴,这个 Alamofire 篇章马上也结束了!那么这也作为 Alamofire 的终章,给大家介绍整个 Alamofire 剩余的内容,以及下载器封装,最后总结一下!

一、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
}

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)
}

2:open var listener: Listener?

3:networkManager!.startListening() 开启监听

这里也是这个内容点的重点所在

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
}

4:self.notifyListener(flags) 我们看看状态处理以及回调

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
}

3️⃣:小结

网络监听处理,还是非常简单的!代码的思路也没有太恶心,就是通过 SCNetworkReachabilityRef 这个一个内部类去处理网络状态,然后通过对 flags 分情况处理,确定是无网络、还是WIFI、还是蜂窝

三、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 错误处理,这个类的代码也是非常简单的!大家自行阅读以下应该没有太多疑问,这里也就不花篇幅去啰嗦了!

四、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")
    }
}
NotificationCenter.default.post(
                name: Notification.Name.Task.DidComplete,
                object: strongSelf,
                userInfo: [Notification.Key.Task: 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"
    }
}
NotificationCenter.default.post(
    name: Notification.Name.Task.DidResume,
    object: self,
    userInfo: [Notification.Key.Task: task]
)

小结

五、下载器

这里的下载器笔者是基于 Alamofire(2)— 后台下载 继续给大家分析几个关键点

1️⃣:暂停&继续&取消

//MARK: - 暂停/继续/取消
func suspend() {
    self.currentDownloadRequest?.suspend()
}
func resume() {
    self.currentDownloadRequest?.resume()
}
func cancel() {
    self.currentDownloadRequest?.cancel()
}

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)
}

3️⃣:应用程序被用户kill的时候

1:准备条件

我们们在前面Alamofire(2)— 后台下载处理的时候,针对 URLSession 是由要求的

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("保存完毕,你可以断点续传!")
    }
}

当然如果你有特殊封装也可以执行调用 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)

问题一:OK,看似感觉一切都完美(不需要处理),但是错了:我们用户不知道你已经在后台执行了,他有可能下次进来有点击下载(还有UI页面,也没有显示的进度)

问题二:因为 Alamofirerequest 没有创建,所以没有对应的 task

思路:重重压力,我找到了一个非常重要的闭包(URLSession 的属性)-- getTasksWithCompletionHandler 于是有下面这么一段代码

manager.session.getTasksWithCompletionHandler({ (dataTasks, uploadTasks, downloadTasks) in
    print(dataTasks)
    print(uploadTasks)
    print(downloadTasks)
})

5️⃣:如果应用程序creash,但是下载完成

首先这里非常感谢 iOS原生级别后台下载详解 提供的测试总结!Tiercel2 框架一个非常强大的下载框架,推荐大家使用

到这里,这个篇章就分析完毕了!看到这里估计你也对 Alamofire 有了一定的了解。这个篇章完毕,我还是会继续更新(尽管现在掘进iOS人群不多,阅读量不多)但这是我的执着!希望还在iOS行业奋斗的小伙伴,继续加油,守的云开见日出!💪💪💪

就问此时此刻还有谁?45度仰望天空,该死!我这无处安放的魅力!

上一篇下一篇

猜你喜欢

热点阅读