• Stars
    star
    509
  • Rank 86,772 (Top 2 %)
  • Language
    Objective-C
  • License
    MIT License
  • Created almost 8 years ago
  • Updated over 6 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

用于修复iOS 10首次安装app时,不会弹出"允许xxx使用数据?"授权框的bug

ZIKCellularAuthorization

用于修复iOS 10首次安装app时,不会弹出"允许xxx使用数据?"授权框的bug。

使用了私有API,请务必阅读App Store审核问题部分。

目录

问题描述

iOS 10有一个系统bug:app在第一次安装时,第一次联网操作会弹出一个授权框,提示"是否允许xxx访问数据?"。而有时候系统并不会弹出授权框,导致app无法联网。

详细情况见:

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

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

关键点总结:

  • 只有iOS 10以上、国行机型、有蜂窝网络功能的设备存在这个授权问题,WiFi版的iPad没有这个问题;
  • 由于授权框是在有网络操作时才弹出的,这就导致app第一次网络访问必定失败;
  • 当出现不弹出授权框的bug时,去设置里更改任意app的蜂窝网络权限,或者打开无线局域网助理,让系统更新一下蜂窝网络相关的数据,可以解决这个bug。

这个系统bug出现时,对用户来说是很麻烦的,app也需要提供详细的提示语来应对这种情况,十分不优雅。

(update:我在iPhone6s 10.3.2上测试,没有再重现此问题,猜测苹果至少在10.3.2系统以上修复了这个bug)。

App Store审核问题

由于使用了私有API,虽然已经经过字符串混淆,但是现在App Store审核时会在运行时检查dlopen、dlsym、NSClassFromString等动态方法的调用,因此用这些方式使用私有API时仍然会被检测出来。

解决方法:

1.让app在某个固定时间之后才执行修复,例如预估2018.01.01审核完毕,就在代码里检测日期,2018.01.01之后才执行修复。这个时间需要适当预估。

2.苹果审核团队好像都是在美国,可以判断系统语言,只有中文时才修复。

目前这些判断需要使用者自己完成。

update: 目前不建议在 App Store 正式版中使用,企业 app 中可以随意使用。现在苹果禁止在使用 dlopen(), dlsym(), respondsToSelector:, performSelector:, method_exchangeImplementations() 时传入动态生成的参数,参考:Are performSelector and respondsToSelector banned by App Store?。这通过静态分析是能够被检查出来的。苹果会检查引用了这些符号的那部分汇编代码,判断传入的参数是否是静态编译的。

虽然用我的 ZIKImageSymbol.h 可以动态获取 dlopen、dlsym、objc_msgSend 的函数指针,避免引入符号,从而绕过检查,不过我只是用来做一些 debug 工具,没在正式产品中使用过。有兴趣的朋友可以尝试。

修复方法

春节有点空,找到了几个相关的私有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时用的是字面量创建的字符串: 使用字面量字符串传入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 不提示「是否允许应用访问数据」,导致应用无法使用的解决方案