戳我
戳我
文章目录
  1. 通讯录数据的读取和性能优化
    1. 通讯录的授权
      1. iOS8查询通讯录授权状态
      2. iOS9查询通讯录授权状态
    2. 通讯录数据写入和删除
      1. iOS8写入联系人
      2. iOS9写入联系人
      3. iOS8删除联系人
      4. iOS9删除联系人
    3. 通讯录数据读取
      1. iOS8读取联系人
      2. iOS9读取联系人
      3. 通讯录分组
      4. 性能优化
        1. 方案一
        2. 方案二
    4. 监听通讯录数据变化
      1. ios8监听通讯录变化
      2. ios9监听通讯录变化

通讯录数据的读取和性能优化

通讯录数据的读取和性能优化

前段时间再做一个关于通讯录相关的项目, 记录一下通讯录相关的基础和读取优化的部分. 在本篇文章中, 我主要侧重读取通讯录数据方面, 至于调用原生的界面在这里则不做阐述. 关于通讯录的API, 在iOS9之后做了较大的调整, 使用Contacts Framework替代AddressBookFramework. 下面我将就iOS9和iOS8两种API进行分析.

通讯录的授权

既然要读取通讯录, 那么通讯录授权状态的查询, 要放在在前面, 这里也区分iOS9和iOS8系统.

iOS8查询通讯录授权状态

+ (void)checkAddressBookAuthorization:(void (^)(bool isAuthorized))block {
    ABAddressBookRef addressBookRef =  ABAddressBookCreateWithOptions(NULL, NULL);
    switch (ABAddressBookGetAuthorizationStatus()) {
        case kABAuthorizationStatusNotDetermined: {
            NSLog(@"未询问用户是否授权");
            ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) {
                if (granted) {
                    NSLog(@"授权可以读取");
                    block(YES);
                } else {
                    NSLog(@"授权不能读取");
                    block(NO);
                }
            });
        } break;
        case kABAuthorizationStatusAuthorized: {
            NSLog(@"同意授权通讯录");
            block(YES);
        } break;
        case kABAuthorizationStatusDenied: {
            block(NO);
            NSLog(@"未授权,用户拒绝造成的");
        } break;
        case kABAuthorizationStatusRestricted: {
            block(NO);
            NSLog(@"未授权,例如家长控制");
        } break;
        default: {
        } break;
    }
}

iOS9查询通讯录授权状态

- (void)addressBookEmpowerCheck {
    CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
    switch (status) {
        case CNAuthorizationStatusNotDetermined: {
            [[[CNContactStore alloc]init] requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
                NSLog(@"还没问");
                if (granted) {
                    NSLog(@"点击了同意");
                } else {
                    NSLog(@"点击了拒绝");
                }
            }];
        }
        break;
        case CNAuthorizationStatusRestricted: {
            NSLog(@"未授权, 例如家长控制");
        }
        break;
        case CNAuthorizationStatusDenied: {
            NSLog(@"未授权, 用户拒绝所致");
        }
        break;
        case CNAuthorizationStatusAuthorized: {
            NSLog(@"已经授权");
        }
        break;
        default: {
        }
        break;
    }
}

注意: 如果是iOS10的系统需要在Info.plist配置NSContactsUsageDescription

<key>NSContactsUsageDescription</key>
<string>请求访问通讯录</string>

通讯录数据写入和删除

当通讯录数据少时, 数据的读取耗时较少, 为了更好地测试大量数据下的性能问题, 我们先写入大量的数据来进行测试.

iOS8写入联系人

- (void)creatItemWithName:(NSString *)name phone:(NSString *)phone {
    if((name.length < 1)||(phone.length < 1)){
        NSLog(@"输入属性不能为空");
        return;
    }
    CFErrorRef error = NULL;

    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, &error);
    ABRecordRef newRecord = ABPersonCreate();
    ABRecordSetValue(newRecord, kABPersonFirstNameProperty, (__bridge CFTypeRef)name, &error);

    ABMutableMultiValueRef multi = ABMultiValueCreateMutable(kABMultiStringPropertyType);
    ABMultiValueAddValueAndLabel(multi, (__bridge CFTypeRef)name, kABPersonPhoneMobileLabel, NULL);

    ABRecordSetValue(newRecord, kABPersonPhoneProperty, multi, &error);
    CFRelease(multi);

    ABAddressBookAddRecord(addressBook, newRecord, &error);
    CFRelease(newRecord);
    CFRelease(addressBook);
}

需要注意的是, 在本例中, 只填写了FirstNamephone, 实际还有许多其他属性可供选择, 具体参照ABAddressBookRefABRecordRef需要调用CFRelease来释放内存.

iOS9写入联系人

- (void)creatItemWithName:(NSString *)name phone:(NSString *)phone {
    // 创建对象
    CNMutableContact * contact = [[CNMutableContact alloc]init];
    contact.givenName = name?:@"defaultname";
    CNLabeledValue *phoneNumber = [CNLabeledValue labeledValueWithLabel:CNLabelPhoneNumberMobile value:[CNPhoneNumber phoneNumberWithStringValue:phone?:@"10086"]];
    contact.phoneNumbers = @[phoneNumber];

    // 把对象加到请求中
    CNSaveRequest * saveRequest = [[CNSaveRequest alloc]init];
    [saveRequest addContact:contact toContainerWithIdentifier:nil];

    // 执行请求
    CNContactStore * store = [[CNContactStore alloc]init];
    [store executeSaveRequest:saveRequest error:nil];
}

更稳健一点的写法, 是可以把请求放在通讯录授权判断的block中

    CNContactStore *store = [[CNContactStore alloc] init];
    [store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
        if (!granted) {
            dispatch_async(dispatch_get_main_queue(), ^{
                //失败原因
            });
            return;
        }
        //do something
    }];

使用下面这种方式

 - (void)addContactWithName:(NSString *)name {

    CNContactStore *store = [[CNContactStore alloc] init];
    [store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
        if (!granted) {
            dispatch_async(dispatch_get_main_queue(), ^{
                //失败原因
            });
            return;
        }

        CNMutableContact *contact = [[CNMutableContact alloc] init];
        contact.familyName = @"Doe";
        contact.givenName = @"John";

        CNLabeledValue *homePhone = [CNLabeledValue labeledValueWithLabel:CNLabelHome value:[CNPhoneNumber phoneNumberWithStringValue:@"312-555-1212"]];
        contact.phoneNumbers = @[homePhone];

        CNSaveRequest *request = [[CNSaveRequest alloc] init];
        [request addContact:contact toContainerWithIdentifier:nil];

        // save it
        NSError *saveError;
        if (![store executeSaveRequest:request error:&saveError]) {
            NSLog(@"error = %@", saveError);
        }
    }];
}

iOS8删除联系人

- (void)removeItemWithName:(NSString *)name phone:(NSString *)phone {
    ABAddressBookRef addressbook = ABAddressBookCreate();
    CFStringRef nameRef = (__bridge CFStringRef) name;
    CFArrayRef  allSearchRecords = ABAddressBookCopyPeopleWithName(addressbook, nameRef);
    if (allSearchRecords != NULL)
    {
        CFIndex count = CFArrayGetCount(allSearchRecords);
        for (int i = 0; i < count; ++i)
        {
            ABRecordRef contact = CFArrayGetValueAtIndex(allSearchRecords, i);
            ABAddressBookRemoveRecord(addressbook, contact, nil);
        }
    }
    ABAddressBookSave(addressbook, nil);
    CFRelease(addressbook);
}

iOS9删除联系人

- (void)removeContactWithName:(NSString *)name {
    CNContactStore *store = [[CNContactStore alloc] init];
    NSPredicate *predicate = [CNContact predicateForContactsMatchingName:name];
    NSArray *contacts = [store unifiedContactsMatchingPredicate:predicate keysToFetch:@[CNContactGivenNameKey, CNContactFamilyNameKey] error:nil];

    for (CNMutableContact *contact in contacts) {
        CNSaveRequest *request = [[CNSaveRequest alloc] init];
        [request deleteContact:contact];
        // save it
        NSError *saveError;
        if (![store executeSaveRequest:request error:&saveError]) {
            NSLog(@"error = %@", saveError);
        }
    }
}

通讯录数据读取

通过上面的一通操作, 我们已经可以创建和删除通讯录了, 那么我们就通过批量写入通讯录数据, 来进行通讯录数据的读取, 并且按照姓名和首字母分组排序. 相应的, 此处我们也区分iOS8iOS9下面两个不同的框架.

iOS8读取联系人

+ (NSArray *)getAllContact {
    NSMutableArray *array = [NSMutableArray arrayWithCapacity:0];

    CFErrorRef *error = NULL;
    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, error);

    CFIndex numberOfPeople = ABAddressBookGetPersonCount(addressBook);
    CFArrayRef people = ABAddressBookCopyArrayOfAllPeople(addressBook);
    if (numberOfPeople == 0) {
        CFRelease(people);
        CFRelease(addressBook);
        return @[];
    }
    for ( int i = 0; i < numberOfPeople; i++){
        AddressBookContact *contact = [[AddressBookContact alloc] init];

        ABRecordRef person = CFArrayGetValueAtIndex(people, i);

        //姓名
        NSString *firstName = (NSString *)CFBridgingRelease(ABRecordCopyValue(person, kABPersonFirstNameProperty));
        NSString *lastName = (NSString *)CFBridgingRelease(ABRecordCopyValue(person, kABPersonLastNameProperty));
        NSString *name = [NSString stringWithFormat:@"%@%@", lastName, firstName;
        contact.name = name;

        //第一次添加该条记录的时间戳
        NSDate *createDate = (NSDate *)CFBridgingRelease(ABRecordCopyValue(person, kABPersonCreationDateProperty));
        NSTimeInterval timeIn = [createDate timeIntervalSince1970];
        NSInteger createTime = round(timeIn);
        contact.createTime = [NSString stringWithFormat:@"%010ld", (long)createTime];

        //读取电话多值
        ABMultiValueRef phone = ABRecordCopyValue(person, kABPersonPhoneProperty);
        NSArray *arr = (NSArray *)CFBridgingRelease(ABMultiValueCopyArrayOfAllValues(phone));
        contact.phone = [AddressBookData filterPhoneFormate:[arr lastObject]];

        if (contact.phone.length > 0) {
            [array addObject:contact];
        }
    }
    CFRelease(people);
    CFRelease(addressBook);
    return array;
}

//剔除手机号中的特殊字符
+ (NSString *)filterPhoneFormate:(NSString *)phoneNumber {
    NSCharacterSet *notAllowedChars = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet];
    NSString *resultString = [[phoneNumber componentsSeparatedByCharactersInSet:notAllowedChars] componentsJoinedByString:@""];
    return resultString;
}

iOS9读取联系人

    // 创建通信录对象
    CNContactStore *contactStore = [[CNContactStore alloc] init];

    // 创建获取通信录的请求对象
    // 拿到所有打算获取的属性对应的key
    NSArray *keys = @[CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey];

    // 创建CNContactFetchRequest对象
    CNContactFetchRequest *request = [[CNContactFetchRequest alloc] initWithKeysToFetch:keys];

    // 遍历所有的联系人
    [contactStore enumerateContactsWithFetchRequest:request error:nil usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
        // 获取联系人的姓名
        NSString *lastname = contact.familyName;
        NSString *firstname = contact.givenName;
        NSLog(@"%@ %@", lastname, firstname);

        // 获取联系人的电话号码
        NSArray *phoneNums = contact.phoneNumbers;
        for (CNLabeledValue *labeledValue in phoneNums) {
            // 获取电话号码的KEY
            NSString *phoneLabel = labeledValue.label;

            // 获取电话号码
            CNPhoneNumber *phoneNumer = labeledValue.value;
            NSString *phoneValue = phoneNumer.stringValue;

            NSLog(@"%@ %@", phoneLabel, phoneValue);
            AddressBookContact *contact = [[AddressBookContact alloc] init];
            contact.name = [NSString stringWithFormat:@"%@%@",lastname,firstname];
            person.phone = phoneValue;
            [_addressBookArray addObject:contact];
        }
    }];

我们通过上面的方法拿到的数据是一个元素是我们自定义数据模型AddressBookContact的数组, 所有的数据都在这一个数组里面, 没有分组, 相同首字母的联系人按照创建时间来排序.显然这样的数据是不能满足我们业务需求的. 常见的是按照首字母来分组, 下面我们就以iOS8为例来对数据进行分组排序和展示, 类似于系统通讯录列表.

通讯录分组

//返回包含分组信息和分组后通讯录数据的字典
+ (NSDictionary *)dealDataWithArray:(NSArray *)array {
    if (array.count == 0) {
        return nil;
    }
    NSMutableArray *titleArray = [NSMutableArray arrayWithCapacity:0];
    NSMutableArray *data = [NSMutableArray arrayWithCapacity:0];
    NSMutableArray * tmpArray = [[NSMutableArray alloc]init];
    for (NSInteger i =0; i <27; i++) {
        //给临时数组创建27个数组作为元素,用来存放A-Z和#开头的联系人
        NSMutableArray * array = [[NSMutableArray alloc]init];
        [tmpArray addObject:array];
    }

    for (AddressBookContact * model in array) {
        //AddressMode是联系人的数据模型
        //转化为首拼音并取首字母
        NSString * nickName = [AddressBookDataManager returnFirstWordWithString:model.name];

        if (nickName.length == 0) {
            //如果不是,就放到最后一个代表#的数组
            NSMutableArray * array =[tmpArray lastObject];
            [array addObject:model];
        } else {
            int firstWord = [nickName characterAtIndex:0];
            //把字典放到对应的数组中去

            if (firstWord >= 65 && firstWord <= 90) {
                //如果首字母是A-Z,直接放到对应数组
                NSMutableArray * array = tmpArray[firstWord - 65];
                [array addObject:model];

            } else {
                //如果不是,就放到最后一个代表#的数组
                NSMutableArray * array =[tmpArray lastObject];
                [array addObject:model];
            }
        }
    }

    //此时数据已按首字母排序并分组
    //遍历数组,删掉空数组
    for (NSMutableArray * mutArr in tmpArray) {
        //如果数组不为空就添加到数据源当中
        if (mutArr.count != 0) {
            [data addObject:mutArr];
            AddressBookContact * model = mutArr[0];
            NSString * nickName = [AddressBookDataManager returnFirstWordWithString:model.name];

            if (nickName.length != 0) {
                int firstWord = [nickName characterAtIndex:0];
                //取出其中的首字母放入到标题数组,暂时不考虑非A-Z的情况
                if (firstWord >= 65 && firstWord <= 90) {
                    [titleArray addObject:nickName];
                }
            }
        }
    }

    //判断是否需要加#
    if (titleArray.count != data.count) {
        [titleArray addObject:@"#"];
    }

    NSDictionary *dic = @{@"source": [AddressBookDataManager sortedArray:data],
                          @"title": titleArray};
    return dic;
}

+ (NSArray *)sortedArray:(NSArray *)data {
    NSMutableArray *sortedArray = [NSMutableArray array];
    for (NSInteger index = 0; index < data.count; index++) {
        NSMutableArray *personArrayForSection = data[index];
        NSArray *temp = [personArrayForSection sortedArrayUsingComparator:^NSComparisonResult(AddressBookContact * contact1, AddressBookContact * contact2) {
            return [contact1.name compare:contact2.name];
        }];
        sortedArray[index] = temp;
    }
    return sortedArray;
}


#pragma mark - Tool
+ (BOOL)objectIsNull:(id)obj{
    return ([obj isKindOfClass:[NSNull class]] || obj == nil) ? YES : NO;
}

+ (NSString*)strNoNull:(id)str{
    if ([AddressBookDataManager objectIsNull:str]) {
        str = @"";
    }
    return str;
}

//汉字转拼音并取得关键字
+ (NSString *)returnFirstWordWithString:(NSString *)str {
    NSMutableString * mutStr = [NSMutableString stringWithString:str];

    //将mutStr中的汉字转化为带音标的拼音(如果是汉字就转换,如果不是则保持原样)
    CFStringTransform((__bridge CFMutableStringRef)mutStr, NULL, kCFStringTransformMandarinLatin, NO);
    //将带有音标的拼音转换成不带音标的拼音(这一步是从上一步的基础上来的,所以这两句话一句也不能少)
    CFStringTransform((__bridge CFMutableStringRef)mutStr, NULL, kCFStringTransformStripCombiningMarks, NO);
    if (mutStr.length > 0) {
        //全部转换为大写    取出首字母并返回
        NSString * res = [[mutStr uppercaseString] substringToIndex:1];
        return res;
    } else {
        return @"";
    }
}

//剔除手机号中的特殊字符
+ (NSString *)filterPhoneFormate:(NSString *)phoneNumber {
    NSCharacterSet *notAllowedChars = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet];
    NSString *resultString = [[phoneNumber componentsSeparatedByCharactersInSet:notAllowedChars] componentsJoinedByString:@""];
    return resultString;
}

数据截图
至此, 我们可以得到符合业务需求的通讯录页面, 已经按照姓名首字母分组, 并且实现了快速索引. 我的通讯录测试数据有2700+条数据, 每次通讯录打开都要耗时特别久, 为了更好的用户体验, 这时就会想到要优化一下代码了.

性能优化

说道性能优化, 首先就要进行性能分析, 知道我们需要优化的地方在哪里. Xcode提供了一个强大的分析工具Instruments, 具体一些常见的分析可以参考这里.
我们主要使用Instruments的Time ProFiler来分析一下, 究竟耗时的代码在哪里, 并且做一下优化.
耗时操作
耗时操作的具体代码
通过分析可以发现, 目前的瓶颈主要出现在汉字转拼音取首字母的方法上, 可以发现我们使用的是CFStringTransform类, 我们先对这个进行一些优化

//汉字首字母
+ (NSString *)returnFirstWordWithString:(NSString *)str {
    NSMutableString *mutableString = [NSMutableString stringWithString:str];
    CFStringTransform((CFMutableStringRef)mutableString, NULL, kCFStringTransformToLatin, false);
    mutableString = (NSMutableString *)[mutableString stringByFoldingWithOptions:NSDiacriticInsensitiveSearch locale:[NSLocale currentLocale]];
    NSString *string = [mutableString stringByReplacingOccurrencesOfString:@"'" withString:@""];
    return [[string uppercaseString] substringToIndex:1];
}

再使用Instruments分析发现, 并没有什么太大的效果. 分析一下发现, 发现我们取首字母主要有两个用途.一个是把全部通讯录按照首字母的方式进行分组, 另一个用途就是为了在列表页生成索引数组.

方案一

一番查找发现, 系统针对这种情况已经有API可供我们调用了(贴心的Apple).下面就要介绍本次优化的关键类UILocalizedIndexedCollation.

返回传入object对象指定selector在[UILocalizedIndexedCollation currentCollation]中的匹配的索引
// Returns the index of the section that will contain the object.
// selector must not take any arguments and return an NSString.

- (NSInteger)sectionForObject:(id)object collationStringSelector:(SEL)selector;

返回传入object对象指定selector在[UILocalizedIndexedCollation currentCollation]中的匹配的索引
// Returns the index of the section that will contain the object.
// selector must not take any arguments and return an NSString.

- (NSInteger)sectionForObject:(id)object collationStringSelector:(SEL)selector;
- 
+ (NSDictionary *)dealDataWithArray:(NSArray *)array {

    // 1.初始化一个索引,根据不同国家语言,会初始化出不同的索引,中文的是“A~Z,#”
    UILocalizedIndexedCollation *collation = [UILocalizedIndexedCollation currentCollation];
    // 2.获取索引的数量,并初始化对应数量的空数组,用于存放筛选数据
    NSInteger sectionTitlesCount = [[collation sectionTitles] count];
    NSMutableArray *sectionArrays = [NSMutableArray arrayWithCapacity:sectionTitlesCount];
    for (int i = 0; i < sectionTitlesCount; i++) {
        NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:1];
        [sectionArrays addObject:sectionArray];
    }
    // 3.排序的方法
    SEL sorter = ABPersonGetSortOrdering() == kABPersonSortByFirstName ? NSSelectorFromString(@"name") : NSSelectorFromString(@"name");
    // 4.分组
    for (AddressBookContact *contact in array) {
        //获取name属性的值所在的位置,比如"小白鼠",首字母是X,在A~Z中排第23(第一位是0),sectionNumber就为23
        NSInteger sectionNumber = [collation sectionForObject:contact collationStringSelector:sorter];
        //把name为“小白鼠”的contact加入newSectionsArray中的第23个数组中去
        NSMutableArray *sectionNames = sectionArrays[sectionNumber];
        [sectionNames addObject:contact];
    }
    //5.排序
    for (NSInteger i = 0; i < sectionTitlesCount; i++) {
        NSMutableArray *personArrayForSection = sectionArrays[i];
        NSArray *sortedPersonArrayForSection = [collation sortedArrayFromArray:personArrayForSection collationStringSelector:@selector(name)];
        sectionArrays[i] = sortedPersonArrayForSection;
    }

    NSArray *titleArray = [[[UILocalizedIndexedCollation currentCollation] sectionTitles] copy];
    NSDictionary *dic = @{@"source": sectionArrays,
                          @"title": titleArray};
    return dic;
    }

分析一下上面的代码, 我们使用UILocalizedIndexedCollation提供的方法, 按照A-Z来快读数据进行分组, 并且排序. 索引数组也直接使用UILocalizedIndexedCollation自带的方法.这样就避免了大量的循环遍历和取姓名拼音首字母造成的开销.

相应的, 列表展示页面的title和索引也要做调整

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
    if ([self.dataArray[section] count] == 0 || self.dataArray.count == 0) {
        return 0.01;
    }
    return 22;
}

// 按照索引个数配置tableview区数
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    if ([self.dataArray[section] count] == 0 || self.dataArray.count == 0) {
        return @"";
    }
    return [[UILocalizedIndexedCollation currentCollation] sectionTitles][section];
}

// 配置索引内容,就是通讯录中右侧的那一列“A~Z、#”
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
    return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles];
}

// 索引点击响应
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
    return [[UILocalizedIndexedCollation currentCollation] sectionForSectionIndexTitleAtIndex:index];
}

至此优化结束, 再通过Time Profiler来分析下发现时间已经从5.8s到2.6s, 优化效果还是很明显的. 2.6s中数组排序大概耗时1.7s, 我们使用的已经是系统推荐的排序方法, 除非采用复杂度更低的排序算法, 这里已经没办法再优化了.

这里有三种不同实现的排序方法, 测试了下, 优化效果都不明显

+ (NSArray *)sortedArray:(NSArray *)data {
    //        NSMutableArray *sortedArray = [NSMutableArray array];
    //        //对每个section中的数组按照name属性排序
    //        for (NSInteger index = 0; index < data.count; index++) {
    //            NSMutableArray *personArrayForSection = data[index];
    //            NSSortDescriptor *nameDesc    = [NSSortDescriptor sortDescriptorWithKey:@"name"
    //                                                                          ascending:YES];
    //            NSArray *descriptorArray = @[nameDesc];//此处可以按照多个排序规则, 顺序比较, 比较的顺序就是数组里面元素的顺序
    //
    //            NSArray *temp = [personArrayForSection sortedArrayUsingDescriptors: descriptorArray];
    //            sortedArray[index] = temp;
    //        }


    NSMutableArray *sortedArray = [NSMutableArray array];
    for (NSInteger index = 0; index < data.count; index++) {
        NSMutableArray *personArrayForSection = data[index];
        NSArray *temp = [personArrayForSection sortedArrayUsingComparator:^NSComparisonResult(AddressBookContact * contact1, AddressBookContact * contact2) {
            return [contact1.name compare:contact2.name];
        }];
        sortedArray[index] = temp;
    }



    //    UILocalizedIndexedCollation *collation = [UILocalizedIndexedCollation currentCollation];
    //    NSMutableArray *sortedArray = [NSMutableArray array];
    //    //对每个section中的数组按照name属性排序
    //    for (NSInteger index = 0; index < data.count; index++) {
    //        NSMutableArray *personArrayForSection = data[index];
    //        NSArray *sortedPersonArrayForSection = [collation sortedArrayFromArray:personArrayForSection collationStringSelector:@selector(name)];
    //        sortedArray[index] = sortedPersonArrayForSection;
    //    }
    return sortedArray;
}

方案二

至于第二种优化方案, 我们需要借助本地化存储SQL来实现. 当然如果没有本地化存储的需求, 则方案一就够用了.

因为主要耗时的操作在分组和排序上, 我么你可以利用SQL的快速查找和排序来实现.在3000条左右数据时和方案一的效果差不多, 此处就不再贴代码了, 只提供一下思路.

监听通讯录数据变化

此处仍然区分iOS8和iOS9的API, 但是需要注意监听规则:
1.当App活跃(前台+后台包活期间)的时候, 当通讯录修改的时候, 会收到通知.
2.当App不活跃的时候(挂起的时候), App收不到通知; 而是, 当App到前台的时候收到延迟的通知.
3.当App被杀掉进程后, App收不到通知; 当再次进入App时依然没有通知.

ios8监听通讯录变化

@property (nonatomic, assign) ABAddressBookRef addresBook;

- (instancetype)init {
    self = [super init];
    if (self) {
        _addresBook = ABAddressBookCreateWithOptions(NULL, NULL);
        ABAddressBookRegisterExternalChangeCallback(_addresBook, addressBookChanged, nil);
    }
    return self;
}

//监听通讯录变化
void addressBookChanged(ABAddressBookRef addressBook, CFDictionaryRef info, void *context) {
    NSLog(@"通讯录变化啦....");
    //    VC1 *myVC = (__bridge VC1 *)context;
    //    [myVC getPersonOutOfAddressBook];
}

- (void)dealloc {
    NSLog(@"%@-------------------dealloc", self);
    ABAddressBookUnregisterExternalChangeCallback(_addresBook, addressBookChanged, nil);
}

ios9监听通讯录变化

- (instancetype)init {
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(addressBookDidChange:) name:CNContactStoreDidChangeNotification object:nil];

    }
    return self;
}


- (void)dealloc {
    NSLog(@"%@-------------------dealloc", self);
    [[NSNotificationCenter defaultCenter] removeObserver:self name:CNContactStoreDidChangeNotification object:nil];
}

- (void)addressBookDidChange:(NSNotification*)notification{
    NSLog(@"通讯录变化啦....");
}

参考文档:

1.iOS9下全新的通讯录框架

2.iOS学习笔记29-系统服务(二)通讯录

3.iOS通讯录开发

4.CNContact官方文档

5.AddressBook官方文档

6.UILocalizedIndexedCollation——本地化索引排序

7.iOS开发中如何快速的实现汉字转拼音

8.iOS通讯录数据变化监听