跳至主要内容

检测 iOS 系统网络权限被关闭

参考链接

本人总结:
使用AFNetworkReachabilityManager监听网络状态变化,同时监听用户授权网络访问权限的状态变化。
在App首页进行判断:
无网络情况下,判断restrictedState是否为kCTCellularDataNotRestricted,是的情况下弹框提示用户去设置,否则属于用户网络连接问题,不给提示。

以下为转载
============================================

背景

一直都有用户反馈无法正常联网的问题,经过定位,发现很大一部分用户是因为网络权限被系统关闭,经过资料搜集和排除发现根本原因是:
1、第一次打开 app 不能访问网络,无任何提示
2、第一次打开 app 直接提示「已为“XXX”关闭网络」
3、第一次打开 app ,用户点错了选择了「不允许」或「WLAN」
对于第 1 种情况,出现在 iOS 10 比较多,一旦出现后系统设置里也找不到「无线数据」这一配置选项,随着 iOS 的更新,貌似被 Apple 修复了,GitHub 上面有 ZIKCellularAuthorization 其进行分析和提出一种解决方案,强制让系统弹出那个询问框。
但是第 2、3种情况现在 iOS 12 还经常有发生,对于这种情况,我们只要检测出来,并提示引导用户去打开网络权限即可,本文提出一新的方法来检测这种情况。

CTCellularData 的局限性

关于网络权限问题,网络上搜集的资料大多数提到了用 CTCellularData 的 cellularDataRestrictionDidUpdateNotifier 方法去判断网络权限关闭,但这样判断会有不完善的情况(后面提到)
CoreTelephony 里的 CTCellularData 可以用来监测 app 的蜂窝网络权限,其定义如下:
typedef NS_ENUM(NSUInteger, CTCellularDataRestrictedState) {
    kCTCellularDataRestrictedStateUnknown, 
    kCTCellularDataRestricted,            
    kCTCellularDataNotRestricted          
};
通过注册 cellularDataRestrictionDidUpdateNotifier 回调可以并判断其 state 可以判断蜂窝数据的权限
CTCellularData *cellularData = [[CTCellularData alloc] init];
    cellularData.cellularDataRestrictionDidUpdateNotifier = ^(CTCellularDataRestrictedState restrictedState) {
           ...
        }
    };
系统设置里 有三种选项分别对应:
系统选项 CTCellularDataRestrictedState
关闭 kCTCellularDataRestricted
WLAN kCTCellularDataRestricted
WALN 与蜂窝移动网 kCTCellularDataNotRestricted
实测发现:
1、若用户此时用蜂窝数据上网,但在「允许“XXX”使用的数据」,选择了「WLAN」 或 「关闭」,回调拿到的值是
kCTCellularDataRestricted ,此时我们可以确定是因为权限问题导致用户不能访问,应该去提示用户打开网络权限。
2、若用户此时用 Wi-Fi 上网,但在「允许“XXX”使用的数据」设置中选择了 「关闭」,我们拿到的值是 kCTCellularDataRestricted ,这种情况下同样需要提示用户打开网络权限。
3、若用户此时用 Wi-Fi 上网,但在「允许“XXX”使用的数据」设置中选择了 「WLAN」,我们拿到的值是 kCTCellularDataRestricted ,但是此时用户是有网络访问权限的,此时不应该去提示用户。

判断思路

结合 SCNetworkReachabilityRef 的回调,以及对网络状态的区分来判断:
通过判断 SCNetworkReachabilityRef 回调的 flag 发现 kSCNetworkReachabilityFlagsReachable 为 0,则说明网络不通,此时可能有两种情况:
1、未打开任何数据连接(Wi-Fi 蜂窝数据)或者开启了飞行模式
2、网络权限被关闭
所以我们的判断思路就是要判断出用户是否 开启了 Wi-Fi 或者 蜂窝数据,如果都不是那必定是网络权限被关闭。

实现细节

判断当前网络类型

思路:
1、先通过 CaptiveNetwork 去判断有没有开启 Wi-Fi,这个判断无论在网络权限是否打开下的判断都是准确的。
2、由于在没有网络权限的情况下,没有办法直接去判断是否开启了蜂窝数据,这里只能通过一种比较 trick 的方式,通过状态栏去判断用户是否开启了蜂窝数据,但是在一些极端的情况下,不一定准确,比如用户同时开启 Wi-Fi 和蜂窝数据,此时先关闭 Wi-Fi 然后迅速关闭蜂窝数据,此时手机处于无网络状态,我们在第 1 步判断出了 Wi-Fi 不可用,但是通过状态栏的方式拿到却还是 Wi-Fi,在这种比较边界的情况下,只能延时一会儿再次检查。
- (void)getCurrentNetworkType:(void(^)(ZYNetworkType))block {
    if ([self isWiFiEnable]) {
        return block(ZYNetworkTypeWiFi);
    }
   ZYNetworkType type = [self getNetworkTypeFromStatusBar];
    if (type == ZYNetworkTypeOffline) {
        block(ZYNetworkTypeOffline);
    } else if (type == ZYNetworkTypeWiFi) { // 这时候从状态栏拿到的是 Wi-Fi 说明状态栏没有刷新,延迟一会再获取
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self getCurrentNetworkType:block];
        });
    } else {
        block(ZYNetworkTypeCellularData);
    }
}

判断是否连接到 Wi-Fi

判断 Wi-Fi 的方法比较简单,导入 SystemConfiguration/CaptiveNetwork.h 并使用下面方法判断即可
- (BOOL)isWiFiEnable {
    NSArray *interfaces = (__bridge_transfer NSArray *)CNCopySupportedInterfaces();
    if (!interfaces) {
        return NO;
    }
    NSDictionary *info = nil;
    for (NSString *ifnam in interfaces) {
        info = (__bridge_transfer NSDictionary *)CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam);
        if (info && [info count]) { break; }
    }
    return (info != nil);
}

从状态栏判断网络类型

上面提到,由于在网络权限拒绝的情况下,我们唯一比较有效的方法是通过状态栏去判断,这个判断方法在网上可以找到,但是 在 iPhone X 会出现 crash 的情况,我针对 iPhone X 做了补充和适配。
- (ZYNetworkType)getNetworkTypeFromStatusBar {
    NSInteger type = 0;
    @try {
        UIApplication *app = [UIApplication sharedApplication];
        UIView *statusBar = [app valueForKeyPath:@"statusBar"];
        
        if (statusBar == nil ){
            return ZYNetworkTypeUnknown;
        }
        BOOL isModernStatusBar = [statusBar isKindOfClass:NSClassFromString(@"UIStatusBar_Modern")];
        if (isModernStatusBar) { // 在 iPhone X 上 statusBar 属于 UIStatusBar_Modern ,需要特殊处理
            id currentData = [statusBar valueForKeyPath:@"statusBar.currentData"];
            BOOL wifiEnable = [[currentData valueForKeyPath:@"_wifiEntry.isEnabled"] boolValue];
            // 这里不能用 _cellularEntry.isEnabled 来判断,该值即使关闭仍然有是 YES
            BOOL cellularEnable = [[currentData valueForKeyPath:@"_cellularEntry.type"] boolValue];
            return  wifiEnable     ? ZYNetworkTypeWiFi :
                    cellularEnable ? ZYNetworkTypeCellularData : ZYNetworkTypeOffline;
    
        } else { // 传统的 statusBar
            NSArray *children = [[statusBar valueForKeyPath:@"foregroundView"] subviews];
            for (id child in children) {
                if ([child isKindOfClass:[NSClassFromString(@"UIStatusBarDataNetworkItemView") class]]) {
                    type = [[child valueForKeyPath:@"dataNetworkType"] intValue];
                    // type == 1  => 2G
                    // type == 2  => 3G
                    // type == 3  => 4G
                    // type == 4  => LTE
                    // type == 5  => Wi-Fi
                }
            }
            return type == 0 ? ZYNetworkTypeOffline :
                   type == 5 ? ZYNetworkTypeWiFi    : ZYNetworkTypeCellularData;
        }
    } @catch (NSException *exception) {
    }
    return 0;
}

整体判断代码

- (void)startCheck {
    
    /* iOS 10 以下默认通过 **/
    
    /* 先用 currentReachable 判断,若返回的为 YES 则说明:
     1. 用户选择了 「WALN 与蜂窝移动网」并处于其中一种网络环境下。
     2. 用户选择了 「WALN」并处于 WALN 网络环境下。
     
     此时是有网络访问权限的,直接返回 ZYNetworkAccessible
    **/
    if ([UIDevice currentDevice].systemVersion.floatValue < 10.0 || [self currentReachable]) {
        [self notiWithAccessibleState:ZYNetworkAccessible];
        return;
    }
    
    CTCellularDataRestrictedState state = _cellularData.restrictedState;
    
    switch (state) {
        case kCTCellularDataRestricted: {// 系统 API 返回 无蜂窝数据访问权限
            
            [self getCurrentNetworkType:^(ZYNetworkType type) {
                /*  若用户是通过蜂窝数据 或 WLAN 上网,走到这里来 说明权限被关闭**/
                
                if (type == ZYNetworkTypeCellularData || type == ZYNetworkTypeWiFi) {
                    [self notiWithAccessibleState:ZYNetworkRestricted];
                } else {  // 可能开了飞行模式,无法判断
                    [self notiWithAccessibleState:ZYNetworkUnknown];
                }
            }];
            
            break;
        }
        case kCTCellularDataNotRestricted: // 系统 API 访问有有蜂窝数据访问权限,那就必定有 Wi-Fi 数据访问权限
            [self notiWithAccessibleState:ZYNetworkAccessible];
            break;
        case kCTCellularDataRestrictedStateUnknown:
            [self notiWithAccessibleState:ZYNetworkUnknown];
            break;
        default:
            break;
    };
}

ZYNetworkAccessibity

GitHub : ZYNetworkAccessibity
我已经把上面的方法做了封装,将 ZYNetworkAccessibity.h 和 ZYNetworkAccessibity.m 拖项目中,监听 ZYNetworkAccessibityChangedNotification 通知即可
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChanged:) name:ZYNetworkAccessibityChangedNotification object:nil];
然后处理通知
- (void)networkChanged:(NSNotification *)notification {
    
    ZYNetworkAccessibleState state = ZYNetworkAccessibity.currentState;

    if (state == ZYNetworkRestricted) {
        NSLog(@"网络权限被关闭");
    }
}
另外还实现了自动提醒用户打开权限,如果你需要,请打开
[ZYNetworkAccessibity setAlertEnable:YES];


评论

此博客中的热门博文

Resolving errSecInternalComponent errors during code signing

原文链接 One code signing issue I commonly see, both here on DevForums and in my Day Job™ with DTS, is that the codesign command fails with errSecInternalComponent. This issue crops up in a wide variety of circumstances and the correct fix depends on the specific problem. This post is my attempt to clarify the potential causes of this error and help folks resolve it. If you have any questions or comments about this, please start a new thread, tagging it with Code Signing so that I see it. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Resolving errSecInternalComponent errors during code signing In some circumstances the codesign command might fail with the error errSecInternalComponent. For example: % codesign -s "Apple Development" "MyTrue" MyTrue: errSecInternalComponent This typically affects folks who are signing code in a nonstandard environm...

iOS:检测使用VPN或Proxy

参考链接: https://www.jianshu.com/p/c3b950dbf86a https://gist.github.com/PramodJoshi/4faad4c91f7dcb4eb9b06be8390c01db http://noodlecode.net/2018/04/check-if-ios-app-is-connected-to-vpn 第一种方法 需要导入框架CFNetwork 然后,这个方法是mrc的:需要添加-fno-objc-arc的flag 代码如下: + ( BOOL )getProxyStatus { NSDictionary *proxySettings = NSMakeCollectable ([( NSDictionary *) CFNetworkCopySystemProxySettings () autorelease]); NSArray *proxies = NSMakeCollectable ([( NSArray *) CFNetworkCopyProxiesForURL (( CFURLRef )[ NSURL URLWithString: @"http://www.google.com" ], ( CFDictionaryRef )proxySettings) autorelease]); NSDictionary *settings = [proxies objectAtIndex: 0 ]; NSLog ( @"host=%@" , [settings objectForKey:( NSString *)kCFProxyHostNameKey]); NSLog ( @"port=%@" , [settings objectForKey:( NSString *)kCFProxyPortNumberKey]); NSLog ( @"type=%@" , [settings objectForKey:( NSString *)kCFProxyTypeKey]); if ([[settings object...

去广告DNS设置,国内ADGuard DNS方案,手机电脑iOS去广告,保护隐私

 原文链接 之前分享过使用mac系统搭建adguard home,这几个月用下来零零散散基本上也被弃用了。主要原因是因为需要保持电脑一直开机。但是我的电脑是笔记本,存在移动各个地域的情况,也就是说只能够屏蔽电脑自身,对于手机而言不太现实。今天偶然发现dnspod推出了高级版的公共解析。dnspod背靠腾讯云,肯定是合法合规的公共解析服务,这个高级版用起来不错。 国内自己搭建解析服务是违法行为,所以这也是为什么使用dnspod的原因。 后台截图 开始使用 首先我们先进入dnspod的公共解析页面,点击开始使用。 专业版公共解析 dnspod会提供几种预设,我们选择「开发者」即可 开发者 然后你就成功的申请到自己个人使用的dns了! 更新拦截规则 我们可以将常见的广告过滤规则加入到dns中。我们在顶部选项卡中选择「拦截规则」。 拦截规则设置 打开adguard adguard 绑定iOS设备 推荐使用描述文件的方式,删除配置时删除描述文件即可。 描述文件 绑定macOS 推荐使用描述文件的方式,删除配置时删除描述文件即可。 描述文件 mac需要在「系统偏好设置」的「网络」中查看是否正在运行。 代理 如果没有运行需要点击「···」来启动服务。 启动服务 绑定路由器 找到自己路由器的DHCP设置,修改dns,然后记得绑定自己的ip。 修改dns 绑定ip 费用 目前有300万次/月的免费额度,但没有超出之后的价格。300万次一个人比较难用完,可以放心使用。 我个人使用iOS设备两台、智能家居、电脑两台,日均请求数大致2万/日。 判断是否搭建成功 可以通过查看日志的方式,日志大概有半小时到一小时的延迟,请耐心等待。