修復iOS 10不彈出是否允許xxx訪問資料導致app無法聯網的bug

黑超熊貓zuik發表於2018-03-25

問題描述

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對使用到的字串還有要求?

時間有限,這個問題以後再研究吧。

用控制檯跟蹤程式間通訊

這幾個私有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 不提示「是否允許應用訪問資料」,導致應用無法使用的解決方案

相關文章