Swift中的Network Reachability(格式未整理)

crazyostrich 发布于1年前 阅读12333次
0 条评论

绝大多数的手机应用在某一时刻需要通过网络向后台主机或服务器请求数据或者进行数据更新。然而,网络连接并不总是一直处于可用状态,随时都有可能出现断开连接导致不可用的情况。为了解此问题,我们可以通过使用 SCNetworkReachability API 接口来获取系统当前的网络状态和检测应用是否可以连接到后台服务器。

SCNetworkReachability 接口属于 Core Foundation框架的一部分,并用C语言来实现的。对于某些开发人员来说不能直接地使用Swift语言来调用SCNetworkReachability 接口。

苹果提供了一些示例代码封装了一个叫做Reachbility的一个类,它是一个Objective-C封装在scnetworkreachability API。

Reachbility类对网络连接进行了友好的封装,可以通过便捷的方法来检查是否已经连接或者断开了网络。当网络状态改变的时候你也可以通过NSNotification把你的对象注入到NSNotifiactionCenter 来进行及时通知。

你可以在 苹果提供的示例项目 中找到Reachbility类。

如果你对Reachability这个类比较熟悉的话,你只需要使用最新版本更新代码(2015/11/11日发布的4.2版本),4.2版本修复了几个可能导致内存泄漏的BUG。

苹果公司已经提供了Objective-C版本的示例代码,但是很多开发者有对Swift版本的诉求,因此我们将在这篇文章中讲述Swift中的NetWork Reachability的API。我们将会一步步的讲解它的工作原理并用Swift 3.0实现我们自己的Reachability类。

SCNetworkReachability API说明

SCNetworkReachability API 提供一个同步的方法来判断网络是否连接状态。该同步方法允许我们通过调用SCNetworkReachabilityGetFlags函数来获取当前的网络状态。该函数的第二个参数是个指向内存的指针,来作为网络状连接状态的标识参数,此参数还包含一些额外的信息,例如网络建立连接时是否属于自动建立连接还是属于用户干预导致的。

SCNetworkReachability API 除了提供同步方法之外还提供了异步方法。为了实现这个方法,我们必须循环调用SCNetworkReachability 对象,当远程服务器连接状态改变时我们都要提供一个回调函数进行广播通知。

使用Swift实现

启动Xcode 8 并新建一个的 Swift  Single View Application项目,项目命名为ReachabilityExample,在项目中添加一个新的swift文件,命名为Reachability.swift ,添加如import声明:

import SystemConfiguration

我们想在网络出现以下三种情况进行变更时通知我们的app(改:我们想在网络出现以下三种情况的时候通知我们的APP)。

1. 当app失去连接时(改:当APP没有连接网络时)

2. 当app通过 wifi连接时

3. 当app通过 WWAN 连接时

我们将会发送一个包含网络连接状态的通知,下面我们将会为这个通知命名,并定义三种可能的网络状态:

let ReachabilityDidChangeNotificationName = "ReachabilityDidChangeNotification"
enum ReachabilityStatus {
case notReachable
case reachableViaWiFi
case reachableViaWWAN
}

在这个类中添加一个属性来保存SCNetworkReachability对象:

private var networkReachability: SCNetworkReachability?

为了监控目前服务器是否可以连接,我们创建一个初始化方法,把域名为作参数传入,并通过SCNetworkReachabilityCreateWithName 函数初始化

SCNetworkReachability对象 。如果SCNetworkReachability初始化失败则返回nil,所以我们创建一个可失败初始化方法(failable initializer):

init?(hostName: String) {
    networkReachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, (hostName as NSString).UTF8String)
    super.init()
    if networkReachability == nil {
    return nil
    }
}

为了创建一个根据ip网络地址的reachability对象,我们需要实现另外一个初始化方法。这种情况我们将使用

SCNetworkReachabilityCreateWithAddress 函数。由于这个函数需要一个指向网络地址的指针,所以我们称它为withUnsafePointer函数。这种情况下,正如我们前面讲到的那样,函数的返回值可能是nil,所以要使init方法可以失败。

init?(hostAddress: sockaddr_in) {
    var address = hostAddress
 
    guard let defaultRouteReachability = withUnsafePointer(to: &address, {
            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
            SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, $0)
        }
    }) else {
        return nil
    }
 
    networkReachability = defaultRouteReachability
 
    super.init()
    if networkReachability == nil {
        return nil
    }
}

为了方便起见,我们创建两个类方法。第一个方法建一个 reachability的实例来控制网络连接。第二个方法用来检测当我们是否连接的是本地wifi。两个方法都必须使用网络地址来使用构造方法。在本地WIFI的情况下,地址169.254.0.0被定义为IN_LINKLOCALNETNUM。

static func networkReachabilityForInternetConnection() -> Reachability? {
    var zeroAddress = sockaddr_in()
    zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
    zeroAddress.sin_family = sa_family_t(AF_INET)
    return Reachability(hostAddress: zeroAddress)
}
 
static func networkReachabilityForLocalWiFi() -> Reachability? {
    var localWifiAddress = sockaddr_in()
    localWifiAddress.sin_len = UInt8(MemoryLayout.size(ofValue: localWifiAddress))
    localWifiAddress.sin_family = sa_family_t(AF_INET)
    // IN_LINKLOCALNETNUM is defined inas 169.254.0.0 (0xA9FE0000).
    localWifiAddress.sin_addr.s_addr = 0xA9FE0000
 
    return Reachability(hostAddress: localWifiAddress)
}

现在我们需要定定义一个开启通知和一个关闭通知方法,并定义一个属性来标识通知状态当前处于开启状态还是关闭状态:

private var notifying: Bool = false

开启通知之前,先检查通知是否为开启状态。然后获取SCNetworkReachabilityContext容器,并为context 对象的info属性赋值为self。之后设置回调函数,传递context(当回调函数调用时,info参数包含的对self的引用指针将会作为第三个参数传递给block数据).如果设置回调函数成功,我们就能在run loop中管理network reachability的引用。

func startNotifier() -> Bool {
 
    guard notifying == false else {
        return false
    }
 
    var context = SCNetworkReachabilityContext()
    context.info = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
 
    guard let reachability = networkReachability, SCNetworkReachabilitySetCallback(reachability, { (target: SCNetworkReachability, flags: SCNetworkReachabilityFlags, info: UnsafeMutableRawPointer?) in
        if let currentInfo = info {
            let infoObject = Unmanaged.fromOpaque(currentInfo).takeUnretainedValue()
            if infoObject is Reachability {
                let networkReachability = infoObject as! Reachability
                NotificationCenter.default.post(name: Notification.Name(rawValue: ReachabilityDidChangeNotificationName), object: networkReachability)
            }
        }
    }, &context) == true else { return false }
 
    guard SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) == true else { return false }
 
    notifying = true
    return notifying
}

停止通知,我们只需要把network reachability的引用管理从run loop中移除就可以了:

func stopNotifier() {
    if let reachability = networkReachability, notifying == true {
        SCNetworkReachabilityUnscheduleFromRunLoop(reachability, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode as! CFString)
        notifying = false
    }
}

在Reachability 销毁之前确保关闭通知已经关闭:

deinit {
    stopNotifier()
}

为了获取网络连接状态,我们定义一个flags属性来获取 SCNetworkReachability对象:

private var flags: SCNetworkReachabilityFlags {
 
    var flags = SCNetworkReachabilityFlags(rawValue: 0)
 
    if let reachability = networkReachability, withUnsafeMutablePointer(to: &flags, { SCNetworkReachabilityGetFlags(reachability, UnsafeMutablePointer($0)) }) == true {
        return flags
    }
    else {
        return []
    }
}

我创建了一个根据flags传递的值返回网络状态的函数(在方法的注释中解释了flags值所代表的链接状态):

var currentReachabilityStatus: ReachabilityStatus {
    if flags.contains(.reachable) == false {
        // The target host is not reachable.
        return .notReachable
    } 
    else if flags.contains(.isWWAN) == true {
        // WWAN connections are OK if the calling application is using the CFNetwork APIs.
        return .reachableViaWWAN
    } 
    else if flags.contains(.connectionRequired) == false {
        // If the target host is reachable and no connection is required then we'll assume that you're on Wi-Fi...
        return .reachableViaWiFi
    } 
    else if (flags.contains(.connectionOnDemand) == true || flags.contains(.connectionOnTraffic) == true) && flags.contains(.interventionRequired) == false {
        // The connection is on-demand (or on-traffic) if the calling application is using the CFSocketStream or higher APIs and no [user] intervention is needed
        return .reachableViaWiFi
    } 
    else {
        return .notReachable
    }
}

如果你想了解每个flag的含义和用法以及不同之处,请阅读 苹果的文档

最后,我们创造一个函数决定当前的网络连接状态,最终通过一个bool变量检查是否连接:

var isReachable: Bool {
    switch currentReachabilityStatus {
    case .notReachable:
        return false
    case .reachableViaWiFi, .reachableViaWWAN:
        return true
    }
}

如何使用Reachability?

使用新的Reachability非常的简单直白:你创建一个Reachability实例,并启动通知。然后,如果你想根据reachability返回的状态在UI上控制状态的显示。你可以在reachability 通知注入你的视图控制器,直接把reachability状态改变显示到视图上。

下面用一段简单的示例代码来判定我们的 reachability 类。如果连接网络时我可以改变视图控制的颜色为绿色,当断开连接时我们把视图控制器颜色变成红色。

打开ViewController.swift  文件,并在类中新增一个属性:

import UIKit
 
class ViewController: UIViewController {
 
    var reachability: Reachability? = Reachability.reachabilityForInternetConnection()

我们在一个视图控制器viewDidLoad()方法中增加一个观察者来进行reachability通知

override func viewDidLoad() {
        super.viewDidLoad()
     
        NotificationCenter.default.addObserver(self, selector: #selector(reachabilityDidChange(_:)), name: NSNotification.Name(rawValue: ReachabilityDidChangeNotificationName), object: nil)
     
        _ = reachability?.startNotifier()
    }

在deinit中我们关闭通知:

deinit {
        NotificationCenter.default.removeObserver(self)
        reachability?.stopNotifier()
    }

我们通过检查reachability 来决定控制器是否显示绿色。

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        checkReachability()
    }
     
    func checkReachability() {
        guard let r = reachability else { return }
        if r.isReachable  {
            view.backgroundColor = UIColor.green
        } else {
            view.backgroundColor = UIColor.red
        }
    }

当我们接收到一个通知时控会执行以下方法:

func reachabilityDidChange(_ notification: Notification) {
         checkReachability()
     }

当然,你可以使用域名地址调用相应的构造器,如下所示:

var reachability = Reachability(hostName: "www.apple.com")

小结:

现在,希望你已经对SystemConfiguration框架的工作原理有了更好的理解。

查看原文:Swift中的Network Reachability(格式未整理)

需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。