通讀AFN③--HTTPS訪問控制(AFSecurityPolicy),Reachability(AFNetworkReachabilityManager)

這一篇主要介紹使用AFN如何訪問HTTPS網站以及這些作法的實現原理,還有介紹AFN的網絡狀態監測部分AFNetworkReachabilityManager,這個模塊會和蘋果官方推薦的Reachability框架作一個對比。git

本文全部的代碼都運行在iOS9.2的模擬器上,而且在info.plist對ATS作了適配:設置容許非法的加載Allow Arbitrary Loads爲YES。
不要認爲在info.plist添加NSAppTransportSecurity > NSAllowsArbitraryLoads爲YES
就覺得弄懂iOS9網絡適配了,有關具體細節問題請看南峯子的這篇文章App Transport Security(ATS)github

介於iOS有關HTTPS訪問的認證過程代碼並非特別常用,本文會用大量的篇幅介紹HTTPS認證的過程,並會經過系統的NSURLSession完成一些認證相關的代碼,畢竟AFN就是使用了這些代碼來實現對HTTPS網站的訪問支持的。web

HTTPS網站訪問過程當中,瀏覽器幫你作了什麼

不一樣於普通的HTTP請求,當訪問一個HTTPS的網站時,瀏覽器會幫咱們不少隱藏的工做,這實際上是SSL通道創建的三次握手過程:
1.發起請求。
首先當輸入完https網址敲擊回車以後,瀏覽器首先向服務器發送一個須要訪問的請求,這個請求中包含着瀏覽器SSL 協議的版本號,加密算法的種類,產生的隨機數,以及其餘服務器和客戶端之間通信所須要的各類信息。
2.服務端返回證書。
服務器向客戶端傳送SSL 協議的版本號,加密算法的種類,隨機數以及其餘相關信息,同時服務器還將向客戶端傳送本身的證書,這些信息被保存在客戶端被稱做'被保護空間'的地方。這裏最關鍵的就是證書信息。
3.瀏覽器驗證證書信息。
瀏覽器利用服務器傳過來的信息驗證服務器的合法性,服務器的合法性包括:證書是否過時,發行服務器證書的CA 是否可靠,發行者證書的公鑰可否正確解開服務器證書的「發行者的數字簽名」,服務器證書上的域名是否和服務器的實際域名相匹配。
若是合法性驗證沒有經過,通信將斷開;若是合法性驗證經過,將繼續進行第四步。
4.客戶端向服務器發送「預主密碼」。
瀏覽器隨機產生一個用於後面通信的「對稱密碼」,而後用服務器的公鑰(服務器的公鑰從步驟②中的服務器的證書中得到)對其加密,而後將加密後的「預主密碼」傳給服務器。算法

4.1.若是服務器要求客戶的身份認證(在握手過程當中爲可選),用戶不光要傳給服務器「預主密碼」,還需創建一個隨機數而後對其進行數據簽名,將這個含有簽名的隨機數和客戶本身的證書也傳給服務器。數組

4.2.若是不須要,則只將「預主密碼」傳給服務器,並直接進行第6步。
5.服務端身份驗證(須要才進行)。
若是服務器要求客戶的身份認證,服務器必須檢驗客戶證書和簽名隨機數的合法性,具體的合法性驗證過程包括:客戶的證書使用日期是否有效,爲客戶提供證書的CA 是否可靠,發行CA 的公鑰可否正確解開客戶證書的發行CA 的數字簽名,檢查客戶的證書是否在證書廢止列表(CRL)中。
檢驗若是沒有經過,通信馬上中斷;
若是驗證經過,進行下一步。
6.瀏覽器、服務端各自生成通話密碼。
服務器將用本身的私鑰解開加密的「預主密碼」,而後執行一系列步驟來產生主通信密碼(客戶端也將經過一樣的方法產生相同的主通信密碼)。
7.約定通話密碼。
服務器和客戶端用相同的主通信密碼即「通話密碼」,一個對稱密鑰用於SSL 協議的安全數據通信的加解密通信。同時在SSL 通信過程當中還要完成數據通信的完整性,防止數據通信中的任何變化。
8.瀏覽器通知服務器已準備就緒。
客戶端向服務器端發出信息,指明後面的數據通信將使用的步驟⑦中的主密碼爲對稱密鑰,同時通知服務器客戶端的握手過程結束。
9.服務端通知瀏覽器已準備就緒。
服務器向客戶端發出信息,指明後面的數據通信將使用的步驟⑦中的主密碼爲對稱密鑰,同時通知客戶端服務器端的握手過程結束。
10.開始數據通信。
SSL 的握手部分結束,SSL安全通道創建完成,開始進行數據通信開始,通信過程當中客戶和服務器開始使用相同的對稱密鑰。
若是以https://www.baidu.com爲例,這時候已經表現爲baidu的主頁打開了,可是SSL加密通道在下次請求的時候不用再次創建。xcode

對於訪問的過程當中,一般會在第3步出現問題,以12306的購票頁面爲例:
當進行到第3步的時候,瀏覽器驗證爲:發行服務器證書的CA是不可靠的,能夠在Chrome的地址欄中點擊被打了紅叉的鎖來查看這個頁面的證書頒發機構,
12306HTTPS證書
咱們能夠搜索到這個命名爲'SRCA'的機構其實是‘中鐵認證中心’也就是12306本身的認證系統,它是用了本身的認證系統給本身頒發了一個SSL加密證書,而Chrome怎麼會承認它呢。順便看了一下百度的證書:
baiduHTTPS證書
這是一個由美國Symantec Trust Network組織頒發的證書,是一個比較權威的證書頒發機構,幾乎在全部的瀏覽器中都是承認的。而baidu使用的證書是這個機構的根證書的子證書,而之因此瀏覽器能承認它,是由於根證書經過webtrust國際認證,並已經內置到各大瀏覽器如谷歌,火狐,微軟等系統中。
那麼這畢竟只是瀏覽器默認的一種認證方式,畢竟咱們仍是須要訪問12306的,這裏就要改變一下第3步驗證的結果,在瀏覽器中,咱們能夠手動選擇信任,而後繼續向下進行。
手動信任證書
這樣就能訪問這些網站了。瀏覽器

使用系統的NSURLSession模擬瀏覽器完成HTTPS的證書認證

與瀏覽器的驗證過程類似,iOS的HTTPS驗證過程也要走相似的步驟,不過不用擔憂的是,不少過程咱們也不須要處理,只須要處理好第3步就好了,當咱們進行訪問一個HTTPS網站時,當走到第二步的時候,也就是服務器返回證書時,須要咱們在本地本身完成證書信任的過程,若是使用session建立的task進行網絡訪問,這時候就會進入到- URLSession:didReceiveChallenge:completionHandler:這個代理方法中,這時候已經完成了HTTPS訪問的第二步,session會讓咱們在這個方法中完成第3步的過程。這個方法的參數有以下的解釋:安全

參數 解釋
challenge 一個包含了受權請求的對象
completionHandler 你的代理方法必定會調用的一個handler. 它的參數是
disposition—描述challenge如何被處理的幾個常量中的一個
credential—若是disposition是NSURLSessionAuthChallengeUseCredential,credential是受權驗證時會被使用到的憑據,其餘狀況爲NULL.

challenge參數須要另外說明的是challenge是一個NSURLAuthenticationChallenge對象,表明着進行https請求進行時,服務端發送過來的質詢,當接收到質詢以後就要開始進行客戶端的驗證了。服務器

這個對象中最重要的屬性就是protectionSpace它表明着對須要驗證的受保護空間的驗證,是一個NSURLProtectionSpace類型的對象。NSURLProtectionSpace對象包含請求的主機host、端口號port、代理類型proxyType、使用的協議protocol、服務端要求客戶端對其驗證的方法authenticationMethod等重要的信息,還有表明着服務器SSL傳輸狀態的SecTrustRef類型的屬性serverTrust,不過當且僅當authenticationMethod爲NSURLAuthenticationMethodServerTrust這個屬性值纔不爲Nil.網絡

這裏還要說明一下服務端指定的驗證方法的類型,驗證方法的類型有不少種,這裏再也不一一列舉,咱們一般會見到這樣幾種類型:

NSURLAuthenticationMethodHTTPBasic 
NSURLAuthenticationMethodHTTPDigest
NSURLAuthenticationMethodNTLM
NSURLAuthenticationMethodClientCertificate
NSURLAuthenticationMethodServerTrust

其中HTTP Basic、HTTP Digest與NTLM認證都是基於用戶名/密碼的認證,ClientCertificate(客戶端證書)認證要求從客戶端上傳證書。客戶端須要按照服務端指定的認證方法進行認證,不然可能會按照錯誤處理。例如使用HTTP Basic方式,客戶端須要將用戶名和密碼信息放到憑據中,而後傳遞給服務端;若是使用的是ServerTrust方式,那麼客戶端就要將信任的憑據發給服務端。
通常在HTTPS訪問的第3步過程當中,服務端要求的認證方法幾乎老是ServerTrust方式。有遇到過一些網絡代理工具使用HTTP Digest的驗證方式,在瀏覽器端進行訪問的時候就彈出一個要求輸入帳號和密碼的彈窗。

對於completionHandler參數是一個最終處理憑據的回調,要求在建立好包含驗證信息的憑據以後必須調用,這樣纔會將驗證的信息發送給服務端,也就意味着第3步的完成,開始進行第4步。
它的第一個參數是處理的選項,是一個枚舉類型:

typedef NS_ENUM(NSInteger, NSURLSessionAuthChallengeDisposition) {
    NSURLSessionAuthChallengeUseCredential = 0,   // 使用服務器發回的憑據,不過可能爲空     
    NSURLSessionAuthChallengePerformDefaultHandling = 1,  // 默認的處理方法,憑據參數會被忽略
    NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2,  //取消整個請求,忽略憑據參數 
    NSURLSessionAuthChallengeRejectProtectionSpace = 3, // 此次質詢被拒絕,下次再試 ,憑據參數被忽略
} NS_ENUM_AVAILABLE(NSURLSESSION_AVAILABLE, 7_0);

理清上面的思路以後,咱們能夠試一試使用系統的session訪問HTTPS網站了:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
    [[self.session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            NSLog(@"%@", error);
            return ;
        }
        NSLog(@"%@", response);
    }] resume];
}

#pragma mark - NSURLSessionDelegate

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler {
    // 判斷服務器的身份驗證的方法是不是:ServerTrust方式
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        
        // 建立一個新憑據,這個憑據指定了'握手'是被信任的
        NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        
        if (credential != nil) {
            // 完成'處置',將信任憑據發給服務端
            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
        }
        // 若是credential == nil 如下回調會自動完成
        // completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, credential);
    }
}

由於咱們使用的是使用第2步中服務端傳回來的證書,因此即便是對付https://kyfw.12306.cn/otn/leftTicket/init這樣的流氓頁面也一樣是能夠的。可是對於iOS9來講並非這樣,必須設置了Allow Arbitrary Loads爲YES纔會達到預期效果。

對於AFN,不管實在iOS9以前仍是iOS9以後,當訪問https://kyfw.12306.cn/otn/leftTicket/這個頁面的時候都會走不通,這是由於AFN對於自簽名的HTTPS網站有着特殊的驗證(有關驗證細節,請看本文下一部分),必須證書提早導入到項目中,將Chrome中的證書導入到項目中,請參見下圖:
Chrome生成證書
將生成的證書文件kyfw.12306.cn.cer加入到xcode項目中,使用AFN按照以下方式調用便可:

NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"kyfw.12306.cn.cer" ofType:nil];
NSData *cerData = [NSData dataWithContentsOfFile:cerPath];
NSSet *set = [[NSSet alloc] initWithObjects:cerData, nil];

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:set];
manager.securityPolicy.allowInvalidCertificates = YES;

[manager GET:@"https://kyfw.12306.cn/otn/leftTicket/init" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
    NSLog(@"%@", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    NSLog(@"%@",error);
}];

這樣便能正確的訪問自簽名的網站了。

AFN實現HTTPS訪問的細節

說了那麼多如何使用代碼訪問HTTPS網站,那麼AFN是如何實現的呢,AFURLSessionManager中實現了- URLSession:didReceiveChallenge:completionHandler:代理方法:

- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    if (self.taskDidReceiveAuthenticationChallenge) {
        disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential);
    } else {
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                disposition = NSURLSessionAuthChallengeUseCredential;
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            } else {
                disposition = NSURLSessionAuthChallengeRejectProtectionSpace;
            }
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }

    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

它的思路上這樣的
若是主動經過manger的setTaskDidReceiveAuthenticationChallengeBlock:方法傳遞了taskDidReceiveAuthenticationChallenge的值那麼,會按照傳入的block處理此次質詢,
若是沒有傳入就走AFN處理方式(else分支):

若是驗證方法爲ServerTrust就會使用securityPolicy屬性的方法針對host評判serverTrust的合法性,若是成功了就會使用服務端傳來的證書進行處理,失敗了則會拒絕本次質詢。

若是驗證方法不是ServerTrust,則使用默認的處理方式(NSURLSessionAuthChallengePerformDefaultHandling)處理。

那麼,能夠看出,這裏最關鍵的就是評判合法性的過程了,咱們重點來看一下。評判合法性的方法被定義在AFSecurity類中,是這個類惟一的對象方法:

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
        NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
        return NO;
    }

    NSMutableArray *policies = [NSMutableArray array];
    if (self.validatesDomainName) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }

    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }

    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
        default:
            return NO;
        case AFSSLPinningModeCertificate: {
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);

            if (!AFServerTrustIsValid(serverTrust)) {
                return NO;
            }
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            
            return NO;
        }
        case AFSSLPinningModePublicKey: {
            NSUInteger trustedPublicKeyCount = 0;
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

            for (id trustChainPublicKey in publicKeys) {
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
    }
    
    return NO;
}

這段長度爲60行的代碼實現了這樣的過程:
第一個if分支是對自簽名訪問設立條件:
domain不存在,或者
不容許無效證書,或者
不須要驗證域名,或者
SSLPinningMode不是AFSSLPinningModeNone,並且必須上傳了證書文件。若是是走了這個分支,就要求若是想要實現自簽名的HTTPS訪問成功,必須設置pinnedCertificates,且不能使用defaultPolicy,由於不能SSLPinningMode屬性是readonly的,而defaultPolicy在建立的時候已經設置SSLPinningMode屬性爲AFSSLPinningModeNone。(咱們剛纔的實現方案就是在這條分支下完成的)

接下來是這樣一塊代碼:

NSMutableArray *policies = [NSMutableArray array];
if (self.validatesDomainName) {
    [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
    [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}

SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

if (self.SSLPinningMode == AFSSLPinningModeNone) {
    return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
    return NO;
}

它完成的工做是:
先用policies數組組裝驗證策略,在經過SecTrustSetPolicies函數給serverTrust設置驗證策略,不過AFN並無接收函數的返回值,查看是否設置成功,不知道是爲何。
當SSLPinningMode爲AFSSLPinningModeNone時,若是容許無效的證書(allowInvalidCertificates = YES)直接返回評測成功,若是不容許,按照剛纔的驗證策略驗證,返回的是驗證的結果。
當SSLPinningMode不是AFSSLPinningModeNone時,若是既沒有驗證成功又不容許無效證書,則直接返回評測失敗。
(這裏讓我想到了另外一種訪問12306實現的方案:

manager.securityPolicy.validatesDomainName = NO;
manager.securityPolicy.allowInvalidCertificates = YES;

既不用使用證書,也不用本身建立securityPolicy。
)

接下來看一下那個長長的switch:

若是self.SSLPinningMode是AFSSLPinningModeCertificate:取出self.pinnedCertificates中的全部證書,經過SecTrustSetAnchorCertificates函數設置證書驗證策略,失敗則直接返回評測失敗,不然檢查本地的證書是否包含服務端的證書
,若是是返回評測成功,不然返回評測失敗。

若是self.SSLPinningMode是AFSSLPinningModePublicKey:取出服務端證書的全部公鑰,和self.pinnedPublicKeys中全部公鑰,遍歷檢查有沒有相等的兩項,有則返回評測成功。我嘗試給securityPolicy的pinnedPublicKeys賦值一個公鑰集合,可是它並無對外提供接口,self.pinnedPublicKeys是一個私有屬性,而且是計算型的,是從本地的證書self.pinnedCertificates中提取出來的。

有關AFSecurityPolicy最核心的部分基本上將完了,最後咱們仍是要總結一下,訪問可惡的12306的兩種方法:

// 方式一 兩句就能夠
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.securityPolicy.validatesDomainName = NO; // 關鍵語句1
manager.securityPolicy.allowInvalidCertificates = YES; // 關鍵語句2
[manager GET:@"https://kyfw.12306.cn/otn/leftTicket/init" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
    NSLog(@"%@", responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
}];


// 方式二 須要將證書導入到項目中
// 準備:將證書的二進制讀取,放入set中
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"kyfw.12306.cn.cer" ofType:nil];
NSData *cerData = [NSData dataWithContentsOfFile:cerPath];
NSSet *set = [[NSSet alloc] initWithObjects:cerData, nil];

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:set]; // 關鍵語句1
manager.securityPolicy.allowInvalidCertificates = YES; // 關鍵語句2
[manager GET:@"https://kyfw.12306.cn/otn/leftTicket/init" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
    NSLog(@"%@", responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
}];

AFN的AFNetworkReachabilityManager和Reachability

有關AFNetworkReachabilityManager使用比較簡單,不作太多的解釋,只是羅列一些注意點。
AFN開啓必須開啓監控以後才能獲取到新的網絡狀態,若是不開啓各類網絡狀態都爲不可到達,例如

AFNetworkReachabilityManager *reachabilityManager = [AFNetworkReachabilityManager sharedManager]; 

NSLog(@"%zd", reachabilityManager.isReachableViaWiFi); // 始終是0
NSLog(@"%zd", reachabilityManager.isReachable);
NSLog(@"%zd", reachabilityManager.isReachableViaWWAN);

即便開啓了網絡監控,也沒法再第一時間獲取到網絡狀態,例以下面的代碼執行以後,第一時間查看各類狀態依然不可達,這是由於它會在網絡情況改變時,異步改變單例中存儲的狀態。

AFNetworkReachabilityManager *reachabilityManager = [AFNetworkReachabilityManager sharedManager];
[reachabilityManager startMonitoring]; // 從開啓監控  到獲得下列值須要必定的時間
NSLog(@"%zd", reachabilityManager.isReachableViaWiFi); // 馬上調用爲0 ,過一段時間後準確
NSLog(@"%zd", reachabilityManager.isReachable); // 馬上調用爲0 ,過一段時間後準確
NSLog(@"%zd", reachabilityManager.isReachableViaWWAN); // 馬上調用爲0 ,過一段時間後準確

其實我使用較多的仍是Reachability框架,
Reachability具備獲取實時網絡狀態的-currentReachabilityStatus方法,不須要開啓監控,只要用實例調用便可。
Reachability一樣能夠進行網絡狀態改變的監控,能夠用-startNotifier方法開啓,可是無法傳入回調。可是每當網絡狀態改變的時候會發送一個kReachabilityChangedNotification通知,能夠接收這個通知完成回調。

相關文章
相關標籤/搜索