在后台线程上将Core Data图像加载到UITableViewCell时崩溃

时间:2014-01-24 01:14:42

标签: ios objective-c core-data uitableview

我将一系列个人资料图片存储在核心数据中Binary Data(启用了允许外部存储选项,因此请勿跳转到我在Core Data中存储图像: )

每张图片都以UITableViewCell显示。目前,当用户点击显示表视图时会有轻微的延迟,大概是因为它正在从Core Data加载它们,这对于在主线程上加载UI时具有足够的性能影响来锁定UI。

我想将图片加载到一个单独的后台线程中,以便立即显示表格视图,并且图像显示何时从Core Data加载。

我在-cellForRowAtIndexPath:方法中使用以下代码尝试了解决方案in this post

    // Create the cell
    MyCell *cell = [tableView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void) {

        // Get a reference to the object for this cell (an array obtained from Core Data previously in the code)
        MyObject *theObject = [self.objectArray objectAtIndex:indexPath.item];

        // Load the photo from Core Data relationship (ProfilePhoto entity)
        UIImage *profilePhoto = [UIImage imageWithData:theObject.profilePhoto.photo];

        // Set the name
        cell.nameLabel.text = theObject.name;

        dispatch_sync(dispatch_get_main_queue(), ^(void) {

            // Set the photo
            cell.photoImageView.image = profilePhoto;

        });

    });

    // Return the cell
    return cell;

但是,应用程序崩溃并出现以下错误(对于表视图中的每一行重复):

CoreData: error: exception during fetchRowForObjectID: statement is still active with userInfo of (null)

理解和解决这个问题的任何帮助都将不胜感激。

4 个答案:

答案 0 :(得分:3)

solution which Marcus Zarra provided给了我一个关于如何解决这个问题的想法,但我认为为其他任何寻找解决方案的人提供实施的代码是值得的。所以,这是我的-cellForRowAtIndexPath:方法的新代码:

// Create the cell
MyCell *cell = [tableView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];

// Get a reference to the object for this cell
MyObject *theObject = [self.objectArray objectAtIndex:indexPath.item];

// Reset the image
cell.imageView.image = nil;

// Set the name
cell.nameLabel.text = theObject.name;

// Check if the object has a photo
if (theObject.profilePhoto != nil) {

    // Create a new managed object context for the background thread
    NSManagedObjectContext *backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    backgroundContext.parentContext = self.managedObjectContext;

    // Perform the operations on the new context
    [backgroundContext performBlockAndWait:^{

        // Fetch the object
        NSError *error;
        ProfilePhoto *profilePhotoObject = (ProfilePhoto *)[backgroundContext existingObjectWithID:theObject.profilePhoto.objectID error:&error];

        // Create the image
        UIImage *thePhoto = [UIImage imageWithData:myObject.profilePhoto];

        dispatch_async(dispatch_get_main_queue(), ^(void) {

            // Set the photo
            cell.imageView.image = thePhoto;

        });

    }];

}

return cell;

您还需要更改App Delegate文件中的行,该行创建托管对象上下文:

_managedObjectContext = [[NSManagedObjectContext alloc] init];

_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

否则.parentContext分配将无效。

希望这可以帮助其他人遇到这个问题!

答案 1 :(得分:2)

您违反了Core Data的线程规则。 NSManagedObjectContext及其所有关联对象只能在创建它的线程上访问(或者线程已分配给它)。

您似乎正在访问该块中NSManagedObject的实例,该实例位于另一个线程上,因此违反了线程包含规则。

您想通过将这些内容移动到后台队列来实现什么目标?

更新

首先,您确定UIImage创作是缓慢的吗?你有没有想过这段代码?这些图片有多大?它们是否太大而且成本是在调整大小?实际成本在哪里?

其次,UIImage的创建可以在另一个线程上完成,但是您无法从多个线程访问上下文。这是一条难以克服任何稳定性的强硬路线。

第三,我不建议在Core Data中存储图像。不建议在SQLite文件中存储二进制数据。我将假设您正在使用外部记录存储选项。

解决这个问题的方法,假设UIImage的加载和创建是实际问题,那就是:

  1. 在后台队列上创建上下文(或创建专用队列上下文)
  2. 从该上下文重新加载NSManagedObject(您可以传递objectID,因为它是线程安全的。
  3. NSManagedObject
  4. 的第二个实例中检索图像
  5. 继续使用您的代码。
  6. 如果这听起来像很多工作你会是对的!因此建议首先分析这个代码,并确保实际上是什么慢,并尝试找出它为什么慢。只需加载和初始化UIImage不应该 慢。

答案 2 :(得分:1)

您在多个队列中使用相同的托管对象上下文,因此您的应用程序崩溃。

当您致电dispatch_get_global_queue时,您会获得并发队列。当您在该队列上排队一堆块时,它们可以并行运行。在块中,您使用的核心数据对象都来自同一个托管对象上下文 - 这不是线程安全的。这几乎是崩溃的秘诀。

您应该做的是使用NSManagedObjectContext队列限制。使用NSPrivateQueueConcurrencyTypeNSMainQueueConcurrencyType创建上下文(私有在这里更好,因为您正在尝试从主队列中获取工作)。然后使用performBlockAndWait来处理Core Data对象。这就像是

[self.context performBlockAndWait:^{
    MyObject *theObject = [self.objectArray objectAtIndex:indexPath.item];
    ... etcetera ...
    dispatch_async(dispatch_get_main_queue(), ^(void) {

        // Set the photo
        cell.photoImageView.image = profilePhoto;

    });

}];

块将按顺序执行,因此没有线程问题。它们将在上下文的私有队列中运行,该队列不会成为主队列。请注意,我更改了UI更新块以使用上面的dispatch_async - 没有理由通过执行同步调用来阻止托管对象上下文队列。

答案 3 :(得分:0)

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{


        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
         });

});

dispatch_get_global_queue为您提供一个后台队列,您可以在其上调度异步运行的后台任务(主要思想不会阻止您的用户界面)。

但是不允许在后台执行用户界面进程,因此dispatch_async dispatch_get_main_queue允许后台队列将用户界面更新分发回主队列。