AFNetworkReachabilityManager 是对系统 SCNetworkReachability的封装,算是 AFN 里面一个相对比较独立的模块了,m 文件只有短短240行,不过里面还是有很多C 和 OC 的语言基础可以给我们复习提供一个良好的示例。
TL;DR
这个组件我认为有以下主要的知识点
- 使用__bridge 系列方法 CF 对象的内存管理权在 MRC 和 ARC 相互转换
- Block 在 MRC 下需要使用 Block_copy Block_release 管理生命周期
- 重载 keyPathsForValuesAffectingValueForKey 来让「计算属性」获得 KVO 能力
针对 Reachability 我们需要知道的是: - 只要数据包能发出去就会被 Api 认为是 Reachable 了
- Reachability 建议被用作如下,而不是阻止一个网络请求:
- 网络恢复时的重试
- 网络失败的原因
先看头文件可以让我们更加容易理解作者想要我们如何使用这个模块。
.h
Reachability 功能依赖于 SystemConfiguration.framework
关于NS_DESIGNATED_INITIALIZER
的宏定义与NSObjCRuntime.h
中一致。
配合 Swift 中的 designated 概念。
#ifndef NS_DESIGNATED_INITIALIZER
#if __has_attribute(objc_designated_initializer)
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
#else
#define NS_DESIGNATED_INITIALIZER
#endif
#endif
在 Class 的注释中,说明了建议用法,和苹果官方的示例代码。
/*
...
See Apple's Reachability Sample Code (https://developer.apple.com/library/ios/samplecode/reachability/)
@warning Instances of `AFNetworkReachabilityManager` must be started with `-startMonitoring` before reachability status can be determined.
*/
@interface AFNetworkReachabilityManager : NSObject
根据这个 @warning 可以看出,其实 Manager 的核心方法是围绕着 -startMonitoring
。
消息通过注册 block 或者 notification 通知到各处。
.m
初始化
初始化方法有如下
// 使用一个空地址,使用 address 系列的方法
+ (instancetype)sharedManager {
static AFNetworkReachabilityManager *_sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_len = sizeof(address);
address.sin_family = AF_INET;
// 创建对象
_sharedManager = [self managerForAddress:&address];
});
return _sharedManager;
}
#ifndef __clang_analyzer__
+ (instancetype)managerForAddress:(const void *)address {
SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *)address);
AFNetworkReachabilityManager *manager = [[self alloc] initWithReachability:reachability];
return manager;
}
#endif
- (instancetype)initWithReachability:(SCNetworkReachabilityRef)reachability {
self = [super init];
if (!self) {
return nil;
}
// 转换对象所有权到 ARC,防止被意外释放
self.networkReachability = CFBridgingRelease(reachability);
self.networkReachabilityStatus = AFNetworkReachabilityStatusUnknown;
return self;
}
memset 和 bzero
memset 和 bzero 是两个常用的初始化 C 结构体的方法
memset(&st, 0, sizeof (st));
bzero(&set, sizeof (st)); // sharedManager 使用此方法
memset 和 bzero的主要区别是:参数个数不同;memset 需要三个参数,其中第二个参数是&st指向的内存中要初始化的值,而 bzero 使用 0 来初始化
Toll-Free Bridging
对于 CF 系列对象和 ARC 内存管理相互转换,这部分被称为Toll-Free Bridging
- __bridge 不改变对象所有权
- __bridge_retained = CFBridgingRetain() 解除 ARC 所有权,此对象需要手动调用CFRelease() 来 retain count - 1
- __bridge_transfer = CFBridgingRelease() 给予 ARC 所有权
CFBridgingRetain() 和 CFBridgingRelease() 是双下划线修饰符对应的 inline 函数,效果一样。
开始获取数据
- (void)startMonitoring {
[self stopMonitoring];
if (!self.networkReachability) {
return;
}
// weak strong dance
// 这里用 block 封装主要是因为,SC 方法回调只能指定一个具体的 C 函数指针,而 C 函数是没有实例上下文的,也就无法调用 self
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};
id networkReachability = self.networkReachability;
// 使用 C 标准化方法初始化结构体
// 因为使用了 block,所以需要告诉 SC 方法如何 retain 和 release 第二个参数
// 苹果的官方示例代码在这里传入的是 NULL,因为他不需要使用这些信息
SCNetworkReachabilityContext context = {0, (__bridge void *)callback, AFNetworkReachabilityRetainCallback, AFNetworkReachabilityReleaseCallback, NULL};
SCNetworkReachabilitySetCallback((__bridge SCNetworkReachabilityRef)networkReachability, AFNetworkReachabilityCallback, &context);
// 加入 runloop
SCNetworkReachabilityScheduleWithRunLoop((__bridge SCNetworkReachabilityRef)networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
// 这里不知道为什么要放在后台线程,然后转前台
// 根据测试,这个 API 并不会显著阻塞当前线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^{
SCNetworkReachabilityFlags flags;
if (SCNetworkReachabilityGetFlags((__bridge SCNetworkReachabilityRef)networkReachability, &flags)) {
// 在这里做了转回到主线程的操作
AFPostReachabilityStatusChange(flags, callback);
}
});
}
static void AFPostReachabilityStatusChange(SCNetworkReachabilityFlags flags, AFNetworkReachabilityStatusBlock block) {
AFNetworkReachabilityStatus status = AFNetworkReachabilityStatusForFlags(flags);
// 保证对外发送的数据都是在主线程
dispatch_async(dispatch_get_main_queue(), ^{
if (block) {
block(status);
}
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
NSDictionary *userInfo = @{ AFNetworkingReachabilityNotificationStatusItem: @(status) };
[notificationCenter postNotificationName:AFNetworkingReachabilityDidChangeNotification object:nil userInfo:userInfo];
});
}
static void AFNetworkReachabilityCallback(SCNetworkReachabilityRef __unused target, SCNetworkReachabilityFlags flags, void *info) {
AFPostReachabilityStatusChange(flags, (__bridge AFNetworkReachabilityStatusBlock)info);
}
关于 weak strong dance
括号中使用 strong 或者直接用 weakSelf 其实最最后 run 的效果几乎没有影响,唯一的差别是:
- weak 会在每次赋值的时候自动 retain 一个 strong 对象出来,造成额外的性能负担,
- weak 可能突然变成 nil 而导致余下的代码跑不出效果
Block_copy & Block_release
其实 context 对象并不知道实际的操作,retain 和 release 都是通过委托的指针来做的
而 copy 和 release 指向了一个宏,这个宏又指向系统库
做的事情也很简单:
- 如果 heap 上已经有这个 block,引用 +1
- 如果 heap 上没有,搞一个 copy 出来
static const void * AFNetworkReachabilityRetainCallback(const void *info) {
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
return Block_copy(info);
}
static void AFNetworkReachabilityReleaseCallback(const void *info) {
if (info) {
Block_release(info);
}
}
// usr/include/block.h
// Type correct macros
#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
// Lose the reference, and if heap based and last reference, recover the memory
BLOCK_EXPORT void _Block_release(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
消息获取与处理
在 -startMonitoring
之后,可以访问属性或者注册回调通知来获取最新的网络状态。
获取当前状态有四个方法:
// 核心属性
@property (readonly, nonatomic, assign) AFNetworkReachabilityStatus networkReachabilityStatus;
// 下列三个计算属性都是通过上面的属性得到
@property (readonly, nonatomic, assign, getter = isReachable) BOOL reachable;
@property (readonly, nonatomic, assign, getter = isReachableViaWWAN) BOOL reachableViaWWAN;
@property (readonly, nonatomic, assign, getter = isReachableViaWiFi) BOOL reachableViaWiFi;
在网络状态变化时获得通知,可以通过:
// block callback
- (void)setReachabilityStatusChangeBlock:(nullable void (^)(AFNetworkReachabilityStatus status))block;
// NSNotificationCenter
FOUNDATION_EXPORT NSString * const AFNetworkingReachabilityDidChangeNotification;
// Notification 中 userInfo 的 key
FOUNDATION_EXPORT NSString * const AFNetworkingReachabilityNotificationStatusItem;
把得到的 status 翻译成人类语言:
- (NSString *)localizedNetworkReachabilityStatusString;
FOUNDATION_EXPORT NSString * AFStringFromNetworkReachabilityStatus(AFNetworkReachabilityStatus status);
KVO 的消息转发机制
- (BOOL)isReachable {
return [self isReachableViaWWAN] || [self isReachableViaWiFi];
}
- (BOOL)isReachableViaWWAN {
return self.networkReachabilityStatus == AFNetworkReachabilityStatusReachableViaWWAN;
}
- (BOOL)isReachableViaWiFi {
return self.networkReachabilityStatus == AFNetworkReachabilityStatusReachableViaWiFi;
}
#pragma mark - NSKeyValueObserving
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
if ([key isEqualToString:@"reachable"] || [key isEqualToString:@"reachableViaWWAN"] || [key isEqualToString:@"reachableViaWiFi"]) {
return [NSSet setWithObject:@"networkReachabilityStatus"];
}
return [super keyPathsForValuesAffectingValueForKey:key];
}
使用-keyPathsForValuesAffectingValueForKey:
方法,可以让一个没有 setter 的属性被观察。
在原文中reachable
,reachableViaWWAN
,reachableViaWiFi
三个都是计算属性,所以他们没有人实现值改变的通知。
所以这里使用-keyPathsForValuesAffectingValueForKey
方法来依赖networkReachabilityStatus
的改变来实现,三个计算属性的通知。
参考:
Determining Reachability and Getting Connected
SCNetworkReachability