iOS 網路狀態 API

今天被同事問到相關的問題,讓我想起之前封裝的功能,也在此記錄一下

iOS 舊有的網路狀態 API 有點複雜,Apple 也提供了範例

Reachability
Shows how to monitor network state and host reachability.
Apple Reachability Doc
Download Sample Code

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;
YNetworkReachabilityManager.h
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
YNetworkReachabilityManager.m

使用新 Network API

寫一個新的 NetworkState 來使用新 API

function 整個超簡單

@interface YNetworkState : NSObject

+ (YNetworkState*)Instance;

- (BOOL)isNetworkConnect;
- (BOOL)isWiFiConnect;
- (BOOL)isVPN;
- (NSString *)GetNetworkType;
- (NSString *)GetCellularType;

@end
YNetworkState.h

實作上新的 API 真的簡潔方便很多,但因為要兼容舊版,我會在裡面去判斷是否要使用 YNetworkReachabilityManager

#import <Network/Network.h>
匯入 Network framework

我把每一段 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;
}
判斷是不是 VPN

取得行動網路類型

- (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.