国行独有BUG——iOS10无线局域网权限问题解决方案
国行iPhone的独有无奈
iOS9之后苹果针对所有的APP权限新增蜂窝网络访问权限,默认都是允许访问状态,用户可以自行去“设置-蜂窝移动网络”里关闭该权限。
iOS9蜂窝移动网络应用权限设置
iOS10之后苹果针对国行手机的APP在蜂窝网络访问权限的基础上新增一个无线局域网权限的选择。这是由于中国大陆相关部门出台的新规定指出,应用在未经用户允许的前提下,系统不能授予其使用联网的功能,这其中就包括无线局域网(WIFI)。因此许多应用在第一次安装的时候会自动弹出一个弹窗询问用户是否允许该应用使用包括无线局域网和蜂窝移动数据。
iOS10第一次安装应用无线和蜂窝数据的授权弹窗
但是,这一新增的无线网络授权仅仅在手机系统层面,在应用开发的层面上苹果并未做任何相关的调整,也就是说:开发者无法通过系统API获取到当前用户对于某个应用的无线局域网授权情况,因此,如果用户在上图中不注意选择了不允许或者由于系统BUG的原因(原因详见:https://juejin.im/post/57e229880e3dd90069867129)无法获得对应的授权,则很容易被用户误解是该APP出现BUG,用户体验也会大打折扣。
解决思路
首先,我们明确一点:苹果官方没有提供对应的API供开发人员获取到应用的无线局域网的授权情况。我们解决问题的方法是要引导用户到相应的系统界面进行开启权限操作(在“设置-无线局域网/蜂窝移动网络-使用无线局域网与蜂窝移动的应用”里找到对应的APP开启权限)。
接下来,思考一下:能不能对这一种场景进行代码层面的推断呢?我们尝试收集相关的信息:
- 该权限被关闭的结果:在WIFI可以访问网络的情况下,应用内无法访问网络
- 无线局域网权限是在蜂窝移动网络授权的基础上新增一个选项而来
-
国行iOS10的网络授权选项包括“关闭”、“无线局域网”、“无线局域网与蜂窝移动数据”
应用网络授权界面
以上信息提炼一下:
- 已连接到某个无线局域网(成功连接某个网络并且能获取到SSID信息)
- 应用内网络不可触达
- 网络访问权限被关闭
针对以上三点,就能推断出应用的无线网络权限被关闭,那么我们开始对这三点“优雅”地书写代码。
1. 判断当前手机成功连接某个网络并且能获取到SSID信息
以下代码若能成功返回非空信息,则说明成功连接到某个网络(返回的内容为SSID信息)
- (NSDictionary *)fetchSSIDInfo {
NSArray *ifs = (__bridge_transfer NSArray *)CNCopySupportedInterfaces();
if (!ifs) {
return nil;
}
NSDictionary *info = nil;
for (NSString *ifnam in ifs) {
info = (__bridge_transfer NSDictionary *)CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam);
if (info && [info count]) { break; }
}
return info;
}
2. 判断应用内网络不可触达
这里使用 AFNetworking 的 AFNetworkReachabilityManager 进行监测,可以使用苹果官方的 Reachability (https://developer.apple.com/library/ios/#samplecode/Reachability/Introduction/Intro.html),二者同源。
PS:这里的判断方法并不是真正意义上的判断网络是否可以触达,该方法仅仅判断应用能否连接上手机网络,网络类型如何,并不能判断手机连接到无线局域网之后是否可以访问外网的情况,不过用这种方法已经满足我们的需求,因为权限限制是在应用能否访问手机网络这一节点。
- (void)startAFNetworkMonitoring {
// 这里sharedHTTPSessionManager使用只是对AFHTTPSessionManager进行单例封装,因为对默认的AFHTTPSessionManager直接使用有内存泄漏的问题
AFHTTPSessionManager *manager = [KCSharedSessionManager sharedHTTPSessionManager];
// 应用网络状态改变时执行异步回调
[manager.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
switch (status) {
// 蜂窝网络
case AFNetworkReachabilityStatusReachableViaWWAN:
break;
case AFNetworkReachabilityStatusReachableViaWiFi:
_netState = YES;
break;
case AFNetworkReachabilityStatusNotReachable:
_netState = NO;
break;
case AFNetworkReachabilityStatusUnknown:
_netState = NO;
break;
default:
_netState = NO;
break;
}
}];
[manager.reachabilityManager startMonitoring];
}
3. 判断网络访问权限被关闭
使用iOS9新增的系统库CoreTelephony.framework进行判断(蜂窝网络权限授权也是iOS9才增加的)。
这里带一个思考:无线局域网权限授权是在蜂窝网络权限授权的基础上新增一项,我们可以尝试下通过这个库,获取到的仅仅只有无线局域网权限而无蜂窝网络权限时的授权状态值是怎样的。
PS:值得一提的是,CTCellularData的block属性cellularDataRestrictionDidUpdateNotifier并不会自动释放,而且即使对应的CTCellularData实例释放了,该block属性也不会释放,注意使用即可
@import CoreTelephony;
- (void)startCellularDataAuthMonitoring {
if (!self.cellularData) self.cellularData = [[CTCellularData alloc] init];
self.cellularData.cellularDataRestrictionDidUpdateNotifier = nil;
if (self.cellularData) {
// 该block为异步回调
self.cellularData.cellularDataRestrictionDidUpdateNotifier = ^(CTCellularDataRestrictedState state) {
// 获取应用联网授权状态
switch (state) {
case kCTCellularDataRestricted: NSLog(@"Restricrted"); // 权限受限
break;
case kCTCellularDataNotRestricted: NSLog(@"Not Restricted"); // 权限不受限
break;
case kCTCellularDataRestrictedStateUnknown: NSLog(@"Unknown"); // 未知,第一次请求
break;
default:
break;
}
};
};
}
经过测试,发现如下图的状态值对应的网络受限情况:
网络受限情况对应的状态值
总结使用
现在逻辑已经很清晰,就是当以上三个判断都成立的时候,可以推断出用户关闭了应用访问无线互联网的权限,这时候就可以弹出引导用户打开相关权限的弹窗了。
这里有一个需要注意的地方是:CTCellularData和AFNetworkReachabilityManager用户监测的回调都是异步的,也就是说,当发生对应的状态改变时,回调才会生效,这里需要分别对状态值的改变进行监听或者设置依赖才能正确执行我们的判断逻辑,这里我使用了RAC进行信号的监听。
@weakify(self);
// 当网络权限和网络状态值发生改变(开机启动时状态值从空改变到有值)时,触发信号监听逻辑
[[[RACSignal merge:@[RACObserve(self.cellularData, restrictedState),
RACObserve([KCSharedSessionManager sharedHTTPSessionManager].reachabilityManager, networkReachabilityStatus)]]
bufferWithTime: 0 onScheduler: [RACScheduler mainThreadScheduler]]
subscribeNext: ^(id value) {
@strongify(self);
if (self.cellularData.restrictedState == kCTCellularDataRestricted && [KCSharedSessionManager sharedHTTPSessionManager].reachabilityManager.networkReachabilityStatus == AFNetworkReachabilityStatusNotReachable && [self fetchSSIDInfo]) {
// 回到主线程
dispatch_async_on_main_queue(^{
// 显示弹窗
});
}
}];