ZIKCellularAuthorization

by Zuikyo

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

459 Stars 47 Forks Last release: Not found MIT License 18 Commits 0 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

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 

//加载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); </dlfcn.h>

这个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 

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。

工具代码和Demo

地址在ZIKCellularAuthorization,用到的私有API已经经过混淆。测试前记得先把

Build Configuration
改为
Release
模式。有帮助请点个Star~

参考

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

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

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.