本文由我們團隊的 康祖彬 童鞋撰寫,在他的個人主頁也可以看到這篇文章:https://kangzubin.cn。
Reachability 是蘋果官方提供的示例原始碼,它是對 SystemConfiguration.framework
模組中的 SCNetworkReachability.h
標頭檔案裡提供的一系列網路連線狀態相關的 C 函式進行簡單封裝,以示範如何在 iOS App 開發中實現網路狀態變化監聽,由此也衍生出各種 Reachability
框架,比較著名的有 Github 上的 tonymillion/Reachability 以及 AFNetworking
中的 AFNetworkReachabilityManager
模組,它們的實現原理基本上是完全相同的。
下面我們就來閱讀分析一下蘋果提供的 Reachability 原始碼,原始碼中最核心的就 Reachability.h
和 Reachability.m
兩個檔案。
初始化方法
Reachability
中提供了三個快速初始化方法,分別為 reachabilityWithHostName:
、reachabilityWithAddress:
和 reachabilityForInternetConnection
。
reachabilityWithHostName: 方法
該方法通過指定的 伺服器域名
初始化一個 Reachability
物件以進行判斷網路連線狀態,原始碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
+ (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; } |
分析:上述程式碼比較簡單,通過呼叫 SCNetworkReachabilityCreateWithName
C 函式生成一個 SCNetworkReachabilityRef
引用,然後初始化一個 Reachability
物件,並把剛才生成的引用賦給該物件中的 _reachabilityRef
成員變數,以供後面網路狀態監聽使用。
reachabilityWithAddress: 方法
該方法通過指定的 伺服器 IP 地址
初始化一個 Reachability
物件以進行判斷網路連線狀態,原始碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+ (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; } |
分析:與上述類似,該方法通過呼叫 SCNetworkReachabilityCreateWithAddress
C 函式生成一個 SCNetworkReachabilityRef
引用,並賦給 Reachability
物件中的 _reachabilityRef
成員變數。
reachabilityForInternetConnection 方法
該方法通過 預設的路由地址
初始化一個 Reachability
物件以進行判斷網路連線狀態,通常用於 App 沒有連線到特定主機的情況,原始碼如下:
1 2 3 4 5 6 7 8 9 |
+ (instancetype)reachabilityForInternetConnection { struct sockaddr_in zeroAddress; bzero(&zeroAddress, sizeof(zeroAddress)); zeroAddress.sin_len = sizeof(zeroAddress); zeroAddress.sin_family = AF_INET; return [self reachabilityWithAddress: (const struct sockaddr *) &zeroAddress]; } |
分析:在該方法中先初始化一個預設的 sockaddr_in
Socket 地址(這裡建立的為零地址,0.0.0.0 地址表示查詢本機的網路連線狀態),然後呼叫 reachabilityWithAddress:
方法返回一個 Reachability
物件。
網路狀態監聽
開始監聽
通過上述初始化方法獲得一個 Reachability
物件後,可呼叫 startNotifier
方法開始進行網路狀態變化的監聽,原始碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- (BOOL)startNotifier { BOOL returnValue = NO; SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, NULL, NULL}; // 構造一個監聽網路連線狀態的上下文資訊,詳細說明見下面; // 通過呼叫 SCNetworkReachabilitySetCallback 函式(並傳入 Reachability 物件的 ref,以及根據 SCNetworkReachabilityCallBack 自定義的一個回撥函式和上述 context)設定 ref 的網路連線狀態變化時對應的回撥函式為 ReachabilityCallback; if (SCNetworkReachabilitySetCallback(_reachabilityRef, ReachabilityCallback, &context)) { // 通過呼叫 SCNetworkReachabilityScheduleWithRunLoop 函式設定 Reachability 物件的 ref 在 Current Runloop 中對應的模式(kCFRunLoopDefaultMode)開始監聽網路狀態; if (SCNetworkReachabilityScheduleWithRunLoop(_reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode)) { returnValue = YES; } } return returnValue; // 如果監聽成功,返回 YES,否則返回 NO。 } |
關於 SCNetworkReachabilityContext
的定義和註釋如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
typedef struct { CFIndex version; // 建立一個 SCNetworkReachabilityContext 結構體時,需要呼叫 SCDynamicStore 的建立函式,而此建立函式會根據 version 來建立出不同的結構體,SCNetworkReachabilityContext 對應的 version 是 0; void * __nullable info; // A C pointer to a user-specified block of data. 使用者指定的需要傳遞的資料快,下面兩個 block(retain 和 release)的引數就是 info。如果 info 是一個 block 型別,需要呼叫下面定義的 retain 和 release 進行拷貝和釋放; const void * __nonnull (* __nullable retain)(const void *info); // 該 retain block 用於對上述 info 進行 retain(一般通過呼叫 Block_copy 巨集 retain 一個 block 函式,即在堆空間新建或直接引用一個 block 拷貝),該值可以為 NULL; void (* __nullable release)(const void *info); // 該 release block 用於對 info 進行 release(一般通過呼叫 Block_release 巨集 release 一個 block 函式,即將 block 從堆空間移除或移除相應引用),該值可以為 NULL; CFStringRef __nonnull (* __nullable copyDescription)(const void *info); // 提供 info 的描述,一般取為 NULL。 } SCNetworkReachabilityContext; |
此處 Reachability
示例程式碼中建立的 context 的 info
取的是物件本身 self
(Reachability 物件型別),不是 block 型別,所以後面 retain
和 release
兩個引數都取 NULL
,關於 SCNetworkReachabilityContext
的詳細用法可參見 AFNetworkReachabilityManager.m
另外,上述回撥函式 ReachabilityCallback
的定義如下,在該回撥函式中,首先獲取一個 Reachability
物件,並把該物件作為引數傳送一個全域性通知,因此我們可以監聽 kReachabilityChangedNotification
通知以獲得實時網路連線狀態的變化。
1 2 3 4 5 6 7 8 9 10 11 12 |
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"); Reachability* noteObject = (__bridge Reachability *)info; // 因為上述 context 傳入的是 self(Reachability 物件),所以這裡的 info 為 Reachability 物件型別。 // 傳送一個全域性通知告訴監聽者網路連線狀態已發生改變,可通過 noteObject 獲取狀態。 [[NSNotificationCenter defaultCenter] postNotificationName: kReachabilityChangedNotification object: noteObject]; } |
SCNetworkReachabilityCallBack
規定了自定義的回撥函式的引數需要滿足如下形式:
1 2 3 4 5 |
typedef void (*SCNetworkReachabilityCallBack) ( SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void * __nullable info ); |
取消監聽
我們可呼叫 Reachability
物件的 stopNotifier
進行取消網路連線狀態變化的監聽,原始碼如下:
1 2 3 4 5 6 7 8 |
- (void)stopNotifier { if (_reachabilityRef != NULL) { // 通過呼叫 SCNetworkReachabilityUnscheduleFromRunLoop 函式設定 Reachability 物件的 ref 在 Current Runloop 中對應的模式(kCFRunLoopDefaultMode)取消監聽網路狀態。 SCNetworkReachabilityUnscheduleFromRunLoop(_reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); } } |
釋放物件
當要釋放一個 Reachability 物件時,我們需要在其 dealloc
方法裡取消網路狀態監聽。另外由於 SCNetworkReachabilityRef
是 Core Foundation
物件,所以這裡需要呼叫 CFRelease()
函式釋放 _reachabilityRef。
1 2 3 4 5 6 7 8 |
- (void)dealloc { [self stopNotifier]; if (_reachabilityRef != NULL) { CFRelease(_reachabilityRef); } } |
獲取當前網路連線狀態
當通過上述方法初始化一個 Reachability
物件並呼叫 startNotifier
方法開始監聽後,我們可以隨時呼叫物件的 currentReachabilityStatus
方法獲取當前網路連線狀態,返回的狀態型別 NetworkStatus
定義如下:
1 2 3 4 5 |
typedef enum : NSInteger { NotReachable = 0, //無網路連線 ReachableViaWiFi, //網路通過 WiFi 連線 ReachableViaWWAN //網路通過行動網路連線 } NetworkStatus; |
currentReachabilityStatus
方法的實現原始碼如下,首先通過呼叫 SCNetworkReachabilityGetFlags(...)
函式並傳入 _reachabilityRef
引用作為引數,獲得一個表示當前網路連線狀態的 SCNetworkReachabilityFlags
列舉值,然後根據列舉值呼叫 networkStatusForFlags:
方法判斷當前網路狀態型別並返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (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; } |
networkStatusForFlags:
方法根據具體的 SCNetworkReachabilityFlags
列舉值,判斷當前是否有網路連線,並且連線型別是 WiFi 還是 WWAN,具體實現和註釋如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
- (NetworkStatus)networkStatusForFlags:(SCNetworkReachabilityFlags)flags { PrintReachabilityFlags(flags, "networkStatusForFlags"); if ((flags & kSCNetworkReachabilityFlagsReachable) == 0) { // The target host is not reachable. return NotReachable; } NetworkStatus returnValue = NotReachable; if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) { // If the target host is reachable and no connection is required then we'll assume (for now) that you're on Wi-Fi... returnValue = ReachableViaWiFi; } if ((((flags & kSCNetworkReachabilityFlagsConnectionOnDemand ) != 0) || (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0)) { // ... and the connection is on-demand (or on-traffic) if the calling application is using the CFSocketStream or higher APIs... if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0) { // ... and no [user] intervention is needed... returnValue = ReachableViaWiFi; } } if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN) { // ... but WWAN connections are OK if the calling application is using the CFNetwork APIs. returnValue = ReachableViaWWAN; } return returnValue; } |
在上述 networkStatusForFlags:
方法中,先呼叫了 PrintReachabilityFlags
函式列印當前網路連線狀態對應的 flags
字元,根據拼接的不同字元我們可以判斷不同的網路連線型別,比如 WiFi、2G、3G 等,該函式的實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#define kShouldPrintReachabilityFlags 1 static void PrintReachabilityFlags(SCNetworkReachabilityFlags flags, const char* comment) { #if kShouldPrintReachabilityFlags NSLog(@"Reachability Flag Status: %c%c %c%c%c%c%c%c%c %s\n", (flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-', (flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-', (flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-', (flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-', (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-', (flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-', (flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-', (flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-', (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-', comment ); #endif } |
比如,當是 WiFi 連線時會列印 “R”(這裡忽略 “-” 字元),當是 3G 連線時,列印 “Rt”,當是聯通或移動 2G 連線時,則列印 “Rtc” 等等。
另外,在 Reachability
類中,還提供了一個 connectionRequired
方法,用於判斷網路是否需要進一步連線(例如,雖然裝置的 WWAN 連線可用,但並沒有啟用,需要建立一個連線來啟用;或者雖然已連線上 WiFi,但該 WiFi 需要進一步 VPN 連線等情況),該方法通過驗證 SCNetworkReachabilityFlags
值是否為 kSCNetworkReachabilityFlagsConnectionRequired
判斷,實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
- (BOOL)connectionRequired { NSAssert(_reachabilityRef != NULL, @"connectionRequired called with NULL reachabilityRef"); SCNetworkReachabilityFlags flags; if (SCNetworkReachabilityGetFlags(_reachabilityRef, &flags)) { return (flags & kSCNetworkReachabilityFlagsConnectionRequired); } return NO; } |
使用示例
在 Reachability
原始碼的 APLViewController.m
檔案中,蘋果給出了上述封裝的使用示例。在我們的 App 開發中,我們可以按如下步驟獲取當前網路連線型別或者監聽網路連線變化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// 1、新增 kReachabilityChangedNotification 通知監聽,以監聽網路連線變化; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reachabilityChanged:) name:kReachabilityChangedNotification object:nil]; // 2、 根據 www.apple.com 域名初始化一個 Reachability 物件,當然這裡也可以通過 IP 地址來初始化; NSString *remoteHostName = @"www.apple.com"; Reachability *hostReachability = [Reachability reachabilityWithHostName:remoteHostName]; // 此處 hostReachability 根據需求可以定義為全域性變數或靜態變數 // 3、開始網路連線狀態監聽 [hostReachability startNotifier]; // ... // 4、在其他需要獲取網路連線狀態的地方呼叫 currentReachabilityStatus 方法; NetworkStatus netStatus = [reachability currentReachabilityStatus]; // ... // 5、當網路連線狀態發生變化時,會根據全域性通知回撥此方法; - (void)reachabilityChanged:(NSNotification *)note { Reachability* reachability = [note object]; NSParameterAssert([reachability isKindOfClass:[Reachability class]]); NetworkStatus netStatus = [reachability currentReachabilityStatus]; switch (netStatus) { case NotReachable: // 無網路連線 break; case ReachableViaWWAN: // 網路通過行動網路連線 break; case ReachableViaWiFi: // 網路通過 WiFi 連線 break; } } |
總結
通過分析上述 Reachability
原始碼,我們可以總結 SCNetworkReachability.h
標頭檔案裡提供的一系列網路連線狀態相關的 C 函式的使用流程如下:
- 首先在
SCNetworkReachabilityCreateWithName(...)
、SCNetworkReachabilityCreateWithAddress(...)
、SCNetworkReachabilityCreateWithAddressPair(...)
3個初始化函式中任選其一建立一個SCNetworkReachabilityRef
引用; - 其次根據
SCNetworkReachabilityCallBack
定義一個網路監聽回撥函式,並初始化一個SCNetworkReachabilityContext
上下文資訊,然後呼叫SCNetworkReachabilitySetCallback
函式並傳入上述 ref、callback、context 3個引數,設定上述建立的 ref 在網路狀態發生變化時的回撥函式; - 通過呼叫
SCNetworkReachabilityScheduleWithRunLoop(...)
或SCNetworkReachabilityUnscheduleFromRunLoop(...)
函式並傳入上述 ref,在 Current Runloop 中開始或取消監聽網路連線狀態變化,另外也可以通過SCNetworkReachabilitySetDispatchQueue(...)
函式設定在指定執行緒裡監聽; - 呼叫
SCNetworkReachabilityGetFlags(...)
函式並傳入上述 ref,可獲得當前網路連線狀態的 flags 列舉值,另外需要注意的是,當 DNS 伺服器無法連線,或者在弱網環境下,此函式將會很耗時,所以蘋果建議在子執行緒裡非同步呼叫此函式; - 根據不同的
SCNetworkReachabilityFlags
列舉值,判斷當前網路連線狀態和連線型別。