iOS 10有一个系统bug:app在第一次安装时,第一次联网操做会弹出一个受权框,提示"是否容许xxx访问数据?"。而有时候系统并不会弹出受权框,致使app没法联网。html
详细状况见:git
iOS 10 的坑:新机首次安装 app,请求网络权限“是否容许使用数据”github
iOS 10 不提示「是否容许应用访问数据」,致使应用没法使用的解决方案bash
关键点总结:网络
这个系统bug出现时,对用户来讲是很麻烦的,app也须要提供详细的提示语来应对这种状况,十分不优雅。app
春节有点空,找到了几个相关的私有API来修复这个bug。iphone
首先找到的是一个能直接弹出受权框的API。异步
//Image: /System/Library/PrivateFrameworks/FTServices.framework/FTServices
@interface FTNetworkSupport : NSObject
+ (id)sharedInstance;
- (bool)dataActiveAndReachable;
@end
复制代码
头文件参考:FTNetworkSupport.hide
当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时用的是字面量建立的字符串:
_CTServerConnectionSetCellularUsagePolicy
, 能够看到,调用以后系统更新了本app的权限状态。
CommCenter
就是这几个私有API通讯的对应进程,用于管理设备的网络。参考
CommCenter - The iPhone Wiki。
下面是用[NSBundle mainBundle].bundleIdentifier
传入_CTServerConnectionSetCellularUsagePolicy
的第二个参数时的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
里会有一次回调,此时才能获取到正确的权限状态。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。
因为使用了私有API,虽然已经通过混淆,但混淆只能绕过静态检查,而如今App Store审核时会检查dlopen、dlsym、NSClassFromString等动态方法的调用,所以用这些方式使用私有API时仍然会被检测出来。解决方法:
1.让app在某个固定时间以后才执行修复,例如预估2018.01.01审核完毕,就在代码里检测日期,2018.01.01以后才执行修复。这个时间须要适当预估。
2.苹果审核团队好像都是在美国,能够判断系统语言,只有中文时才修复。
目前这些判断须要使用者本身完成。
不过目前iOS10已是过去式了,这个问题彷佛已经不是特别严重。各位酌情考虑是否使用吧,这篇文章最大的做用仍是给出一个研究方式的参考。
地址在ZIKCellularAuthorization,用到的私有API已经通过混淆。测试前记得先把Build Configuration
改成Release
模式。有帮助请点个Star~