之前的项目兼容的版本在iOS9.0以上,所以在项目中使用了CNContacts框架获取用户的通讯录信息,大致的实现如下:
/// 判断字符串是否可用
/// 辅助函数
/// @param str 原始对象
BOOL isAvailableString(NSString *str) {
if ([str isKindOfClass:[NSNull class]]) {
return false;
}
if (!str || !str.length) {
return false;
}
return true;
}
/// 查看通讯录访问权限
void getAuthrization(void(^_Nonnull result)(BOOL granted)) {
CNAuthorizationStatus authorizationStatus = [CNContactStore authorizationStatusForEntityType:(CNEntityTypeContacts)];
if (authorizationStatus == CNAuthorizationStatusNotDetermined) {
CNContactStore *contactStore = [[CNContactStore alloc] init];
[contactStore requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (result) {
result(granted);
}
}];
} else if (authorizationStatus == CNAuthorizationStatusAuthorized){
if (result) {
result(true);
}
} else {
if (result) {
result(true);
}
}
}
/// 获取通讯录信息
/// @param result 结果回调
- (void)getAllContacts:(void(^)(NSArray *))result {
getAuthrization(^(BOOL granted){
if (granted) {
CNContactStore *contactStore = [[CNContactStore alloc] init];
CNContactFetchRequest *fetchRequest = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[CNContactFamilyNameKey, CNContactGivenNameKey, CNContactMiddleNameKey, CNContactNoteKey]];
NSError *error = nil;
__block NSMutableArray *> *allContacts = @[].mutableCopy;
[contactStore enumerateContactsWithFetchRequest:fetchRequest error:&error usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
NSString *familyName = contact.familyName;
familyName = isAvailableString(familyName) ? familyName : @"";
NSString *middleName = contact.middleName;
middleName = isAvailableString(middleName) ? middleName : @"";
NSString *givenName = contact.givenName;
givenName = isAvailableString(givenName) ? givenName : @"";
NSString *note = contact.note;
note = isAvailableString(note) ? note : @"";
NSString *fullName = [NSString stringWithFormat:@"%@%@%@", familyName, middleName, givenName];
if (isAvailableString(fullName)) {
[allContacts addObject:@{@"fullName" : fullName, @"note" : note}];
}
}];
if (result) {
result(allContacts);
}
}
});
}
一直相安无事,正常运行.直到某一天早上突然发现,预警系统相当一部分用户的所有的联系人信息一夜之间全部为空,一脸懵逼.
其实在早前的发布大会上,已经说明了要对要用户通讯录隐私数据的使用权限做限制,尤其是"备注"信息,可能会涉及到用户的个人私密数据,例如银行卡密码,亲密人物关系等,所以在iOS13.x上使用用户通讯录数据时就需要注意.
在iOS 13.x 上获取用户的通讯录数据时,不能获取 "备注" 信息,否则会系统会抛出异常,从而导致获取信息失败.在获取的方法中,修改一下实现来查看一下是否有异常发生:
/// 获取通讯录信息
/// @param result 结果回调
- (void)getAllContacts:(void(^)(NSArray *))result {
getAuthrization(^(BOOL granted){
if (granted) {
CNContactStore *contactStore = [[CNContactStore alloc] init];
CNContactFetchRequest *fetchRequest = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[CNContactFamilyNameKey, CNContactGivenNameKey, CNContactMiddleNameKey, CNContactNoteKey]];
NSError *error = nil;
__block NSMutableArray *> *allContacts = @[].mutableCopy;
BOOL fetchSuccess = [contactStore enumerateContactsWithFetchRequest:fetchRequest error:&error usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
NSString *familyName = contact.familyName;
familyName = isAvailableString(familyName) ? familyName : @"";
NSString *middleName = contact.middleName;
middleName = isAvailableString(middleName) ? middleName : @"";
NSString *givenName = contact.givenName;
givenName = isAvailableString(givenName) ? givenName : @"";
NSString *note = contact.note;
note = isAvailableString(note) ? note : @"";
NSString *fullName = [NSString stringWithFormat:@"%@%@%@", familyName, middleName, givenName];
if (isAvailableString(fullName)) {
[allContacts addObject:@{@"fullName" : fullName, @"note" : note}];
}
}];
if (!fetchSuccess || error) {
NSLog(@"fetchSuccess : %@", fetchSuccess ? @"成功" : @"失败");
NSLog(@"error : %@", error);
}
if (result) {
result(allContacts);
}
}
});
}
再次运行,输出信息果然是异常的:
fetchSuccess : 失败
error : Error Domain=CNErrorDomain Code=102 "(null)" UserInfo={CNKeyPaths=(
note
), CNInvalidRecords=(
""
)}
然后在CNErrorDomain定义中找到了关于Code=102的说明:
CONTACTS_EXTERN NSString * const CNErrorDomain NS_AVAILABLE(10_11, 9_0);
typedef NS_ENUM(NSInteger, CNErrorCode)
{
CNErrorCodeCommunicationError = 1,
CNErrorCodeDataAccessError = 2,
CNErrorCodeAuthorizationDenied = 100,
CNErrorCodeNoAccessableWritableContainers NS_ENUM_AVAILABLE(10_13_3, 11_3) = 101,
CNErrorCodeUnauthorizedKeys API_AVAILABLE(macos(10.14), ios(13.0), watchos(6.0)) = 102,
CNErrorCodeFeatureDisabledByUser API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0)) = 103,
CNErrorCodeRecordDoesNotExist = 200,
CNErrorCodeInsertedRecordAlreadyExists = 201,
CNErrorCodeContainmentCycle = 202,
CNErrorCodeContainmentScope = 203,
CNErrorCodeParentRecordDoesNotExist = 204,
CNErrorCodeRecordIdentifierInvalid = 205,
CNErrorCodeRecordNotWritable API_AVAILABLE(macos(10.14), ios(13.0), watchos(6.0)) = 206,
CNErrorCodeParentContainerNotWritable API_AVAILABLE(macos(10.14), ios(13.0), watchos(6.0)) = 207,
CNErrorCodeValidationMultipleErrors = 300,
CNErrorCodeValidationTypeMismatch = 301,
CNErrorCodeValidationConfigurationError = 302,
CNErrorCodePredicateInvalid = 400,
CNErrorCodePolicyViolation = 500,
CNErrorCodeClientIdentifierInvalid = 600,
CNErrorCodeClientIdentifierDoesNotExist = 601,
CNErrorCodeClientIdentifierCollision = 602,
CNErrorCodeChangeHistoryExpired = 603,
CNErrorCodeChangeHistoryInvalidAnchor = 604,
CNErrorCodeVCardMalformed NS_ENUM_AVAILABLE(10_13, 11_0) = 700,
CNErrorCodeVCardSummarizationError NS_ENUM_AVAILABLE(10_14, 12_0) = 701,
} NS_ENUM_AVAILABLE(10_11, 9_0);
发现CNErrorCodeUnauthorizedKeys = 102是获取了未授权的属性key.由此真相大白,苹果爸爸不再授权获取note属性了.那么我是不是只要在CNContactFetchRequest初始化时,不加入CNContactNoteKey就可以了,大不了在访问时获取不到数据就完事了?
CNContactStore *contactStore = [[CNContactStore alloc] init];
CNContactFetchRequest *fetchRequest = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[CNContactFamilyNameKey, CNContactGivenNameKey, CNContactMiddleNameKey, /*CNContactNoteKey*/]];
再次运行,结果
Ignored Exception: A property was not requested when contact was fetched.
(
0 CoreFoundation 0x00000001a2f49c44 97285ACB-7B21-393A-ABF6-03F1DBB5D2A2 + 1256516
1 libobjc.A.dylib 0x00000001a2c640c8 objc_exception_throw + 60
2 CoreFoundation 0x00000001a2e393ac 97285ACB-7B21-393A-ABF6-03F1DBB5D2A2 + 140204
3 Contacts 0x00000001aa321d18 75694259-6C2D-34A1-828B-E0A506C23F35 + 818456
4 ContactsDemo 0x0000000100a91528 __33-[ViewController getAllContacts:]_block_invoke.83 + 412
5 Contacts 0x00000001aa2c0434 75694259-6C2D-34A1-828B-E0A506C23F35 + 418868
6 Contacts 0x00000001aa2f5a08 75694259-6C2D-34A1-828B-E0A506C23F35 + 637448
7 CoreFoundation 0x00000001a2f1b7c4 97285ACB-7B21-393A-ABF6-03F1DBB5D2A2 + 1066948
8 CoreFoundation 0x00000001a2e19424 97285ACB-7B21-393A-ABF6-03F1DBB5D2A2 + 9252
9 Contacts 0x00000001aa2f5914 75694259-6C2D-34A1-828B-E0A506C23F35 + 637204
10 ContactsFoundation 0x00000001ac8ac78c CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 178060
11 ContactsFoundation 0x00000001ac8e4b24 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 408356
12 ContactsFoundation 0x00000001ac8e4b24 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 408356
13 ContactsFoundation 0x00000001ac8e4b24 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 408356
14 ContactsFoundation 0x00000001ac8e4b24 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 408356
15 ContactsFoundation 0x00000001ac8e4b24 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 408356
16 Contacts 0x00000001aa2881f8 75694259-6C2D-34A1-828B-E0A506C23F35 + 188920
17 Contacts 0x00000001aa2888b8 75694259-6C2D-34A1-828B-E0A506C23F35 + 190648
18 Contacts 0x00000001aa289024 75694259-6C2D-34A1-828B-E0A506C23F35 + 192548
19 CoreFoundation 0x00000001a2f50724 97285ACB-7B21-393A-ABF6-03F1DBB5D2A2 + 1283876
20 CoreFoundation 0x00000001a2e19280 97285ACB-7B21-393A-ABF6-03F1DBB5D2A2 + 8832
21 Foundation 0x00000001a34489cc 672CF0CB-4951-3B91-89DF-55E953AEA00F + 2410956
22 Foundation 0x00000001a3211088 672CF0CB-4951-3B91-89DF-55E953AEA00F + 86152
23 Foundation 0x00000001a3449d04 672CF0CB-4951-3B91-89DF-55E953AEA00F + 2415876
24 Foundation 0x00000001a32102d4 672CF0CB-4951-3B91-89DF-55E953AEA00F + 82644
25 Foundation 0x00000001a321e0ac 672CF0CB-4951-3B91-89DF-55E953AEA00F + 139436
26 Foundation 0x00000001a3450f44 672CF0CB-4951-3B91-89DF-55E953AEA00F + 2445124
27 Contacts 0x00000001aa288c78 75694259-6C2D-34A1-828B-E0A506C23F35 + 191608
28 Contacts 0x00000001aa288738 75694259-6C2D-34A1-828B-E0A506C23F35 + 190264
29 Contacts 0x00000001aa288060 75694259-6C2D-34A1-828B-E0A506C23F35 + 188512
30 ContactsFoundation 0x00000001ac8c6780 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 284544
31 ContactsFoundation 0x00000001ac8abb08 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 174856
32 ContactsFoundation 0x00000001ac8c6780 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 284544
33 ContactsFoundation 0x00000001ac8ab800 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 174080
34 ContactsFoundation 0x00000001ac8c6780 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 284544
35 ContactsFoundation 0x00000001ac8ab800 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 174080
36 ContactsFoundation 0x00000001ac8c6780 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 284544
37 ContactsFoundation 0x00000001ac8abb08 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 174856
38 ContactsFoundation 0x00000001ac8c6780 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 284544
39 ContactsFoundation 0x00000001ac8ab45c CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 173148
40 ContactsFoundation 0x00000001ac8c6780 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 284544
41 ContactsFoundation 0x00000001ac8ac638 CAD1A4FE-E7C1-3980-8391-9E6E7CA88370 + 177720
42 Contacts 0x00000001aa2f55d4 75694259-6C2D-34A1-828B-E0A506C23F35 + 636372
43 Contacts 0x00000001aa3584d0 75694259-6C2D-34A1-828B-E0A506C23F35 + 1041616
44 libsystem_trace.dylib 0x00000001a2b27d30 os_activity_apply_f + 88
45 Contacts 0x00000001aa358438 75694259-6C2D-34A1-828B-E0A506C23F35 + 1041464
46 Contacts 0x00000001aa2f52a4 75694259-6C2D-34A1-828B-E0A506C23F35 + 635556
47 Contacts 0x00000001aa2c0320 75694259-6C2D-34A1-828B-E0A506C23F35 + 418592
48 ContactsDemo 0x0000000100a91180 __33-[ViewController getAllContacts:]_block_invoke + 480
49 ContactsDemo 0x0000000100a90dbc getAuthrization + 340
50 ContactsDemo 0x0000000100a90f7c -[ViewController getAllContacts:] + 120
51 ContactsDemo 0x0000000100a917f0 -[ViewController tableView:didSelectRowAtIndexPath:] + 104
52 UIKitCore 0x00000001a71b04c0 55D60569-DBB7-3BC9-8A97-DCAC95C64D4B + 12588224
53 UIKitCore 0x00000001a71afff8 55D60569-DBB7-3BC9-8A97-DCAC95C64D4B + 12587000
54 UIKitCore 0x00000001a71b06e0 55D60569-DBB7-3BC9-8A97-DCAC95C64D4B + 12588768
55 UIKitCore 0x00000001a6fe9078 55D60569-DBB7-3BC9-8A97-DCAC95C64D4B + 10723448
56 UIKitCore 0x00000001a6fd876c 55D60569-DBB7-3BC9-8A97-DCAC95C64D4B + 10655596
57 UIKitCore 0x00000001a70099a0 55D60569-DBB7-3BC9-8A97-DCAC95C64D4B + 10856864
58 CoreFoundation 0x00000001a2ec467c 97285ACB-7B21-393A-ABF6-03F1DBB5D2A2 + 710268
59 CoreFoundation 0x00000001a2ebf31c 97285ACB-7B21-393A-ABF6-03F1DBB5D2A2 + 688924
60 CoreFoundation 0x00000001a2ebf8cc 97285ACB-7B21-393A-ABF6-03F1DBB5D2A2 + 690380
61 CoreFoundation 0x00000001a2ebf098 CFRunLoopRunSpecific + 480
62 GraphicsServices 0x00000001ad029534 GSEventRunModal + 108
63 UIKitCore 0x00000001a6fdf7ac UIApplicationMain + 1940
64 ContactsDemo 0x0000000100a92cb8 main + 124
65 libdyld.dylib 0x00000001a2d3ef30 0DC9A4BA-C3E8-3487-99DB-1B5C86597AF5 + 3888
)
尴尬....就连之前设定的日志
if (!fetchSuccess || error) {
NSLog(@"fetchSuccess : %@", fetchSuccess ? @"成功" : @"失败");
NSLog(@"error : %@", error);
}
都没有打印,但是通过断点,那就说明fetchSuccess返回了成功,同时error为空,这种隐藏的异常是最难发现的.通过设置断点发现获取的遍历方法还是执行了的.为啥会这样呢,我们使用异常捕获来查看一下:
@try {
NSString *familyName = contact.familyName;
familyName = isAvailableString(familyName) ? familyName : @"";
NSString *middleName = contact.middleName;
middleName = isAvailableString(middleName) ? middleName : @"";
NSString *givenName = contact.givenName;
givenName = isAvailableString(givenName) ? givenName : @"";
NSString *note = contact.note;
note = isAvailableString(note) ? note : @"";
NSString *fullName = [NSString stringWithFormat:@"%@%@%@", familyName, middleName, givenName];
if (isAvailableString(fullName)) {
[allContacts addObject:@{@"fullName" : fullName, @"note" : note}];
}
} @catch (NSException *exception) {
NSLog(@"exception == %@", exception);
} @finally {
}
然后发现:
exception == A property was not requested when contact was fetched.
exception == A property was not requested when contact was fetched.
...
所以,既然苹果粑粑不让用,在正常情况下就不要试图去申请,也不要视图去获取,因为你粑粑终究是你粑粑,如果非要不听话,那就要准备好背锅.然后删掉
contact.note
调用,代码正常运行.
最后我们可以在此处添加异常检测代码,以防止以后此处再次发生改动时可以及时找到原因.
- (void)getAllContacts:(void(^)(NSArray *))result {
getAuthrization(^(BOOL granted){
if (granted) {
CNContactStore *contactStore = [[CNContactStore alloc] init];
CNContactFetchRequest *fetchRequest = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[CNContactFamilyNameKey, CNContactGivenNameKey, CNContactMiddleNameKey, /*CNContactNoteKey*/]];
NSError *error = nil;
__block NSMutableArray *> *allContacts = @[].mutableCopy;
BOOL fetchSuccess = [contactStore enumerateContactsWithFetchRequest:fetchRequest error:&error usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
@try {
NSString *familyName = contact.familyName;
familyName = isAvailableString(familyName) ? familyName : @"";
NSString *middleName = contact.middleName;
middleName = isAvailableString(middleName) ? middleName : @"";
NSString *givenName = contact.givenName;
givenName = isAvailableString(givenName) ? givenName : @"";
NSString *note = /*contact.note*/@"";
note = isAvailableString(note) ? note : @"";
NSString *fullName = [NSString stringWithFormat:@"%@%@%@", familyName, middleName, givenName];
if (isAvailableString(fullName)) {
[allContacts addObject:@{@"fullName" : fullName, @"note" : note}];
}
} @catch (NSException *exception) {
NSLog(@"exception == %@", exception);
//添加异常检测
[WEDExceptionManager sendExcetion:@"ContactEnumerateException" description:exception.description];
} @finally {
}
}];
if (!fetchSuccess || error) {
//添加异常检测
[WEDExceptionManager sendExcetion:@"ContactEnumerateException" description:error ? error.description : @"" ];
}
if (result) {
result(allContacts);
}
}
});
}