問題描述
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時用的是字面量建立的字串:
_CTServerConnectionSetCellularUsagePolicy
,
可以看到,呼叫之後系統更新了本app的許可權狀態。CommCenter
就是這幾個私有API通訊的對應程式,用於管理裝置的網路。參考CommCenter - The iPhone Wiki。
下面是用[NSBundle mainBundle].bundleIdentifier
傳入_CTServerConnectionSetCellularUsagePolicy
的第二個引數時的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
例項新建時,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。
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~