UITableView在向上滚动时卡顿。

14
解决方案:使用SDWebImage:https://github.com/rs/SDWebImage 我有一个UITableView,在滚动时变得非常卡顿。我发现当我使用的图片再次出现在屏幕上时,就会出现卡顿现象。
我正在使用自定义的UITableViewCell。这也是导致卡顿的原因吗?
我的自定义UITableViewCell如下图所示: enter image description here 我的代码:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

NSString *CellIdentifier = [NSString stringWithFormat:@"Cell%d%d",indexPath.row,indexPath.section];


tableViewCellActiviteiten *cell = (tableViewCellActiviteiten *)[self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];

if (cell == nil)
{

    NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"tableViewCellActiviteiten" owner:self options:nil];
    cell = (tableViewCellActiviteiten *)[nib objectAtIndex:0];
}


cell.thetitle.text = [self.alletitels objectAtIndex:indexPath.row];
cell.thesubtitle.text = [self.allesubtitels objectAtIndex:indexPath.row];

NSString * imagePath = [self.alleimages objectAtIndex:indexPath.row];

NSURL * imageURL = [NSURL URLWithString:imagePath];
NSData * imageData = [NSData dataWithContentsOfURL:imageURL];
UIImage * image = [UIImage imageWithData:imageData];

cell.image.image = image;

return cell;

}

数组内容:

self.alletitels 包含一个字符串:"活动标题"

self.allesubtitels 包含一个字符串:"活动副标题"

self.alleimages 包含一个 URL: "http://m2.myhappygames.com//files/pics/0/Paranormal_Shark_Activity_3.jpg"

有人能指出导致滚动卡顿的原因吗?


1
你的速度变慢是因为你正在从网络获取图像。你可以尝试从另一个调度队列异步获取图像。查找Grand Central Dispatch。 - Douglas
这是一个经典的反模式和非常常见的编程失误。每周大约会有10次被问到这个问题。使用搜索功能可能已经揭示了数十种解决方案。 - CouchDeveloper
@CouchDeveloper 我明白,但我仍然找不到答案。我已经修复了一部分问题,但是表格视图仍然很卡顿。真的很令人沮丧... - SDW
在缓存图像时,您应确保该图像也被解码(通过绘制到上下文中获得位图),并且其当前大小完全适合UIImage的框架。您需要确保在绘制单元格之前立即满足此条件。因此,可能需要更复杂的缓存策略。尽管如此,没有任何提到的辅助库会为您完成这项工作。 - CouchDeveloper
当从数据库加载项目并实现动态字体大小和动态单元格大小时,我遇到了相同的问题? - Albin Joseph
@AlbinJoseph 我使用 EGO 框架修复了它:https://github.com/enormego/EGOImageLoading 和 https://github.com/enormego/EGOCache。 - SDW
7个回答

9
问题在于每次调用tableView:cellForRowAtIndexPath:方法时,您的代码都会生成一张图片。每当单元格出现在屏幕上时,就会调用此方法,因此,如果您快速滚动,该方法将开始同步分配大量图像,这将减慢滚动速度。

作为一个快速解决方案,在您的视图控制器中实现NSCache并在其中存储图像。

UIImage *image = [_imageCache objectForKey:@indexPath.row];
if (image == nil) {
    NSString * imagePath = [self.alleimages objectAtIndex:indexPath.row];
    NSURL * imageURL = [NSURL URLWithString:imagePath];
    NSData * imageData = [NSData dataWithContentsOfURL:imageURL];
    image = [UIImage imageWithData:imageData];
    [_imageCache setObject:image forKey:@indexPath.row];
}

cell.image.image = image;

_imageCache是视图控制器的实例变量,您可以进行实现。

希望这能帮到您,祝好!


作为快速解决方案,在您的视图控制器中实现NSCache并将图像存储在其中。我对NSCache非常陌生...如何将图像存储在NSCache中?我是否仍应使用self.alleimages数组? - SDW
简单来说,NSCache的行为几乎与NSDictionary完全相同,但功能较少。您可以使用与NSDictionary相同的方式将图像存储在NSCache中(setObject:forKey:方法)。是的-您仍然需要self.alleimages数组来检索URL。这是有关NSCache的AppleDocs链接https://developer.apple.com/library/ios/documentation/cocoa/reference/NSCache_Class/Reference/Reference.html - Arthur Shinkevich
@indexPath.row 是一个对象字面量。本质上是这样的 - [NSNumber numberWithInteger:indexPath.row]。我认为您不能将NSCache作为IBOutlet,因为没有界面元素负责NSCache。最后,是的,我的代码应该能够工作,但这只是一个快速解决方案。 - Arthur Shinkevich
2
这个解决方案唯一的问题是它行不通 ;) 你需要异步加载图片并可能将它们保存到缓存中。如果没有图片,则显示占位符。异步加载器的完成块必须检查单元格是否可见。如果是,则设置图像,否则不执行任何操作。当图像缓存用尽时,您需要一个更复杂的缓存,仅保存少量图像但提前预加载准备图像。这样,每当要绘制任何单元格时,图像已经准备好并且渲染速度快。 - CouchDeveloper
什么是检查给定单元格(或图像)是否可见的首选机制? - owenfi
显示剩余6条评论

6
这里有两行代码:

NSData * imageData = [NSData dataWithContentsOfURL:imageURL];
UIImage * image = [UIImage imageWithData:imageData];

以下是您表格性能明显减慢的原因:

每次刷新单元格时,您都会同步获取和加载图像数据。

更明智的做法是以某种方式缓存(或本地保存)要显示的图片。如果它们没有被本地或缓存保存,则仅在这些情况下获取这些图像(并异步执行该"cellForRowAtIndexPath:"方法之外的操作)。


我正在使用NSURLConnection的sendAsynchronousRequest方法从我的JSON数据中获取图像URL。然后,我将这些URL保存在NSMutableArray中。如何将这些URL保存到缓存中? - SDW
你已经获取图像的URL并将它们保存在可变数组中,这很好。但更好的方法是使用sendAsynchronousRequest来获取实际图片,然后将这些UIImages保存在可变数组中。 - Michael Dautermann

4
您正在进行同步网络调用以下载图像,同时单元格正在创建。这些调用会阻塞主线程,直到单元格的图像下载完成。延迟时间将根据网络质量而变化。
解决此问题的方法是使用一种称为“懒加载”的技术。在这种技术中,图像加载将在单独的线程上进行,从而解除主线程的阻塞。图像被下载,然后应用于正确的容器UIImageView。这里是一个关于在表视图中懒加载图像的苹果示例代码
您还可以使用SDWebImage。这还会为您进行图像缓存。
#import <SDWebImage/UIImageView+WebCache.h>

...

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *MyIdentifier = @"MyIdentifier";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];

    if (cell == nil)
    {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                       reuseIdentifier:MyIdentifier] autorelease];
    }

    // Here we use the new provided setImageWithURL: method to load the web image
    [cell.imageView setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
                   placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

    return cell;
}

希望有所帮助!

3
除了使用缓存之外,您还可以考虑使用Grand Central Dispatch在后台加载图像。当单元格被加载时,放置UIActivityIndicator,然后在单独的线程中用图像替换它。
// get a data provider referencing the relevant file
CGDataProviderRef dataProvider = CGDataProviderCreateWithFilename(filename);

// use the data provider to get a CGImage; release the data provider
CGImageRef image = CGImageCreateWithPNGDataProvider(dataProvider, NULL, NO, 
                                                kCGRenderingIntentDefault);
CGDataProviderRelease(dataProvider);

// make a bitmap context of a suitable size to draw to, forcing decode
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
unsigned char *imageBuffer = (unsigned char *)malloc(width*height*4);

CGColorSpaceRef colourSpace = CGColorSpaceCreateDeviceRGB();

CGContextRef imageContext =
CGBitmapContextCreate(imageBuffer, width, height, 8, width*4, colourSpace,
              kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);

CGColorSpaceRelease(colourSpace);

// draw the image to the context, release it
CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), image);
CGImageRelease(image);

// now get an image ref from the context
CGImageRef outputImage = CGBitmapContextCreateImage(imageContext);

// post that off to the main thread, where you might do something like
// [UIImage imageWithCGImage:outputImage]
[self performSelectorOnMainThread:@selector(haveThisImage:) 
     withObject:[NSValue valueWithPointer:outputImage] waitUntilDone:YES];

// clean up
CGImageRelease(outputImage);
CGContextRelease(imageContext);
free(imageBuffer);

您也可以使用UIImage+ImmediateLoad.m,它可以立即解码图像。除了UIImage -initWithContentsOfFile:,-imageWithCGImage:和CG *之外,在iOS4之后,这些方法都是线程安全的。


2

我曾经遇到过类似的问题,我使用了 Profiler 进行了检查,以下是我的一些想法:

1)您正在错误地使用可重用标识符,您应该始终使用相同的标识符。

而不是这样:

NSString *CellIdentifier = [NSString stringWithFormat:@"Cell%d%d",indexPath.row,indexPath.section];

使用这个:

NSString *CellIdentifier = @"MyIdentifier";

更多阅读:如何使用UItableViewCell reuseIdentifier

2)loadNibNamed方法非常慢(而且你每次都在为每个单元格加载它),你应该摆脱nib,通过覆盖UITableViewCell的这个方法,从代码中放置UI元素。

-(id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier;

3) 使用Instruments


2

您正在尝试在与滚动同一的UI线程上加载图像。您应该在后台将它们加载到缓存中,然后在它们准备好显示时从缓存中获取。


2
您每个图像都在进行同步网络调用,这就是为什么您的应用程序会出现延迟。尝试使用 AsyncImageView,它将异步地从网络上下载您的图像。这样,您就可以消除应用程序中的延迟。 :-)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接