私有API-修复iOS 10不弹出"是否允许xxx访问数据"导致app无法联网的bug

目录

  • 问题描述
  • 修复方法
    • 弹出授权框
      • 调用方式
    • 让系统更新蜂窝网络权限数据
      • 调用方式
      • 出现了玄学
    • 用控制台跟踪进程间通信
  • 检查网络权限情况
  • 检测国行机型和是否有蜂窝功能
  • 测试修复是否成功的方法
  • 工具代码和Demo
  • 参考

问题描述

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对使用到的字符串还有要求?

时间有限,这个问题以后再研究吧。

<a name="debug-trace"></a>用控制台跟踪进程间通信

这几个私有API都用了进程间通信,要进行调试跟踪有点麻烦。

可以使用Mac上的控制台查看设备的实时log,寻找通信行为。打开控制台app,在左侧选择连接到Mac的iOS设备,就可以看到设备log了。

下面是调用了_CTServerConnectionSetCellularUsagePolicy之后的log,传入bundle id时用的是字面量创建的字符串:

使用字面量字符串传入bundle id

高亮的那行是测试demo打的log,可以认为就是在这里调用了_CTServerConnectionSetCellularUsagePolicy
可以看到,调用之后系统更新了本app的权限状态。CommCenter就是这几个私有API通信的对应进程,用于管理设备的网络。参考CommCenter - The iPhone Wiki

下面是用[NSBundle mainBundle].bundleIdentifier传入_CTServerConnectionSetCellularUsagePolicy的第二个参数时的log:

使用动态创建的字符串传入bundle id

没有看到系统更新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实例新建时,restrictedStatekCTCellularDataRestrictedStateUnknown,之后在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 identifierdisplay 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。

工具代码和Demo

地址在ZIKCellularAuthorization,用到的私有API已经经过混淆。测试前记得先把Build Configuration改为Release模式。有帮助请点个Star~

参考

iOS 10 的坑:新机首次安装 app,请求网络权限“是否允许使用数据”

iOS 10 不提示「是否允许应用访问数据」,导致应用无法使用的解决方案

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容