Alamofire源碼解讀系列(七)之網絡監控(NetworkReachabilityManager)
本篇主要講解iOS開發中的網絡監控
前言
在開發中,有時候我們需要獲取這些信息:
- 手機是否聯網
- 當前網絡是WiFi還是蜂窩
那么我總結一下具體的使用場景有哪些?肯定有遺漏:
- 聊天列表,需要實時監控當前的網絡是不是可達的,如果不可達,則出現不能聯網的提示
- 在線視屏播放,需要判斷當前的網絡狀態,如果不是WiFi,應該給出流量播放的提示
- 對於比較重要的網絡請求,在請求出錯的情況下,判斷網路狀態,找出請求失敗原因。
- 可以把請求進行緩存后,當監聽到網絡連接成功后發送。舉個例子,每次進app都要把位置信息發給服務器,如果發送失敗后,發現是網絡不可達造成的失敗,那么可以把這個請求放入到一個隊列中,在網絡可達的時候,開啟隊列任務。
- 當網絡狀態變化時,實時的給用戶提示信息
- 獲取某個節點或地址是不是可達的
但是,極其不建議在發請求前,先檢測當前的網絡是不是可達。因為手機的網絡狀態是經常變化的》
SCNetworkReachabilityFlags
SCNetworkReachabilityFlags是獲取網絡狀態最核心的東西。我們來看看它有哪些內容:
作用
SCNetworkReachabilityFlags能夠判斷某個指定的網絡節點名稱或者地址是不是可達的,也能判斷該節點或地址是不是需要先建立連接,也可以判斷是不是需要用戶手動去建立連接。
注意:這里所說的連接分為用編程手段連接和用手動建立連接兩種
我們只列舉出跟本類相關的一些選項:
kSCNetworkReachabilityFlagsReachable
表明當前指定的節點或地址是可達的。注意:可達不是代表節點或地址接受到了數據,而是代表數據能夠離開本地,因此。就算是可達的,也不一定能夠發送成功kSCNetworkReachabilityFlagsConnectionRequired
表明要想和指定的節點或地址通信,需要先建立連接。比如說撥號上網。注意:對於手機來說,如果沒有返回該標記,就說明手機正在使用蜂窩網路或者WiFikSCNetworkReachabilityFlagsConnectionOnTraffic
表明要想和指定的節點或地址通信,必須先建立連接,但是在當前的網絡配置下,目標是可達的。注意:任何連接到指定的節點或地址的請求都會觸發該標記,舉個例子,在很多地方需要輸入手機,獲取驗證碼后才能聯網,就是這個原理kSCNetworkReachabilityFlagsConnectionOnDemand
表明要想和指定的節點或地址通信,必須先建立連接,但是在當前的網絡配置下,目標是可達的。但是建立連接必須通過CFSocketStream APIs
才行,其他的APIs不能建立連接kSCNetworkReachabilityFlagsInterventionRequired
表明要想和指定的節點或地址通信,必須先建立連接,但是在當前的網絡配置下,目標是可達的。需要用戶手動提供一些數據,比如密碼或者tokenkSCNetworkReachabilityFlagsIsWWAN
表明是不是通過蜂窩網絡連接
上邊的這些選項,會在下邊的一個核心方法中使用到,我們在下邊的代碼中在給出說明。
ConnectionType
/// Defines the various connection types detected by reachability flags.
///
/// - ethernetOrWiFi: The connection type is either over Ethernet or WiFi.
/// - wwan: The connection type is a WWAN connection.
public enum ConnectionType {
case ethernetOrWiFi
case wwan
}
對於手機而言,我們需要的連接類型就兩種,一種是蜂窩網絡,另一種是WiFi網絡。因此在設計NetworkReachabilityManager的時候,通過上邊的枚舉獲取當前的網絡連接類型。
NetworkReachabilityStatus
/// Defines the various states of network reachability.
///
/// - unknown: It is unknown whether the network is reachable.
/// - notReachable: The network is not reachable.
/// - reachable: The network is reachable.
public enum NetworkReachabilityStatus {
case unknown
case notReachable
case reachable(ConnectionType)
}
網絡狀態明顯要比網絡類型范圍更大,因此又增加了兩個選項,一個表示當前的網絡是未知的,另一個表示當前的網路不可達。
綜上所述,我們的目的就是拿到這個NetworkReachabilityStatus,那么NetworkReachabilityManager是如何把NetworkReachabilityStatus傳遞出來的呢? 答案就是閉包,
/// A closure executed when the network reachability status changes. The closure takes a single argument: the
/// network reachability status.
public typealias Listener = (NetworkReachabilityStatus) -> Void
swift的閉包,我們已經很熟悉了,在開發中,首先初始化NetworkReachabilityManager,然后設置Listener,第三部開啟監控,這個開啟監控的方法會在下邊講到。
Properties
在NetworkReachabilityManager中,屬性分為public和private,我們先看public部分:
/// Whether the network is currently reachable.
public var isReachable: Bool { return isReachableOnWWAN || isReachableOnEthernetOrWiFi }
/// Whether the network is currently reachable over the WWAN interface.
public var isReachableOnWWAN: Bool { return networkReachabilityStatus == .reachable(.wwan) }
/// Whether the network is currently reachable over Ethernet or WiFi interface.
public var isReachableOnEthernetOrWiFi: Bool { return networkReachabilityStatus == .reachable(.ethernetOrWiFi) }
/// The current network reachability status.
public var networkReachabilityStatus: NetworkReachabilityStatus {
guard let flags = self.flags else { return .unknown }
return networkReachabilityStatusForFlags(flags)
}
/// The dispatch queue to execute the `listener` closure on.
public var listenerQueue: DispatchQueue = DispatchQueue.main
/// A closure executed when the network reachability status changes.
public var listener: Listener?
public表明我們可以通過NetworkReachabilityManager實例直接獲得的屬性,能夠讓我們很方便的獲取我們想要的數據。我們對這些屬性做一些簡單的說明:
isReachable: Bool
當前網絡是可達的,要么是蜂窩網絡,要么是WiFi連接isReachableOnWWAN: Bool
表明當前網絡是通過蜂窩網絡連接isReachableOnEthernetOrWiFi: Bool
表明當前網絡是通過WiFi連接networkReachabilityStatus: NetworkReachabilityStatus
返回當前的網絡狀態,這也是上邊3個判斷的基礎listenerQueue
監聽listener在那個隊列中調用,默認的是主隊列listener: Listener
監聽閉包,當網絡狀態發生變化時會調用
上邊這些public屬性有的是只讀的,有的不是,我們在看看private屬性:
-
flags: SCNetworkReachabilityFlags?
主要目的是獲取flags,在上邊我們介紹過,網絡狀態就是根據flags判斷出來的是通過下邊的方法獲取到的:@available(iOS 2.0, *) public func SCNetworkReachabilityGetFlags(_ target: SCNetworkReachability, _ flags: UnsafeMutablePointer<SCNetworkReachabilityFlags>) -> Bool
-
reachability: SCNetworkReachability
必不可少的對象,有了它才能獲取flags -
previousFlags: SCNetworkReachabilityFlags
用於記錄當前的flags,在收到系統的callBack方法后,通過比較現在的flags和previousFlags來判斷是不是要調用listener函數
Initialization
關於初始化,NetworkReachabilityManager提供了三種選擇:
通過指定host
/// Creates a `NetworkReachabilityManager` instance with the specified host.
///
/// - parameter host: The host used to evaluate network reachability.
///
/// - returns: The new `NetworkReachabilityManager` instance.
public convenience init?(host: String) {
guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
self.init(reachability: reachability)
}
通過init方法會默認的設置為指向0.0.0.0
/// Creates a `NetworkReachabilityManager` instance that monitors the address 0.0.0.0.
///
/// Reachability treats the 0.0.0.0 address as a special token that causes it to monitor the general routing
/// status of the device, both IPv4 and IPv6.
///
/// - returns: The new `NetworkReachabilityManager` instance.
public convenience init?() {
var address = sockaddr_in()
address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
address.sin_family = sa_family_t(AF_INET)
guard let reachability = withUnsafePointer(to: &address, { pointer in
return pointer.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size) {
return SCNetworkReachabilityCreateWithAddress(nil, $0)
}
}) else { return nil }
self.init(reachability: reachability)
}
通過指定SCNetworkReachability
private init(reachability: SCNetworkReachability) {
self.reachability = reachability
self.previousFlags = SCNetworkReachabilityFlags()
}
deinit
deinit {
stopListening()
}
上邊的代碼表明,在NetworkReachabilityManager被銷毀的時候,會停止監控,因此在開發中就要額外注意這一點,最好讓控制器強引用它。
startListening
在開發中,對於開發某個功能,我有時候會稱為開發某種能力類,我們可以采取自上而下的方法,我先定義出最基本的偽代碼,對於網絡監控我們的偽代碼就應該是下邊這樣的:
- 創建一個監控者
- 設置監控回調事件
- 開始監控
- 停止監控
在這里講點額外的編程技巧,上邊的4個偽代碼我們可以成為子程序,每個子程序都應該有一定的內聚性要求,就是說每個子程序最好能夠實現一個單一的功能。子程序會出現成對出現的情況,比如開始和停止,等等。那么我們現在要講的就是第三步,開始監控。
@discardableResult
public 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 {
self.previousFlags = SCNetworkReachabilityFlags()
self.notifyListener(self.flags ?? SCNetworkReachabilityFlags())
}
return callbackEnabled && queueEnabled
}
@discardableResult表明可以忽略返回值。其實開始監控網絡狀態就分為兩部:
- 設置Callback回調函數
- 設置Callback回調隊列
當然必要的前提是必須初始化了一個reachability。
這里有一些很有意思的東西,可能我們在swift中是不常見的。比如:Unmanaged.passUnretained(self).toOpaque()
,比如:let reachability = Unmanaged<NetworkReachabilityManager>.fromOpaque(info!).takeUnretainedValue()
/// A type for propagating an unmanaged object reference.
///
/// When you use this type, you become partially responsible for
/// keeping the object alive.
public struct Unmanaged<Instance : AnyObject> {
/// Unsafely turns an opaque C pointer into an unmanaged class reference.
///
/// This operation does not change reference counts.
///
/// let str: CFString = Unmanaged.fromOpaque(ptr).takeUnretainedValue()
///
/// - Parameter value: An opaque C pointer.
/// - Returns: An unmanaged class reference to `value`.
public static func fromOpaque(_ value: UnsafeRawPointer) -> Unmanaged<Instance>
/// Unsafely converts an unmanaged class reference to a pointer.
///
/// This operation does not change reference counts.
///
/// let str0: CFString = "boxcar"
/// let bits = Unmanaged.passUnretained(str0)
/// let ptr = bits.toOpaque()
///
/// - Returns: An opaque pointer to the value of this unmanaged reference.
public func toOpaque() -> UnsafeMutableRawPointer
/// Creates an unmanaged reference with an unbalanced retain.
///
/// The instance passed as `value` will leak if nothing eventually balances
/// the retain.
///
/// This is useful when passing an object to an API which Swift does not know
/// the ownership rules for, but you know that the API expects you to pass
/// the object at +1.
///
/// - Parameter value: A class instance.
/// - Returns: An unmanaged reference to the object passed as `value`.
public static func passRetained(_ value: Instance) -> Unmanaged<Instance>
/// Creates an unmanaged reference without performing an unbalanced
/// retain.
///
/// This is useful when passing a reference to an API which Swift
/// does not know the ownership rules for, but you know that the
/// API expects you to pass the object at +0.
///
/// CFArraySetValueAtIndex(.passUnretained(array), i,
/// .passUnretained(object))
///
/// - Parameter value: A class instance.
/// - Returns: An unmanaged reference to the object passed as `value`.
public static func passUnretained(_ value: Instance) -> Unmanaged<Instance>
/// Gets the value of this unmanaged reference as a managed
/// reference without consuming an unbalanced retain of it.
///
/// This is useful when a function returns an unmanaged reference
/// and you know that you're not responsible for releasing the result.
///
/// - Returns: The object referenced by this `Unmanaged` instance.
public func takeUnretainedValue() -> Instance
/// Gets the value of this unmanaged reference as a managed
/// reference and consumes an unbalanced retain of it.
///
/// This is useful when a function returns an unmanaged reference
/// and you know that you're responsible for releasing the result.
///
/// - Returns: The object referenced by this `Unmanaged` instance.
public func takeRetainedValue() -> Instance
/// Performs an unbalanced retain of the object.
public func retain() -> Unmanaged<Instance>
/// Performs an unbalanced release of the object.
public func release()
/// Performs an unbalanced autorelease of the object.
public func autorelease() -> Unmanaged<Instance>
}
這里提供一個文章地址[HandyJSON] 設計思路簡析,關於swift中指針的使用可以參考這篇文章。很強大啊。后續我會寫HandyJson
的源碼解讀文章。
在上邊的開始監控中有一個函數:notifyListener
,這個函數的目的就是通知監聽者,也就是觸發回調函數。
func notifyListener(_ flags: SCNetworkReachabilityFlags) {
guard previousFlags != flags else { return }
previousFlags = flags
listener?(networkReachabilityStatusForFlags(flags))
}
networkReachabilityStatusForFlags
這個函數是根據flags獲取狀態的核心函數,但是我覺得沒什么好說的,在開發中用的也不多,我們把代碼粘一下,然后重點來說說swift中運算符==
重載:
func networkReachabilityStatusForFlags(_ flags: SCNetworkReachabilityFlags) -> NetworkReachabilityStatus {
/// 這里的contains函數要傳遞的值是OptionSet自身,因此.reachable換成SCNetworkReachabilityFlags.reachable也是可以的,reachable是一個靜態方法
/// flags.contains(.reachable)如果是true,就代表有網絡連接
guard flags.contains(.reachable) else { return .notReachable }
var networkStatus: NetworkReachabilityStatus = .notReachable
if !flags.contains(.connectionRequired) { networkStatus = .reachable(.ethernetOrWiFi) }
if flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic) {
if !flags.contains(.interventionRequired) { networkStatus = .reachable(.ethernetOrWiFi) }
}
#if os(iOS)
if flags.contains(.isWWAN) { networkStatus = .reachable(.wwan) }
#endif
return networkStatus
}
運算符重載
要想重載==
,需要實現Equatable
協議:
public protocol Equatable {
/// Returns a Boolean value indicating whether two values are equal.
///
/// Equality is the inverse of inequality. For any values `a` and `b`,
/// `a == b` implies that `a != b` is `false`.
///
/// - Parameters:
/// - lhs: A value to compare.
/// - rhs: Another value to compare.
public static func ==(lhs: Self, rhs: Self) -> Bool
}
其實,這種思想還是很重要的,在開發中可以通過這種方式來判斷兩個模型是不是相同,等等很多種使用場景。我簡單的把Apple文檔中的注釋說明部分翻譯一下。
==
和!=
是對立統一的關系,我們自定義了==
,同理,!=
也就支持了。在swift中,很多基本的數據類型都支持了Equatable
協議。
Equatable
協議的一個典型的應用場景就是判斷一個集合中是否包含某個值。在swift中,如果集合中的值都實現了Equatable
協議,那么就可以通過contains(_:)
方法來判斷是不是包含該值。這也說明了contains(_:)
內部實現應該是通過==
來實現的。使用contains(_:)
方法的好處就是省去了我們遍歷數據,然后再進行判斷的繁瑣步驟。我們看個例子:
/// let students = ["Nora", "Fern", "Ryan", "Rainer"]
///
/// let nameToCheck = "Ryan"
/// if students.contains(nameToCheck) {
/// print("\(nameToCheck) is signed up!")
/// } else {
/// print("No record of \(nameToCheck).")
/// }
/// // Prints "Ryan is signed up!"
需要把==
聲明成為自定義類型的靜態方法
假如說我們有一個街道地址的結構體:
/// struct StreetAddress {
/// let number: String
/// let street: String
/// let unit: String?
///
/// init(_ number: String, _ street: String, unit: String? = nil) {
/// self.number = number
/// self.street = street
/// self.unit = unit
/// }
/// }
我們讓StreetAddress
實現Equatable
協議:
///
/// extension StreetAddress: Equatable {
/// static func == (lhs: StreetAddress, rhs: StreetAddress) -> Bool {
/// return
/// lhs.number == rhs.number &&
/// lhs.street == rhs.street &&
/// lhs.unit == rhs.unit
/// }
/// }
///
接下來我們就能使用系統的contains(_:)
方法來判斷一個集合中是不是包含摸個街道地址了。
///
/// let addresses = [StreetAddress("1490", "Grove Street"),
/// StreetAddress("2119", "Maple Avenue"),
/// StreetAddress("1400", "16th Street")]
/// let home = StreetAddress("1400", "16th Street")
///
/// print(addresses[0] == home)
/// // Prints "false"
/// print(addresses.contains(home))
/// // Prints "true"
///
有了上邊的知識,我們在看看NetworkReachabilityManager
是怎么用的:
extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable {}
/// Returns whether the two network reachability status values are equal.
///
/// - parameter lhs: The left-hand side value to compare.
/// - parameter rhs: The right-hand side value to compare.
///
/// - returns: `true` if the two values are equal, `false` otherwise.
public func ==(
lhs: NetworkReachabilityManager.NetworkReachabilityStatus,
rhs: NetworkReachabilityManager.NetworkReachabilityStatus)
-> Bool
{
switch (lhs, rhs) {
case (.unknown, .unknown):
return true
case (.notReachable, .notReachable):
return true
case let (.reachable(lhsConnectionType), .reachable(rhsConnectionType)):
return lhsConnectionType == rhsConnectionType
default:
return false
}
}
在swift中,static函數還可以像上邊這么用,把函數寫到類的代碼塊之外,當然,上邊的代碼也可以這么寫:
extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable {
public static func ==(
lhs: NetworkReachabilityManager.NetworkReachabilityStatus,
rhs: NetworkReachabilityManager.NetworkReachabilityStatus)
-> Bool
{
switch (lhs, rhs) {
case (.unknown, .unknown):
return true
case (.notReachable, .notReachable):
return true
case let (.reachable(lhsConnectionType), .reachable(rhsConnectionType)):
return lhsConnectionType == rhsConnectionType
default:
return false
}
}
}
總結
由於知識水平有限,如有錯誤,還望指出
鏈接
Alamofire源碼解讀系列(一)之概述和使用 簡書-----博客園
Alamofire源碼解讀系列(二)之錯誤處理(AFError) 簡書-----博客園
Alamofire源碼解讀系列(三)之通知處理(Notification) 簡書-----博客園
Alamofire源碼解讀系列(四)之參數編碼(ParameterEncoding) 簡書-----博客園