【问题标题】:UISearchBar performance issue with Core DataCore Data 的 UISearchBar 性能问题
【发布时间】:2014-01-08 15:18:29
【问题描述】:

当我使用UISearchBar 并写一些东西作为搜索字符串整个搜索变得有点滞后。我的猜测是我在主线程中弄乱了 UI 的东西和核心数据,但我可能错了。更重要的是,我对所有这些东西都使用一个实体,所以没有关系等。这个表中有 3321 个对象,这个应用程序消耗大约 12 到 14 MB 的 RAM 你可以看到在下面的屏幕截图中:

我认为它会更有效,因为 3321 个对象并不多。对于所有核心数据,我使用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 中并异步执行,将其与 UI 分开... UI 将不得不等待很长时间才能重新加载表并不那么滞后看法。但这是正确的方法吗?我必须提高这个UISearchBar 的性能,因为我不想让客户不满意。
我希望你能给我一些想法,或者你对我的代码有任何改进

【问题讨论】:

  • fetchBatchSize 是否设置在任何地方(因此 fetch 不会尝试返回比您需要的更多的数据)?
  • 不...会检查的!但我真的不认为MagicalRecords 确实需要这批...

标签: ios multithreading uitableview core-data nspredicate


【解决方案1】:

首先,CONTAINS 很慢。虽然这些信息对您的问题没有帮助,但很高兴知道您从一开始就在努力上山。

其次,您在按下每个字母时都会敲击磁盘。这很浪费,当然也很慢。

您正在使用 Magical Record,它似乎在每个字母印刷机上构建一个新的NSFetchedResultsController。这很浪费,当然也很慢。

你应该怎么做?

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

在第二个和随后的字母上按 过滤现有的NSArray。您不会返回到磁盘。

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

这会将您的磁盘命中限制为仅第一个字母,并且在检测到删除时会从根本上增加您的搜索时间。

更新

关于魔法记录。我对第三方框架的看法很苍白。我总是建议避免使用它们。这种避免与代码质量无关,而是与尽可能接近金属有关。 MR 是 Core Data 之上的一个层,我看不到它的价值。当然,我也不喜欢点语法,所以对我的意见持保留态度:)

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

至于CONTAINS;不知道你是否可以避免它。它很慢,但很有效,而且您正在进行字符串比较,因此在这方面您无能为力。因此,请修复所有其他问题,这样您就不会支付超出您需要的计算税。

【讨论】:

  • 那么,基本上每次开始搜索时创建NSArray 会更好?更重要的是,我应该放弃 MagicalRecord 并让一切都使用普通的 Core Data 吗?还有一件事,这个CONTAINS...我不知道我应该使用什么谓词...留在我脑海中的是LIKE...
  • 非常感谢您让一切变得更加清晰。现在我知道我有很多工作:)
  • 作为 MagicalRecord 的作者和 Marcus Zarra 的同事,我会说在 MagicalRecord 中没有什么是您自己做不到的。图书馆的重点是有一个地方来保存这些帮助者,并让其他人为公共图书馆做出贡献。从您的代码来看,您仍然没有完全掌握 Core Data,我强烈建议您在使用诸如 MagicalRecord(或任何其他)之类的帮助程序库之前使用它。
猜你喜欢
  • 2013-01-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-31
相关资源
最近更新 更多