iOS 網路監控框架 - Reachability 原始碼解讀

要上班的斌哥發表於2017-12-12

Reachability 專案是 Apple 提供的一個官方 Demo, 用於演示如何使用 System Configuration framework 來監控 iOS 裝置的網路狀態。值得注意的是 Reachability 僅僅能檢測到資料包是否可以離開本裝置,而不能檢測到資料包是否能達到目的地。也就是說不能把它當成 Ping 來使用。Reachability 使用起來也特別的簡單,接下來我們來看看如何使用 Reachability。

Reachability 使用

設定欲檢測的域名,啟動網路狀態監控,Reachability 會在裝置的網路狀態發生變化的時候會發出一個名為 kReachabilityChangedNotification 的通知,我們可以通過接收這個通知,然後根據裝置的網路狀態做業務應對處理。

// 接收 kReachabilityChangedNotification 通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reachabilityChanged:) name:kReachabilityChangedNotification object:nil];

// 檢測某個域名是否可達
self.hostReachability = [Reachability reachabilityWithHostName:remoteHostName];

// 開始監控
[self.hostReachability startNotifier];

// 收到 kReachabilityChangedNotification 通知
- (void) reachabilityChanged:(NSNotification *)note
{
	Reachability* curReach = [note object];
	NSParameterAssert([curReach isKindOfClass:[Reachability class]]);
      // 做業務應對處理 
}
複製程式碼

Reachability 的組成

Reachability 專案僅僅由一個 Reachability.h 和 Reachability.m 檔案組成。麻雀雖小,五臟俱全。Reachability 也是這樣的。Reachability 的介面提供了以下的能力:

  1. 檢測一個域名是否可達,使用 reachabilityWithHostName: 方法。
  2. 檢測一個 IP 是否可達,使用 reachabilityWithAddress: 方法。
  3. 檢測裝置網路的可到達性,使用 reachabilityForInternetConnection 方法。
  4. 開始監控網路狀況,使用 startNotifier 方法。
  5. 停止監控網路狀況,使用 stopNotifier 方法。與 startNotifier 方法配合使用。
  6. 獲取當前的網路連線方式,使用 currentReachabilityStatus 方法。
  7. 判斷裝置的網路是否是按需連線方式,使用 connectionRequired 方法。
@interface Reachability : NSObject

/*!
 * Use to check the reachability of a given host name.
 */
+ (instancetype)reachabilityWithHostName:(NSString *)hostName;

/*!
 * Use to check the reachability of a given IP address.
 */
+ (instancetype)reachabilityWithAddress:(const struct sockaddr *)hostAddress;

/*!
 * Checks whether the default route is available. Should be used by applications that do not connect to a particular host.
 */
+ (instancetype)reachabilityForInternetConnection;

/*!
 * Start listening for reachability notifications on the current run loop.
 */
- (BOOL)startNotifier;
- (void)stopNotifier;

- (NetworkStatus)currentReachabilityStatus;

/*!
 * WWAN may be available, but not active until a connection has been established. WiFi may require a connection for VPN on Demand.
 */
- (BOOL)connectionRequired;

@end
複製程式碼

Reachability 的實現

Reachability 的實現依賴於系統的 SCNetworkReachability 類,SCNetworkReachability 允許應用程式獲取當前系統的網路配置情況,也可以用來判斷一個 target host 的可達性。值得注意的是,當應用程式可以將一個 data packet 傳送到 network stack,並且該 data packet 可以離開本地裝置,這個時候 SCNetworkReachability 就會判斷網路是可達的。正是由於這個原因,Reachability 不能保證 data packet 會被送到 target host 。SCNetworkReachability 的使用分為同步模式和非同步模式

非同步模式

我們先從非同步模式開始解讀,這畢竟是比較常用的模式!

第一步 我們從 startNotifier 方法開始。把 SCNetworkReachabilityRef 放到當前 runloop 的 kCFRunLoopDefaultMode 模式,當網路連線狀態發生變化,SCNetworkReachabilityRef 會執行通過 SCNetworkReachabilitySetCallback 方法設定好的 ReachabilityCallback 方法。

- (BOOL)startNotifier
{
	BOOL returnValue = NO;
	SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};

    if (SCNetworkReachabilitySetCallback(_reachabilityRef, ReachabilityCallback, &context))
	{
        //把 SCNetworkReachabilityRef 放到當前 runloop 的 kCFRunLoopDefaultMode 模式
        //當 SCNetworkReachabilityRef 判斷到網路狀態發生變化,會執行 ReachabilityCallback 回撥
		if (SCNetworkReachabilityScheduleWithRunLoop(_reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode))
		{
			returnValue = YES;
		}
	}
    
	return returnValue;
}
複製程式碼

第二步 接下來看 ReachabilityCallback 回撥方法,在這裡主要是傳送一個 名為 kReachabilityChangedNotification 通知,並將 Reachability 物件作為引數,將網路連線狀態通知應用程式。

static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
{
#pragma unused (target, flags)
	NSCAssert(info != NULL, @"info was NULL in ReachabilityCallback");
	NSCAssert([(__bridge NSObject*) info isKindOfClass: [Reachability class]], @"info was wrong class in ReachabilityCallback");
    // 傳送 kReachabilityChangedNotification 通知,並將 Reachability 作為引數
    Reachability* noteObject = (__bridge Reachability *)info;
    // Post a notification to notify the client that the network reachability changed.
    [[NSNotificationCenter defaultCenter] postNotificationName: kReachabilityChangedNotification object: noteObject];
}
複製程式碼

第三步 startNotifier 和 stopNotifier 方法是配合使用的。stopNotifier 的主要任務是對 startNotifier 做的操作進行逆向處理。把 SCNetworkReachabilityRef 從當前 runloop 的 kCFRunLoopDefaultMode 模式移除。

- (void)stopNotifier
{
	if (_reachabilityRef != NULL)
	{
		SCNetworkReachabilityUnscheduleFromRunLoop(_reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
	}
}
複製程式碼

第四步 上面主要是主流程解讀,解讀 Reachability 開始網路監控,網路狀態發生變化處理,到停止網路監控的流程。這過程 Reachability 都 依賴於 SCNetworkReachabilityRef 的物件,接下來我們來說說 Reachability 怎麼建立 SCNetworkReachabilityRef 物件。SCNetworkReachabilityRef 物件的建立依賴於 IP 或者域名。依賴於域名使用 SCNetworkReachabilityCreateWithName 方法,依賴於 IP 使用 SCNetworkReachabilityCreateWithAddress 方法。

// 通過域名建立 SCNetworkReachabilityRef 物件
+ (instancetype)reachabilityWithHostName:(NSString *)hostName
{
	Reachability* returnValue = NULL;
	SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithName(NULL, [hostName UTF8String]);
	if (reachability != NULL)
	{
		returnValue= [[self alloc] init];
		if (returnValue != NULL)
		{
			returnValue->_reachabilityRef = reachability;
		}
        else {
            CFRelease(reachability);
        }
	}
	return returnValue;
}

// 通過 IP 建立 SCNetworkReachabilityRef 物件
+ (instancetype)reachabilityWithAddress:(const struct sockaddr *)hostAddress
{
	SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, hostAddress);

	Reachability* returnValue = NULL;

	if (reachability != NULL)
	{
		returnValue = [[self alloc] init];
		if (returnValue != NULL)
		{
			returnValue->_reachabilityRef = reachability;
		}
        else {
            CFRelease(reachability);
        }
	}
	return returnValue;
}
複製程式碼

以上是非同步模式的解讀,Reachability 的非同步模式顧名思義就是網路連線發生了變化,Reachability 發通知告知應用程式。而同步模式呢? 那就是應用程式主動找 Reachability 獲取當前的網路連線狀態。

同步模式

應用程式主動找 Reachability 獲取當前的網路連線狀態,使用 currentReachabilityStatus 方法。Reachability 能夠實現同步模式依賴於 SCNetworkReachabilityRef 的 SCNetworkReachabilityGetFlags 方法。SCNetworkReachabilityGetFlags 方法使用同步模式獲取裝置的網路連線狀態。

- (NetworkStatus)currentReachabilityStatus
{
	NSAssert(_reachabilityRef != NULL, @"currentNetworkStatus called with NULL SCNetworkReachabilityRef");
	NetworkStatus returnValue = NotReachable;
	SCNetworkReachabilityFlags flags;
        // 同步模式獲取網路連線狀態
	if (SCNetworkReachabilityGetFlags(_reachabilityRef, &flags))
	{
        returnValue = [self networkStatusForFlags:flags];
	}
    
	return returnValue;
}
複製程式碼

總結

Reachability 可以用來檢測網路狀態變化和網路的可達性的一個框架。但是 Reachability 僅僅能檢測到資料包是否可以離開本裝置,而不能檢測到資料包是否能達到目的地,也就是說不能把它當成 Ping 來使用。

參考

  1. Reachability 的程式碼和 Demo https://developer.apple.com/library/content/samplecode/Reachability/Introduction/Intro.html
  2. SCNetworkReachability 參考 https://developer.apple.com/documentation/systemconfiguration/scnetworkreachability-g7d

相關文章