iOS 網路狀態 API
今天被同事問到相關的問題,讓我想起之前封裝的功能,也在此記錄一下
iOS 舊有的網路狀態 API 有點複雜,Apple 也提供了範例
Reachability
Shows how to monitor network state and host reachability.
Apple 好意的提供了範例檔,但也就因為這個好意,導致蠻多專案都直接下載來使用😂😂
之前好幾次就發現輸出時,有好多重複的 Reachability.h .m 檔案,後來優化專案的時候,我重構了網路狀態的相關 API,並匯入了新的 Network API。
定義狀態
typedef NS_ENUM(NSInteger, YNetworkStatus) {
YNetworkStatusUnknown = -1,
YNetworkStatusNotConnection = 0,
YNetworkStatusViaWWAN = 1,
YNetworkStatusViaWiFi = 2,
YNetworkStatusViaEthernet = 3,
};
Reachability 先改名!
由於 Reachability 真的太多人都直接用(我們專案也是),新的 API 又只支援 iOS 12 ,雖然 iOS 12 也很舊了,但公司專案還有些 min 是設定在 11,所以保留舊的來兼容 iOS 11。
我這邊就將 Reachability
取名為 YNetworkReachabilityManager
,後續 Code 會整理上傳到 Github,這邊就先講不一樣的地方。
/**
現在的網路狀態
*/
@property (readonly, nonatomic, assign) YNetworkStatus networkReachabilityStatus;
/**
是否連上網路
*/
@property (readonly, nonatomic, assign, getter = isReachable) BOOL reachable;
/**
是否連上行動網路
*/
@property (readonly, nonatomic, assign, getter = isReachableViaWWAN) BOOL reachableViaWWAN;
/**
是否連上 WiFi
*/
@property (readonly, nonatomic, assign, getter = isReachableViaWiFi) BOOL reachableViaWiFi;
/**
開始監視網路狀態
*/
- (void)startMonitoring;
/**
停止監視網路狀態
*/
- (void)stopMonitoring;
typedef void (^YNetworkStatusBlock)(YNetworkStatus status);
typedef YNetworkReachabilityManager * (^YNetworkStatusCallback)(YNetworkStatus status);
static void YPostReachabilityStatusChange(SCNetworkReachabilityFlags flags, YNetworkStatusCallback block) {
YNetworkStatus status = YNetworkStatusForFlags(flags);
dispatch_async(dispatch_get_main_queue(), ^{
YNetworkReachabilityManager *manager = nil;
if (block) {
manager = block(status);
}
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
NSDictionary *userInfo = @{ YNetworkingReachabilityNotificationStatusItem: @(status) };
[notificationCenter postNotificationName:YNetworkingReachabilityDidChangeNotification object:manager userInfo:userInfo];
});
}
static void YNetworkReachabilityCallback(SCNetworkReachabilityRef __unused target, SCNetworkReachabilityFlags flags, void *info) {
YPostReachabilityStatusChange(flags, (__bridge YNetworkStatusCallback)info);
}
@interface YNetworkReachabilityManager ()
@property (readonly, nonatomic, assign) SCNetworkReachabilityRef networkReachability;
@property (readwrite, nonatomic, assign) YNetworkStatus networkReachabilityStatus;
@property (readwrite, nonatomic, copy) YNetworkStatusBlock networkReachabilityStatusBlock;
@end
@implementation YNetworkReachabilityManager
- (BOOL)isReachable {
// 這裡可以根據需求改變
return [self isReachableViaWWAN] || [self isReachableViaWiFi];
}
- (BOOL)isReachableViaWWAN {
return self.networkReachabilityStatus == YNetworkStatusViaWWAN;
}
- (BOOL)isReachableViaWiFi {
return self.networkReachabilityStatus == YNetworkStatusViaWiFi;
}
- (void)startMonitoring {
[self stopMonitoring];
if (!self.networkReachability) {
return;
}
__weak __typeof(self)weakSelf = self;
YNetworkStatusCallback callback = ^(YNetworkStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
return strongSelf;
};
SCNetworkReachabilityContext context = {0, (__bridge void *)callback, YNetworkReachabilityRetainCallback, YNetworkReachabilityReleaseCallback, NULL};
SCNetworkReachabilitySetCallback(self.networkReachability, YNetworkReachabilityCallback, &context);
SCNetworkReachabilityScheduleWithRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^{
SCNetworkReachabilityFlags flags;
if (SCNetworkReachabilityGetFlags(self.networkReachability, &flags)) {
YPostReachabilityStatusChange(flags, callback);
}
});
}
- (void)stopMonitoring {
if (!self.networkReachability) {
return;
}
SCNetworkReachabilityUnscheduleFromRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
}
@end
使用新 Network API
寫一個新的 NetworkState 來使用新 API
function 整個超簡單
@interface YNetworkState : NSObject
+ (YNetworkState*)Instance;
- (BOOL)isNetworkConnect;
- (BOOL)isWiFiConnect;
- (BOOL)isVPN;
- (NSString *)GetNetworkType;
- (NSString *)GetCellularType;
@end
實作上新的 API 真的簡潔方便很多,但因為要兼容舊版,我會在裡面去判斷是否要使用 YNetworkReachabilityManager
#import <Network/Network.h>
我把每一段 code 拆開
@implementation YNetworkState {
BOOL isConnect;
BOOL isWiFi;
BOOL isMobile;
BOOL isOther;
BOOL isEthernet;
BOOL getFirstInfo;
nw_path_monitor_t path_monitor;
CTTelephonyNetworkInfo* netInfo;
}
Instance 沒什麼特別的就略過。接著先說明註冊
- (void)registerNetworkObserver {
NSString *hostname = @"https://www.google.com";
// iOS 12 以上使用 Network framework API
// 其餘使用 SCNetwork
if (@available(iOS 12.0, *)) {
path_monitor = nw_path_monitor_create();
nw_path_monitor_set_update_handler(path_monitor, ^(nw_path_t _Nonnull path) {
nw_path_status_t status = nw_path_get_status(path);
BOOL connect = status == nw_path_status_satisfied;
BOOL wifi = nw_path_uses_interface_type(path, nw_interface_type_wifi);
BOOL mobile = nw_path_uses_interface_type(path, nw_interface_type_cellular);
BOOL ethernet = nw_path_uses_interface_type(path, nw_interface_type_wired);
//BOOL other = nw_path_uses_interface_type(path, nw_interface_type_other);
YNetworkStatus networkStatus = [self createNetworkStatusWithConnect:connect wifi:wifi mobile:mobile ethernet:ethernet];
[self onNetworkChange:networkStatus];
});
nw_path_monitor_set_queue(path_monitor, dispatch_get_main_queue());
nw_path_monitor_start(path_monitor);
NSLog(@"目前採用 Network framework 判斷網路");
} else {
YNetworkReachabilityManager managerForDomain:hostname];
[[YNetworkReachabilityManager sharedManager] startMonitoring];
[[YNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(YNetworkStatus status) {
[[YNetworkState Instance] onNetworkChange:status];
}];
NSLog(@"目前採用 SCNetworkReachability 判斷網路");
}
由於新的 Network framework 僅支援 iOS 12 以上,所以要在內部去判斷。
- (void)unregisterNetworkObserver {
if (@available(iOS 12.0, *)) {
if (path_monitor != NULL) {
nw_path_monitor_cancel(path_monitor);
}
}
else {
[[YNetworkReachabilityManager sharedManager] stopMonitoring];
}
}
取消也很簡單。
- (instancetype)init {
self = [super init];
[self registerNetworkObserver];
self->netInfo = [[CTTelephonyNetworkInfo alloc] init];
return self;
}
- (void)dealloc {
[self unregisterNetworkObserver];
}
接著是比較重要的連線狀態更改
- (void)onNetworkChange:(YNetworkStatus)status {
self->isConnect = status != YNetworkStatusNotConnection;
self->isEthernet = status == YNetworkStatusViaEthernet;
self->isMobile = status == YNetworkStatusViaWWAN;
self->isWiFi = status == YNetworkStatusViaWiFi;
self->isOther = self->isMobile || self->isWiFi;
NSString *statusMsg = NETWORK_CHANGE_TO_OTHER;
switch (status) {
case YNetworkStatusUnknown:
//NSLog(@"網路不明");
statusMsg = NETWORK_CHANGE_TO_OTHER;
break;
case YNetworkStatusViaEthernet:
//NSLog(@"乙太網路");
statusMsg = NETWORK_CHANGE_TO_ETHERNET;
break;
case YNetworkStatusViaWWAN:
//NSLog(@"行動網路");
statusMsg = NETWORK_CHANGE_TO_CELLULAR;
break;
case YNetworkStatusViaWiFi:
//NSLog(@"WiFi");
statusMsg = NETWORK_CHANGE_TO_WIFI;
break;
case YNetworkStatusNotConnection:
//NSLog(@"無法連線");
statusMsg = NETWORK_CHANGE_TO_LOST;
break;
default:
break;
}
// 這邊可以用自己的邏輯轉通知,或寫一個 callback 給其它地方註冊
}
接著判斷是不是 VPN 的狀態
- (BOOL)isVPN {
BOOL flag = NO;
if (@available(iOS 9.0, *)) {
NSDictionary *dict = CFBridgingRelease(CFNetworkCopySystemProxySettings());
NSArray *keys = [dict[@"__SCOPED__"] allKeys];
for (NSString *key in keys) {
if ([key rangeOfString:@"tap"].location != NSNotFound ||
[key rangeOfString:@"tun"].location != NSNotFound ||
[key rangeOfString:@"ipsec"].location != NSNotFound ||
[key rangeOfString:@"ppp"].location != NSNotFound) {
flag = YES;
break;
}
}
} else {
struct ifaddrs *interfaces = NULL;
struct ifaddrs *temp_addr = NULL;
int success = 0;
// retrieve the current interfaces - returns 0 on success
success = getifaddrs(&interfaces);
if (success == 0)
{
// Loop through linked list of interfaces
temp_addr = interfaces;
while (temp_addr != NULL)
{
NSString *string = [NSString stringWithFormat:@"%s" , temp_addr->ifa_name];
if ([string rangeOfString:@"tap"].location != NSNotFound ||
[string rangeOfString:@"tun"].location != NSNotFound ||
[string rangeOfString:@"ipsec"].location != NSNotFound ||
[string rangeOfString:@"ppp"].location != NSNotFound)
{
flag = YES;
break;
}
temp_addr = temp_addr->ifa_next;
}
}
// Free memory
freeifaddrs(interfaces);
}
return flag;
}
取得行動網路類型
- (NSString *)GetCellularType {
// 判斷是否為 2G
if ([self checkNetworkType:@[CTRadioAccessTechnologyGPRS, CTRadioAccessTechnologyEdge]]) {
return NETWORK_CELLULAR_TYPE_2G;
}
// 判斷是否為 3G
NSArray<NSString*> *array3G = @[CTRadioAccessTechnologyWCDMA, CTRadioAccessTechnologyCDMA1x, CTRadioAccessTechnologyCDMAEVDORev0, CTRadioAccessTechnologyCDMAEVDORevA, CTRadioAccessTechnologyCDMAEVDORevB, CTRadioAccessTechnologyeHRPD, CTRadioAccessTechnologyHSUPA, CTRadioAccessTechnologyHSDPA];
if ([self checkNetworkType:array3G]) {
return NETWORK_CELLULAR_TYPE_3G;
}
// 判斷是否為 4G
if ([self checkNetworkType:@[CTRadioAccessTechnologyLTE]]) {
return NETWORK_CELLULAR_TYPE_LTE;
}
// 判斷是否為 5G
if (@available(iOS 14.1, *)) {
if ([self checkNetworkType:@[CTRadioAccessTechnologyNR]]) {
return NETWORK_CELLULAR_TYPE_NR;
}
}
return NETWORK_CELLULAR_TYPE_UNKNOWN;
}
原先的寫法都只能讓其他使用者被動的呼叫 API 來取得現在的網路狀況,透過持續的監聽網路狀態,並且實作主動通知,還是比較符合現在的使用情境。
Apple Network API Doc
Network | Apple Developer Documentation
Create network connections to send and receive data using transport and security protocols.