修复iOS 10不弹出是否允许xxx访问数据导致app无法联网的bug
问题描述
iOS 10有一个系统bug:app在第一次安装时,第一次联网操作会弹出一个授权框,提示"是否允许xxx访问数据?"。而有时候系统并不会弹出授权框,导致app无法联网。
详细情况见:
iOS 10 的坑:新机首次安装 app,请求网络权限“是否允许使用数据”
iOS 10 不提示「是否允许应用访问数据」,导致应用无法使用的解决方案
关键点总结:
- 只有iOS 10以上、国行机型、有蜂窝网络功能的设备存在这个授权问题,WiFi版的iPad没有这个问题;
- 由于授权框是在有网络操作时才弹出的,这就导致app第一次网络访问必定失败;
- 当出现不弹出授权框的bug时,去设置里更改任意app的蜂窝网络权限,或者打开无线局域网助理,让系统更新一下蜂窝网络相关的数据,可以解决这个bug。
这个系统bug出现时,对用户来说是很麻烦的,app也需要提供详细的提示语来应对这种情况,十分不优雅。
修复方法
春节有点空,找到了几个相关的私有API来修复这个bug。
弹出授权框
首先找到的是一个能直接弹出授权框的API。
//Image: /System/Library/PrivateFrameworks/FTServices.framework/FTServices@interface FTNetworkSupport : NSObject
+ (id)sharedInstance;
- (bool)dataActiveAndReachable;
@end
复制代码
头文件参考:FTNetworkSupport.h
当app之前没有请求过网络权限时,调用dataActiveAndReachable
会弹出"是否允许xxx访问数据?"的授权框,如果网络权限已经确定,则不会弹出。
调用方式
由于FTNetworkSupport
是在PrivateFrameworks
目录下,app并没有加载这个库,所以要使用里面的类前,需要用dlopen
加载FTServices.framework
,简单示意如下:
#import <dlfcn.h>//加载FTServices.framework
void * FTServicesHandle = dlopen("/System/Library/PrivateFrameworks/FTServices.framework/FTServices", RTLD_LAZY);
Class NetworkSupport = NSClassFromString(@"FTNetworkSupport");
id networkSupport = [NetworkSupport performSelector:NSSelectorFromString(@"sharedInstance")];
[networkSupport performSelector:NSSelectorFromString(@"dataActiveAndReachable")];
//卸载FTServices.framework
dlclose(FTServicesHandle);
复制代码
这个API能解决网络权限导致第一个联网操作失败的问题,但是它还是存在有时候不会弹出授权框的bug。
让系统更新蜂窝网络权限数据
既然更改任意app的蜂窝网络权限后,能让app弹出授权框,那么只要找到一个方法,能让系统更新一下网络权限相关的数据就可以了。
用hopper
反编译一下系统的设置app用到的库PreferencesUI.framework
,找到了里面修改app网络权限的API。用到的是CoreTelephony.framework
里的两个私有C函数:
CTServerConnection* _CTServerConnectionCreateOnTargetQueue(CFAllocatorRef, NSString *, dispatch_queue_t, void*/*一个block类型的参数*/)
void _CTServerConnectionSetCellularUsagePolicy(CTServerConnection *, NSString *, NSDictionary *)
大部分时间都花在测试这两个函数上了。几个月前我也研究过这两个函数尝试修复这个bug,但是那时候发现没什么作用,就不了了之了。
调用方式
要调用私有C函数,需要用dlsym
,简单示意如下:
void *CoreTelephonyHandle = dlopen("/System/Library/Frameworks/CoreTelephony.framework/CoreTelephony", RTLD_LAZY);//用函数指针来调用私有C函数,用符号名从库里寻找函数地址
CFTypeRef (*connectionCreateOnTargetQueue)(CFAllocatorRef, NSString *, dispatch_queue_t, void*) = dlsym(CoreTelephonyHandle, "_CTServerConnectionCreateOnTargetQueue");
int (*changeCellularPolicy)(CFTypeRef, NSString *, NSDictionary *) = dlsym(CoreTelephonyHandle, "_CTServerConnectionSetCellularUsagePolicy");//使用设置app的bundle id进行伪装
CFTypeRef connection = connectionCreateOnTargetQueue(kCFAllocatorDefault,@"com.apple.Preferences",dispatch_get_main_queue(),NULL);
//请求修改本app的网络权限为allowed,不会真的修改,只能触发系统更新一下相关的数据
changeCellularPolicy(connection, @"需要授权的app的bundle id", @{@"kCTCellularUsagePolicyDataAllowed":@YES});dlclose(CoreTelephonyHandle);
复制代码
注意,在声明connectionCreateOnTargetQueue和changeCellularPolicy函数指针时,参数类型要严格对应,如果类型错误,可能会导致系统对参数执行错误的内存管理,出现crash。CTServerConnection
是私有的,是CFTypeRef
的子类,所以这里可以用CFTypeRef
来代替。
出现了玄学
_CTServerConnectionSetCellularUsagePolicy
函数的第二个参数是需要修改的app的bundle id。在测试时,发现传入这个参数时,对象必须是用字面量语法创建的NSString
,例如@"com.who.testDemo"
,当传入[NSBundle mainBundle].bundleIdentifier
这种动态生成的NSString
时,仍然会出现不弹出授权框的bug,也就是并没有修复成功。连续测试5-10次就能重现。
不过,用
NSMutableString *bundleIdentifier = [NSMutableString stringWithString:@"com.who"];
[bundleIdentifier appendString:@".testDemo"];
复制代码
这样的字符串也没问题。相同点是最终都是来自字面量语法创建的NSString
。
这个玄学问题目前还没有找到原因。
研究了一下字面量创建出的NSString
,的确是有些特殊的。参考:Constant Strings in Objective-C。它是一个__NSCFConstantString
类型的字符串,在app的整个生命周期内,这个对象的内存都不会被释放。难道iOS的XPC对使用到的字符串还有要求?
时间有限,这个问题以后再研究吧。
用控制台跟踪进程间通信
这几个私有API都用了进程间通信,要进行调试跟踪有点麻烦。
可以使用Mac上的控制台查看设备的实时log,寻找通信行为。打开控制台app,在左侧选择连接到Mac的iOS设备,就可以看到设备log了。
下面是调用了_CTServerConnectionSetCellularUsagePolicy
之后的log,传入bundle id时用的是字面量创建的字符串:
高亮的那行是测试demo打的log,可以认为就是在这里调用了 _CTServerConnectionSetCellularUsagePolicy
, 可以看到,调用之后系统更新了本app的权限状态。 CommCenter
就是这几个私有API通信的对应进程,用于管理设备的网络。参考 CommCenter - The iPhone Wiki。
下面是用[NSBundle mainBundle].bundleIdentifier
传入_CTServerConnectionSetCellularUsagePolicy
的第二个参数时的log:
没有看到系统更新app权限的相关log,进程间通信可能失败了。因此可以确定,使用 _CTServerConnectionSetCellularUsagePolicy
时必须传入字面量语法创建的字符串。
检查网络权限情况
由于dataActiveAndReachable
里面有异步操作,所以不能立即用dlclose
卸载FTServices.framework
。解决方法是监听到蜂窝权限开启时再卸载。
CoreTelephony
里的CTCellularData
可以用来监测app的蜂窝网络权限,并且这不是个私有API。你也可以用它来帮助用户检测蜂窝权限是否被关闭,并给出提示,防止出现用户关了网络权限导致app无法联网的情况。
CTCellularData
的头文件如下:
typedef NS_ENUM(NSUInteger, CTCellularDataRestrictedState) {kCTCellularDataRestrictedStateUnknown,//权限未知kCTCellularDataRestricted,//蜂窝权限被关闭,有 网络权限完全关闭 or 只有WiFi权限 两种情况kCTCellularDataNotRestricted//蜂窝权限开启
};@interface CTCellularData : NSObject
///权限更改时的回调
@property (copy, nullable) CellularDataRestrictionDidUpdateNotifier cellularDataRestrictionDidUpdateNotifier;
///当前的蜂窝权限
@property (nonatomic, readonly) CTCellularDataRestrictedState restrictedState;
@end
复制代码
使用方法:
#import <CoreTelephony/CTCellularData.h>CTCellularData *cellularDataHandle = [[CTCellularData alloc] init];
cellularDataHandle.cellularDataRestrictionDidUpdateNotifier = ^(CTCellularDataRestrictedState state) {//蜂窝权限更改时的回调};
复制代码
使用时需要注意的关键点:
CTCellularData
只能检测蜂窝权限,不能检测WiFi权限。- 一个
CTCellularData
实例新建时,restrictedState
是kCTCellularDataRestrictedStateUnknown
,之后在cellularDataRestrictionDidUpdateNotifier
里会有一次回调,此时才能获取到正确的权限状态。 - 当用户在设置里更改了app的权限时,
cellularDataRestrictionDidUpdateNotifier
会收到回调,如果要停止监听,必须将cellularDataRestrictionDidUpdateNotifier
设置为nil
。 - 赋值给
cellularDataRestrictionDidUpdateNotifier
的block并不会自动释放,即便你给一个局部变量的CTCellularData
实例设置监听,当权限更改时,还是会收到回调,所以记得将block置nil
。
检测国行机型和是否有蜂窝功能
非国行机型,以及没有蜂窝功能的设备是不需要进行修复的。因此也要寻找相关的私有API进行检测。
用到的私有API如下:
//Image: /System/Library/PrivateFrameworks/AppleAccount.framework/AppleAccount@interface AADeviceInfo : NSObject
///是否有蜂窝功能
- (bool)hasCellularCapability;
///设备的区域代码,例如国行机就是CH
- (id)regionCode;
@end
复制代码
头文件参考:AADeviceInfo.h
使用方式和FTServices.framework
类似,不再重复。
测试修复是否成功的方法
我的测试方式是每次运行都修改项目的bundle identifier
和display name
,让系统每次都把它当做一个新app,使用Release
模式,测试是否每次都能够弹出授权框。由于需要不断修改bundle identifier
,写了个脚本在每次build时自动运行,会自动累加几个地方的bundle identifier
后面的数字。demo里已经附带了这个脚本。
你也可以测试一下不执行修复时,进行联网操作是否会弹出授权框。我的测试结果是大约运行5-10次时,就会出现不弹出授权框的bug。需要把项目改为Release
模式才能出现,Debug
模式下不会出bug。
注意,由于build后自动累加的关系,ZIKCellularAuthorization.h
里的AppBundleIdentifier
是下一次app运行时的值。如果你觉得这个脚本把你搞晕了,可以在Build Phases/Run Script
里关掉,在sh ${PROJECT_DIR}/IncreaseBundleId.sh
前面加个#
注释掉就行了。
没有测试覆盖安装同一个bundle identifier
的app,或者更新了版本号的app是否也会出现这个bug,现在是认为只有第一次安装时才会出现bug。
App Store审核问题
由于使用了私有API,虽然已经经过混淆,但混淆只能绕过静态检查,而现在App Store审核时会检查dlopen、dlsym、NSClassFromString等动态方法的调用,因此用这些方式使用私有API时仍然会被检测出来。解决方法:
1.让app在某个固定时间之后才执行修复,例如预估2018.01.01审核完毕,就在代码里检测日期,2018.01.01之后才执行修复。这个时间需要适当预估。
2.苹果审核团队好像都是在美国,可以判断系统语言,只有中文时才修复。
目前这些判断需要使用者自己完成。
不过目前iOS10已经是过去式了,这个问题似乎已经不是特别严重。各位酌情考虑是否使用吧,这篇文章最大的作用还是给出一个研究方式的参考。
工具代码和Demo
地址在ZIKCellularAuthorization,用到的私有API已经经过混淆。测试前记得先把Build Configuration
改为Release
模式。有帮助请点个Star~
参考
iOS 10 的坑:新机首次安装 app,请求网络权限“是否允许使用数据”
iOS 10 不提示「是否允许应用访问数据」,导致应用无法使用的解决方案
修复iOS 10不弹出是否允许xxx访问数据导致app无法联网的bug相关推荐
- iOS 10 不提示「是否允许应用访问数据」,导致应用无法使用的解决方案
每日更新关注:http://weibo.com/hanjunqiang 新浪微博!iOS开发者交流QQ群: 446310206 这个坑最近弄得我很抓狂,不过现在基本弄清楚了.记录一下过程中我收集到的 ...
- IOS项目之弹出动画二
在IOS项目之弹出动画一中只是实现也功能,并没有体现面向对象的思想 ,今天就试着把它封装了一下,弹出视图的内容可以根据自定义,此处只是用UIDatePicker来演示 我把它传到了GitHub上 ...
- joycdr2html.exe系统错误,win7安装软件CDR2020 弹出错误1719无法访问Windws Installer服务解决方案...
安装软件CDR2020 弹出错误1719无法访问Windws Installer服务解决方案 安装软件CDR2020 弹出错误1719无法访问Windws Installer服务解决方案 1.普通删除 ...
- python弹窗怎么抓取_在Python中,按POST请求并从弹出窗口中抓取数据
我想按"Suche starten"按钮,从这个页面上为一个研究项目刮取结果(基本上它可以不填任何表格-然后打开一个弹出窗口,保存我想要的数据).在https://www.inso ...
- 简单利用Dialog实现Ios从底部弹出的效果,合QQ空间里面的发表说说弹出拍照的效果类似
前面项目赶得紧,有个从底部往上弹出的效果,我想都没想就用PopWindow去实现了,但是实现后发现效果不太理想.没办法了,只能用Dialog来做的.现在主要就是实现下面这个效果: 在实现这个效果前,我 ...
- uniapp 解决ios软键盘弹出input框被遮住或光标不在input框内
因为苹果和安卓底层的开发框架不一样 所以软键盘弹出时 苹果系统会出现这些问题 uniapp官方也没有给出明确的解决方案 这个写法是自己谷歌 社区查找并加以实验 得出的解决方案 <view cla ...
- u盘盘符不显示 win10_学会这三招,在win10上安全弹出U盘,保证数据不损坏
为了避免数据丢失,用户必须掌握如何在使用完计算机后安全地从计算机上卸下外部硬盘驱动器或U盘,否则,如果在断开U盘之前未弹出它,则可能会遇到数据损坏的问题,所以不要偷懒.那么,如何在计算机上弹出外部硬盘 ...
- layui 弹出层里面添加表格数据
好文章需要耐心阅读-茶凡-Matrix 这里点击添加按钮会弹出一个弹出层,里面可以添加数据并提交. type - 基本层类型,类型:Number,默认:0 layer官方提供了5种层类型.可传入的值有 ...
- Android仿IOS Dialog底部弹出月历式时间选择器
最近项目需求要写一个类IOS并且是月历样式的时间选择器,如图: 少废话,成品如下: 1. 思路 界面部分 <LinearLayout<LinearLayout/><Linear ...
最新文章
- 分布式一致性协议Raft原理与实例
- 华为nova好不好 先看图
- java 正则表达式的使用
- 多示例学习 MIL(multiple instance learning) 理解
- 链表分割——牛客剑指offer
- python实现自动向QQ邮箱发送天气预报邮件
- 诺基亚n8系统更新服务器模式,新年新版本 诺基亚N8最新固件PR1.1亮相
- PowerDesigner如何自定义报表模板
- 马云有自己的银行,为什么还要贷款?
- python中星号怎么打出来_Python中的星号符号
- 区块链 数据交易专利
- 抑制剂拮抗剂等小分子化合物
- 无他 唯手熟尔 this指针
- 【内网渗透】域横向PTHPTKPTT哈希票据传递
- 销售宝:让客户无法拒绝的销售话术
- 三星老java手机换字体_三星手机设置字体大小与更换默认字体的图文教程
- 通讯方式:近场通讯和无线通讯
- Excel给证件照换底色,红、蓝、白底任你换,看完涨知识了
- “秤”心不如务“时”,Libra合规路暗藏玄机
- Centos离线环境安装pip
热门文章
- java取得输入的运算符_java第三天(运算符、键盘录入和控制语句)
- python RGB图像处理
- Python set函数:创建集合
- 企业招聘会直播前要做什么?
- 26字母起源二十六个英语字母的起源与含义
- 按键screenshot
- Mysql中tinyint与Java数据类型对应关系
- 电子计算机模拟器,TI-89计算器模拟器(TI-89 Emulator)
- Python3网络爬虫开发实战,Scrapy 爬取新浪微博
- 关于PHP+Mysql1045 Access denied for user错误解决办法——你可能想不到的