苹果示例源码阅读:Reachability

[中文教程]
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看164 回复回复5 收藏收藏1 分享淘帖 转播转播 分享分享 微信
查看: 164|回复: 5
收起左侧

[中文教程] 苹果示例源码阅读:Reachability

[复制链接]
swifter 发表于 2016-10-25 10:40:54 | 显示全部楼层 |阅读模式
快来登录
获取最新的苹果动态资讯
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码

Reachability 是苹果官方提供的示例源码,它是对 SystemConfiguration.framework 模块中的 SCNetworkReachability.h 头文件里提供的一系列网络连接状态相关的 C 函数进行简单封装,以示范如何在 iOS App 开发中实现网络状态变化监听,由此也衍生出各种 Reachability 框架,比较著名的有 Github 上的 tonymillion/Reachability 以及 AFNetworking 中的 AFNetworkReachabilityManager 模块,它们的实现原理基本上是完全相同的。

下面我们就来阅读分析一下苹果提供的 Reachability 源码,源码中最核心的就 Reachability.h 和 Reachability.m 两个文件。




1、初始化方法

Reachability 中提供了三个快速初始化方法,分别为 reachabilityWithHostName:、reachabilityWithAddress: 和 reachabilityForInternetConnection。



1.1、reachabilityWithHostName: 方法

该方法通过指定的 服务器域名 初始化一个 Reachability 对象以进行判断网络连接状态,源码如下:

[Objective-C] 纯文本查看 复制代码
+ (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 成员变量,以供后面网络状态监听使用。



1.2、reachabilityWithAddress: 方法

该方法通过指定的 服务器 IP 地址 初始化一个 Reachability 对象以进行判断网络连接状态,源码如下:

[Objective-C] 纯文本查看 复制代码
+ (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 成员变量。



1.3、reachabilityForInternetConnection 方法

该方法通过 默认的路由地址 初始化一个 Reachability 对象以进行判断网络连接状态,通常用于 App 没有连接到特定主机的情况,源码如下:

[Objective-C] 纯文本查看 复制代码
+ (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 对象。




2、网络状态监听



2.1、开始监听

通过上述初始化方法获得一个 Reachability 对象后,可调用 startNotifier 方法开始进行网络状态变化的监听,源码如下:

[Objective-C] 纯文本查看 复制代码
- (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 的定义和注释如下:

[Objective-C] 纯文本查看 复制代码
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 通知以获得实时网络连接状态的变化。

[Objective-C] 纯文本查看 复制代码
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 规定了自定义的回调函数的参数需要满足如下形式:

[Objective-C] 纯文本查看 复制代码
typedef void (*SCNetworkReachabilityCallBack) (
    SCNetworkReachabilityRef target,
    SCNetworkReachabilityFlags flags,
    void * __nullable info
);




2.2、取消监听

我们可调用 Reachability 对象的 stopNotifier 进行取消网络连接状态变化的监听,源码如下:

[Objective-C] 纯文本查看 复制代码
- (void)stopNotifier
{
    if (_reachabilityRef != NULL)
    {
        // 通过调用 SCNetworkReachabilityUnscheduleFromRunLoop 函数设置 Reachability 对象的 ref 在 Current Runloop 中对应的模式(kCFRunLoopDefaultMode)取消监听网络状态。
        SCNetworkReachabilityUnscheduleFromRunLoop(_reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
    }
}




2.3、释放对象

当要释放一个 Reachability 对象时,我们需要在其 dealloc 方法里取消网络状态监听。另外由于 SCNetworkReachabilityRef 是 Core Foundation 对象,所以这里需要调用 CFRelease() 函数释放 _reachabilityRef。

[Objective-C] 纯文本查看 复制代码
- (void)dealloc
{
    [self stopNotifier];
    if (_reachabilityRef != NULL)
    {
        CFRelease(_reachabilityRef);
    }
}





3、获取当前网络连接状态

当通过上述方法初始化一个 Reachability 对象并调用 startNotifier 方法开始监听后,我们可以随时调用对象的 currentReachabilityStatus 方法获取当前网络连接状态,返回的状态类型 NetworkStatus 定义如下:

[Objective-C] 纯文本查看 复制代码
typedef enum : NSInteger {
    NotReachable = 0, //无网络连接
    ReachableViaWiFi, //网络通过 WiFi 连接
    ReachableViaWWAN  //网络通过移动网络连接
} NetworkStatus;


currentReachabilityStatus 方法的实现源码如下,首先通过调用 SCNetworkReachabilityGetFlags(...) 函数并传入 _reachabilityRef 引用作为参数,获得一个表示当前网络连接状态的 SCNetworkReachabilityFlags 枚举值,然后根据枚举值调用 networkStatusForFlags: 方法判断当前网络状态类型并返回。

[Objective-C] 纯文本查看 复制代码
- (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,具体实现和注释如下:

[Objective-C] 纯文本查看 复制代码
- (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 等,该函数的实现如下:

[Objective-C] 纯文本查看 复制代码
#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 判断,实现如下:

[Objective-C] 纯文本查看 复制代码
- (BOOL)connectionRequired
{
    NSAssert(_reachabilityRef != NULL, @"connectionRequired called with NULL reachabilityRef");
    SCNetworkReachabilityFlags flags;
    if (SCNetworkReachabilityGetFlags(_reachabilityRef, &flags))
    {
        return (flags & kSCNetworkReachabilityFlagsConnectionRequired);
    }
    return NO;
}





4、使用示例

在 Reachability 源码的 APLViewController.m 文件中,苹果给出了上述封装的使用示例。在我们的 App 开发中,我们可以按如下步骤获取当前网络连接类型或者监听网络连接变化:

[Objective-C] 纯文本查看 复制代码
// 1、添加 kReachabilityChangedNotification 通知监听,以监听网络连接变化;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reachabilityChanged:) name:kReachabilityChangedNotification object:nil];
// 2、 根据 [url]www.apple.com[/url] 域名初始化一个 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;
    }
}





5、总结

通过分析上述 Reachability 源码,我们可以总结 SCNetworkReachability.h 头文件里提供的一系列网络连接状态相关的 C 函数的使用流程如下:

苹果示例源码阅读:Reachability SCNetworkReachability 使用示意图

苹果示例源码阅读:Reachability - 敏捷大拇指 - 苹果示例源码阅读:Reachability SCNetworkReachability 使用示意图

SCNetworkReachability 使用示意图

  • 首先在 SCNetworkReachabilityCreateWithName(...)、SCNetworkReachabilityCreateWithAddress(...)、SCNetworkReachabilityCreateWithAddressPair(...) 3个初始化函数中任选其一创建一个 SCNetworkReachabilityRef 引用;
  • 其次根据 SCNetworkReachabilityCallBack 定义一个网络监听回调函数,并初始化一个 SCNetworkReachabilityContext 上下文信息,然后调用 SCNetworkReachabilitySetCallback 函数并传入上述 ref、callback、context 3个参数,设置上述创建的 ref 在网络状态发生变化时的回调函数;
  • 通过调用 SCNetworkReachabilityScheduleWithRunLoop(...) 或 SCNetworkReachabilityUnscheduleFromRunLoop(...) 函数并传入上述 ref,在 Current Runloop 中开始或取消监听网络连接状态变化,另外也可以通过 SCNetworkReachabilitySetDispatchQueue(...) 函数设置在指定线程里监听;
  • 调用 SCNetworkReachabilityGetFlags(...) 函数并传入上述 ref,可获得当前网络连接状态的 flags 枚举值,另外需要注意的是,当 DNS 服务器无法连接,或者在弱网环境下,此函数将会很耗时,所以苹果建议在子线程里异步调用此函数;
  • 根据不同的 SCNetworkReachabilityFlags 枚举值,判断当前网络连接状态和连接类型。





6、参考 Reference

Apple Sample Code: Reachability

SCNetworkReachability API Reference

tonymillion/Reachability

AFNetworkReachabilityManager

AFNetworking 3.0 源码阅读笔记(六)

【原】AFNetworking 源码阅读(六)

iOS 学习之 Reachability (一)




作者:Kang Zubin

都看到这里了,就把这篇资料推荐给您的好朋友吧,让他们也感受一下。

回帖是一种美德,也是对楼主发帖的尊重和支持。

*声明:敏捷大拇指是全球最大的Swift开发者社区、苹果粉丝家园、智能移动门户,所载内容仅限于传递更多最新信息,并不意味赞同其观点或证实其描述;内容仅供参考,并非绝对正确的建议。本站不对上述信息的真实性、合法性、完整性做出保证;转载请注明来源并加上本站链接,敏捷大拇指将保留所有法律权益。如有疑问或建议,邮件至marketing@swifthumb.com

*联系:微信公众平台:“swifthumb” / 腾讯微博:@swifthumb / 新浪微博:@swifthumb / 官方QQ一群:343549891(满) / 官方QQ二群:245285613 ,需要报上用户名才会被同意进群,请先注册敏捷大拇指

嗯,不错!期待更多好内容,支持一把:
支持敏捷大拇指,用支付宝支付10.24元 支持敏捷大拇指,用微信支付10.24元

评分

参与人数 1金钱 +10 贡献 +10 专家分 +10 收起 理由
Anewczs + 10 + 10 + 10 32个赞!专家给力!

查看全部评分

我是90后 发表于 2016-10-25 11:11:20 | 显示全部楼层
我都直接调用AFNetworking,都没怎么看源码呢
Ding 发表于 2016-10-25 11:20:44 | 显示全部楼层
看官方实例代码真享受~
如果把Swift的示例贴过来更好。
迷途的羔羊 发表于 2016-10-25 11:45:22 | 显示全部楼层
Ding 发表于 2016-10-25 11:20
看官方实例代码真享受~
如果把Swift的示例贴过来更好。

你来啊~
Ding 发表于 2016-10-25 12:51:34 | 显示全部楼层

来不了了,缘不凑巧~
SwiftRobot 发表于 2016-10-26 06:24:23 | 显示全部楼层
Nice
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

做任务,领红包。
我要发帖

分享扩散

都看到这里了,就把这资料推荐给您的好朋友吧,让他们也感受一下。
您的每一位朋友访问此永久链接后,您都将获得相应的金钱积分奖励
热门推荐

合作伙伴

Swift小苹果

  • 北京治世天下科技有限公司
  • ©2014-2016 敏捷大拇指
  • 京ICP备14029482号
  • Powered by Discuz! X3.1 Licensed
  • swifthumb Wechat Code
  •   
快速回复 返回顶部 返回列表