UISearchBar与Core Data的性能问题

时间:2014-01-08 15:18:29

标签: ios multithreading uitableview core-data nspredicate

当我使用UISearchBar并将某些内容写为搜索字符串时,整个搜索内容会变得有点滞后。我的猜测是我在主线程中弄乱UI内容和Core Data但我可能错了。更重要的是,我使用一个实体来处理所有这些东西,所以没有任何关系等等。这个表中有3321个对象,这个应用程序正在消耗大约 12到14 MB的RAM 你可以看到什么在下面的屏幕截图中:

RAM usage

我认为它会更有效率,因为3321对象不是那么多。对于所有Core Data内容,我使用MagicalReacord。我在NSFetchedResultController的一个实例上操作,但在主表视图和搜索表视图之间切换NSPredicate 但是源代码没有什么比这更有价值了,所以在这里:

#import "GroupsViewController.h"
#import "CashURLs.h"
#import "Group.h"
#import <AFNetworking.h>

@interface GroupsViewController ()
{
    NSPredicate *resultPredicate;
    UIRefreshControl *refreshControl;
}

@property (strong, nonatomic) NSMutableArray *selectedGroups;
@property (strong, nonatomic) NSFetchedResultsController *groupsFRC;

@end

@implementation GroupsViewController

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    // Getting array of selected groups
    self.selectedGroups = [NSMutableArray arrayWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:@"SelectedGroups"]];
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    resultPredicate = nil;

    // Initializing pull to refresh
    refreshControl = [[UIRefreshControl alloc] init];
    [refreshControl addTarget:self action:@selector(refreshData) forControlEvents:UIControlEventValueChanged];
    [self.tableView addSubview:refreshControl];

    // Check if there is at least one Group entity in persistat store
    if (![Group MR_hasAtLeastOneEntity]) {
        [self refreshData];
    } else {
        [self refreshFRC];
        [self.tableView reloadData];
    }
}

#pragma mark - Downloading

-(void)refreshData
{
    // Show refresh control
    [refreshControl beginRefreshing];

    // On refresh delete all previous groups (To avoid duplicates and ghost-groups)
    [Group MR_truncateAll];

    [[AFHTTPRequestOperationManager manager] GET:ALL_GROUPS parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
        // For each group from downloaded JSON...
        for (id group in responseObject) {
            // ... Create entity and filll it with data
            Group *groupEntity = [Group MR_createEntity];

            groupEntity.name = [group valueForKey:@"name"];
            groupEntity.cashID = [group valueForKey:@"id"];
            groupEntity.sectionLetter = [[[group valueForKey:@"name"] substringToIndex:1] uppercaseString];
            groupEntity.caseInsensitiveName = [[group valueForKey:@"name"] lowercaseString];
        }

        // Save Groups to persistent store
        [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
        [self refreshFRC];
        [self.tableView reloadData];
        [refreshControl endRefreshing];
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"Failed to load data: %@", [error localizedDescription]);
        // End refreshing
        [refreshControl endRefreshing];

        // Show alert with info about internet connection
        UIAlertView *internetAlert = [[UIAlertView alloc] initWithTitle:@"Ups!" message:@"Wygląda na to, że nie masz połączenia z internetem" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];
        [internetAlert show];
    }];
}

#pragma mark - Table View

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Count sections in FRC
    return [[self.groupsFRC sections] count];
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Count groups in each section of FRC
    return [[[self.groupsFRC sections] objectAtIndex:section] numberOfObjects];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Get reusable cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"GroupCell"];

    // If there isn't any create new one
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"GroupCell"];
    }

    Group *group = [self.groupsFRC objectAtIndexPath:indexPath];

    cell.textLabel.text = group.name;

    // Checking if group has been selected earlier
    if ([self.selectedGroups containsObject:@{@"name" : group.name, @"id" : group.cashID}]) {
        [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
    } else {
        [cell setAccessoryType:UITableViewCellAccessoryNone];
    }

    return cell;
}

// Adding checkmark to selected cell
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
    Group *group = [self.groupsFRC objectAtIndexPath:indexPath];

    // Checking if selected cell has accessory view set to checkmark and add group to selected groups array
    if (selectedCell.accessoryType == UITableViewCellAccessoryNone)
    {
        selectedCell.accessoryType = UITableViewCellAccessoryCheckmark;
        [self.selectedGroups addObject:@{@"name" : group.name, @"id" : group.cashID}];
        NSLog(@"%@", self.selectedGroups);
    }
    else if (selectedCell.accessoryType == UITableViewCellAccessoryCheckmark)
    {
        selectedCell.accessoryType = UITableViewCellAccessoryNone;
        [self.selectedGroups removeObject:@{@"name" : group.name, @"id" : group.cashID}];
        NSLog(@"%@", self.selectedGroups);
    }

    // Hiding selection with animation for nice and clean effect
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

#pragma mark - Filtering/Searching

// Seting searching predicate if there are any characters in search bar
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    if (searchText.length > 0) {
        resultPredicate = [NSPredicate predicateWithFormat:@"SELF.caseInsensitiveName CONTAINS[c] %@", searchText];
    } else {
        resultPredicate = nil;
    }

    [self refreshFRC];
}

-(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
    // If user cancels searching we set predicate to nil
    resultPredicate = nil;
    [self refreshFRC];
}

// Refreshing NSFetchedResultController
- (void)refreshFRC
{
    self.groupsFRC = [Group MR_fetchAllSortedBy:@"caseInsensitiveName"
                                      ascending:YES
                                  withPredicate:resultPredicate
                                        groupBy:@"sectionLetter"
                                       delegate:self];
}

我在一个thread中读到CONTAINS可能消耗资源但我真的不知道如何以其他方式实现它。 我的另一个猜测是将此搜索放在另一个queue并以异步方式执行,将其与用户界面分开...这不是那么迟钝,以至于用户界面必须等待多年重新加载表视图。但这是正确的方法吗?我必须改善这个UISearchBar的表现,因为我不想让不满意的顾客 我希望您能提出一些想法或者我的代码有任何改进

1 个答案:

答案 0 :(得分:11)

首先,CONTAINS很慢。虽然这些信息对你的问题没有帮助,但最好还是知道你是在为了起步而奋斗。

其次,你在按下的每个字母上击中磁盘。这很浪费,当然也很慢。

您正在使用Magical Record,它似乎在每次按下时都会生成一个新的NSFetchedResultsController。这很浪费,当然也很慢。

你应该怎么做?

在第一个字母上按下一个简单的NSFetchRequest并保持批量大小,甚至可以保持提取限制。保留由此产生的NSArray并使用它来显示结果。 是的,这会使您的UITableViewDataSource更复杂。

在第二封及后续字母上按下,您可以针对现有NSArray 过滤。你不会回到磁盘。

如果检测到删除,则吹走阵列并从磁盘重建。

这会将磁盘命中数限制在第一个字母,并且当检测到删除时会大大增加搜索时间。

更新

关于魔法记录。我对第三方框架有很多灰胡子的看法。我总是建议避免它们。避免与代码质量无关,它与尽可能靠近金属保持一致。 MR是核心数据之上的一层,我看不到它的价值。当然我也不喜欢点语法,所以我的意见很多:)

是的,您应该将第一个搜索结果存储在一个数组中,然后针对该数组显示。它会更快。

至于CONTAINS;不确定你是否可以避免它。它很慢但它有效并且你正在进行字符串比较,所以你在这方面做的并不多。所以要解决其他问题,这样你就不会支付超出你需要的计算税。