在 cellForRowAtIndexPath 从 CoreData 加载图像会减慢滚动速度

Posted

技术标签:

【中文标题】在 cellForRowAtIndexPath 从 CoreData 加载图像会减慢滚动速度【英文标题】:Loading image from CoreData at cellForRowAtIndexPath slows down scrolling 【发布时间】:2013-01-31 10:34:17 【问题描述】:

我正在开发一个与 ios 的原生照片应用程序非常相似的 UITableView:它有很多行,每行有 4 个图像缩略图。 (即每个 UITableViewCell 有 4 个 UIImageViews)从 Core Data 加载的所有缩略图。

我已经多次修改了我的实现,我可以看到性能改进,但它仍然无法像照片应用程序那样流畅地滚动。

我需要有关如何正确缓存照片以获得最佳性能的建议。这是我尝试过的:

1.我的第一次尝试(滚动时非常滞后)

图像在 CoreData 中以 Transformable 类型存储。 在 cellForRow 函数中,每张图片都是从 CoreData 上的 飞。

2。第二次尝试(更快,但滚动时仍然有点滞后)

图像以二进制数据类型存储,CoreData 中勾选了“外部存储”选项。 在 cellForRow 函数中,每个图像首先从 Core Data 加载,然后存储到内存中的 NSCache 中,因此下次 cellForRow 触发时,如果可用,我们将直接使用 NSCache 中的 UIImage。

在使用 NSCache 缓存从 CoreData 加载的图像后,滚动速度明显更快,但由于在 NSCache 中尚不可用时仍需要从 CoreData 加载图像,因此滚动仍会不时出现抖动。

所以,一定有更好的方法,我可以将所有图像预加载到内存中,但由于可能存在大量或多行图像,所以我根本不打算预加载图像。

我还能做些什么来更快地在 cellForRowAtIndexPath 中加载图像?

【问题讨论】:

您从哪里加载图像?是网络服务器还是本地 iphone 相册? 嗨 Muhammad,这些图片实际上是用户 Facebook 好友的 Facebook 个人资料图片。我使用 AFNetworking 类异步下载它们,然后显示并将它们存储到 Core Data + NSCache 中。当我调试这个问题时,我将场景简化为:所有图像都已经下载并在 CoreData 中可用,我只是从 CoreData 加载它们(想象用户在离线模式下查看他们的 Facebook 相册)。该表仍在缓慢滚动,这就是为什么我没有提到问题中的下载部分以简化问题。谢谢! 更多说明:如果我使用 UIImageView+AFNetworking 那么我可以流畅地显示用户的 facebook 朋友头像图像的表格,而不会出现滚动问题。但这意味着应用程序每次加载时都必须一次又一次地从服务器下载图像。我尝试使用 SDURLCache 离线缓存图像,但它不起作用,我喜欢对缓存进行精细控制,所以我决定只使用 AFNetworking 下载配置文件图像,但我自己使用 CoreData 将其缓存以供离线使用。 【参考方案1】:

无论数据来自何处,为了保持滚动流畅,您需要在单独的线程上获取数据,并且仅当内存中有数据时才更新 UI。 Grand Central Despatch 是要走的路。这是一个骨架,假设您有一个 self.photos 字典,其中包含对图像文件的文本引用。图像缩略图可能会或可能不会加载到实时字典中;可能在也可能不在文件系统缓存中;否则从在线商店获取。它可以使用 Core Data,但平滑滚动的关键是您不必等待数据无论来自哪里。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    
    static NSString *CellIdentifier = @"Photo Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    //identify the data you are after
    id photo = [self.photos objectAtIndex:indexPath.row];
        // Configure the cell based on photo id

      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
        //move to an asynchronous thread to fetch your image data
            UIImage* thumbnail = //get thumbnail from photo id dictionary (fastest)
            if (!thumbnail)     //if it's not in the dictionary
                thumbnail =      //get it from the cache  (slower)
                                 // update the dictionary
                if (!thumbnail)    //if it's not in the cache
                  thumbnail =       //fetch it from the (online?) database (slowest)
                                    // update cache and dictionary
                    
                
            
            if (thumbnail)   
                dispatch_async(dispatch_get_main_queue(), ^
                //return to the main thread to update the UI
                    if ([[tableView indexPathsForVisibleRows] containsObject:indexPath]) 
                    //check that the relevant data is still required
                        UITableViewCell * correctCell = [self.tableView cellForRowAtIndexPath:indexPath];
                        //get the correct cell (it might have changed)
                        [[correctCell imageView] setImage:thumbnail];
                        [correctCell setNeedsLayout];
                    
                );
            
        );
    return cell;
    

如果您使用某种单例图像存储管理器,您会希望该管理器处理缓存/数据库访问的细节,这会简化此示例。

这部分

            UIImage* thumbnail = //get thumbnail from photo id dictionary (fastest)
            if (!thumbnail)     //if it's not in the dictionary
                thumbnail =      //get it from the cache  (slower)
                                 // update the dictionary
                if (!thumbnail)    //if it's not in the cache
                  thumbnail =       //fetch it from the (online?) database (slowest)
                                    // update cache and dictionary
                    
                

将被替换为类似的东西

      UIImage* thumbnail = [[ImageManager singleton] getImage];

(您不会使用完成块,因为当您返回主队列时,您实际上是在 GCD 中提供了一个)

【讨论】:

谢谢!我可以再问你一个问题,如果我使用完成块,是否也一样?像 [ImageManager 单例] getImageWithBlock:(void (^)(NSArray *images))block @dunforget - 一种 - 完成块等待(异步)它的方法完成 - 该方法在完成时触发块(它类似于委托的工作方式)。但是除非您另有指示,否则该块本身将在主线程上排队。一般来说,如果它影响 UI,你需要在主线程上运行一些东西。但是任何非 UI 的东西都应该从主线程中移除,如果它会阻碍用户体验。 @dunforget,如果您使用某种图像管理器单例,您可以期望它处理所有字典/缓存/url 访问细节......所以我的示例的异步部分可以减少到一行。我已经更新了答案。还有一个错误,我在调用异步线程后立即调用dispatch_async(dispatch_get_main_queue() - 已更正。 非常好的答案。我对那个案子没有意见,但我肯定赞成! 在后台线程中加载 CoreData 时出现“'NSInternalInconsistencyException',原因:'statement is still active'”崩溃 - 想法?【参考方案2】:

我使用了一个名为JMImageCache 的类将图像缓存到磁盘,然后只在CoreData 中存储本地URL(和远程URL,如果你喜欢的话)。您还可以生成一个缩略图,并将其保存到磁盘并将缩略图 URL 与图像 URL 一起存储在核心数据中,并在表格视图中使用缩略图。

【讨论】:

谢谢关于缓存类的提示,我不知道【参考方案3】:

为核心数据中的图像添加一个NSValueTransformer的子类,

代码如下:

+ (BOOL)allowsReverseTransformation 
    return YES;

+ (Class)transformedValueClass 
    return [NSData class];

 - (id)transformedValue:(id)value 
    return UIImageJPEGRepresentation(value, 0.5);
 
- (id)reverseTransformedValue:(id)value 
    return [[UIImage alloc] initWithData:value];
 

【讨论】:

以上是关于在 cellForRowAtIndexPath 从 CoreData 加载图像会减慢滚动速度的主要内容,如果未能解决你的问题,请参考以下文章

在 cellForRowAtIndexPath 从 CoreData 加载图像会减慢滚动速度

如何使用 cellForRowAtIndexPath 从 TableView 中获取自定义 UITableViewCell?

从情节提要加载 UILabel,无法更改 cellForRowAtIndexPath 中的框架

UITableView dataSource 必须从 tableView:cellForRowAtIndexPath 返回一个单元格

UITableView 数据源必须从 tableView 返回单元格:cellForRowAtIndexPath

UITableView dataSource 必须从 tableView:cellForRowAtIndexPath 返回一个单元格: