修復iOS 10不彈出是否容許xxx訪問數據致使app沒法聯網的bug

問題描述

iOS 10有一個系統bug:app在第一次安裝時,第一次聯網操做會彈出一個受權框,提示"是否容許xxx訪問數據?"。而有時候系統並不會彈出受權框,致使app沒法聯網。html

詳細狀況見:git

iOS 10 的坑:新機首次安裝 app,請求網絡權限「是否容許使用數據」github

iOS 10 不提示「是否容許應用訪問數據」,致使應用沒法使用的解決方案bash

關鍵點總結:markdown

  • 只有iOS 10以上、國行機型、有蜂窩網絡功能的設備存在這個受權問題,WiFi版的iPad沒有這個問題;
  • 因爲受權框是在有網絡操做時才彈出的,這就致使app第一次網絡訪問一定失敗;
  • 當出現不彈出受權框的bug時,去設置裏更改任意app的蜂窩網絡權限,或者打開無線局域網助理,讓系統更新一下蜂窩網絡相關的數據,能夠解決這個bug。

這個系統bug出現時,對用戶來講是很麻煩的,app也須要提供詳細的提示語來應對這種狀況,十分不優雅。網絡

修復方法

春節有點空,找到了幾個相關的私有API來修復這個bug。app

彈出受權框

首先找到的是一個能直接彈出受權框的API。iphone

//Image: /System/Library/PrivateFrameworks/FTServices.framework/FTServices

@interface FTNetworkSupport : NSObject
+ (id)sharedInstance;
- (bool)dataActiveAndReachable;
@end
複製代碼

頭文件參考:FTNetworkSupport.h異步

當app以前沒有請求過網絡權限時,調用dataActiveAndReachable會彈出"是否容許xxx訪問數據?"的受權框,若是網絡權限已經肯定,則不會彈出。ide

調用方式

因爲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。

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 不提示「是否容許應用訪問數據」,致使應用沒法使用的解決方案

相關文章
相關標籤/搜索