我有一个UITableView,我用图像数据填充它。此数据加载正常,但当记录数量增加(例如超过50)时,应用程序开始出现冻结等问题。我知道这是导致问题的我的cellForRowAtIndexPath中的一行:
NSData* data = [DKStoreManager loadFileWithName:[finalArray objectAtIndex:indexPath.row] forFolderNumber:[_FolderCode intValue] forUser:[_PassedUserID intValue] andType:_FolderType];
NSDictionary *myDictionary = (NSDictionary*) [NSKeyedUnarchiver unarchiveObjectWithData:data];
我理解它是因为当我在viewDidLoad中只加载一次数据时使用的myDictionary作为全局变量,然后所有单元格在逻辑上相同,但表格滚动得很好并且应用程序没有崩溃。 finalArray是一个数组,其中的文件名按字母顺序排序,行数对应于其计数。任何人都可以建议一种方法在cellForRowAtIndexPath方法之外加载这些数据吗?如果所有NSData都不同,我如何将所有内容传递给cellForRowAtIndexPath?
我试图做的事情:
1)我尝试将UITableViewCells子类化并从方法加载数据:
cell.FileName = [finalArray objectAtIndex:indexPath.row];
cell.PassedUserID = _PassedUserID;
cell.FolderCode = _FolderCode;
cell.FolderType = _FolderType;
[cell loadContents];
我确保使用BOOL,即loadContents在子类中只运行一次。当我向下或向上滚动时,单元格会改变位置。一团糟......
2)我注意到如果我删除了 if(cell == nil){ 并且停止重复使用单元格,单元格更改位置没有问题,但是存在大量加载时间问题
2)移动if(cell == nil){方法中的所有内容,单元格仍然在滚动时改变位置但滚动速度更快......
4)加载viewDidLoad中的所有数据,显示" loading ..."但是负载非常慢,并没有真正发挥作用。
5)使用dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^ {对数据加载,但不起作用,表格滚动真的很慢......
PS,myDictionary包含图像和单元格的名称。
编辑:
所有数据都保存在本地,方法" loadFileWithName"将已保存的文件加载到文档目录中。
答案 0 :(得分:3)
在tableView
上加载大量单元格不是显示数据的好方法。假设一个用户想要查看你的数据,他会看看你的第一个元素(假设最多20个单元格),为什么他应该拖动才能看到元素一直向下?如果他想要更多,他只会下降并点击加载更多。用户不想等待并将所有数据加载到内存(RAM)。
对我来说,最好的方法是在你加载元素的底部时添加加载更多功能(也称为无限滚动)('和网站上的分页一样)。
有一个很棒的库来实现这个名为SVPullToRefresh的东西。该库允许您通过向下拖动tableView来触发一个方法( completionHandler ),您应该在tableView的末尾加载更多数据。
//This is where we add infinite scrolling to the tableView
__weak YourViewController *weakSelf = self;
[self.tableView addInfiniteScrollingWithActionHandler:^{
[weakSelf loadMoreData];
[weakSelf.tableView reloadData];
}];
现在我们添加了更多的负载,我们需要设置我们的数据。首先我们将NSInteger *lastIndex;
声明为实例变量,在viewWillAppear
中我们用0实例化它,并调用loadMoreData,它将为lastIndex赋值20
- (void)viewWillAppear:(BOOL)animated {
lastIndex = 0;
[self loadMoreData];
}
这是处理从Array到tableView的数据的方法。在这里,我们描述了一个示例,它可以获得更多按下的20个元素。
-(void)loadMoreData {
//Lets suppose we are loading 20 elemets each time
//Here we control if we have elements on array
if(yourDataArray.count - lastIndex>0) {
//We control if we have 20 or more so we could load all 20 elements
if(yourDataArray.count - lastIndex>=20) {
lastIndex += 20;
} else {
//Our last index is the last element on array
lastIndex = yourDataArray.count - lastIndex;
}
} else {
//We have loaded all the elements on the array so alert the user
}
}
在numberOfRowsInSection
中,我们返回lastIndex
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return lastIndex;
}
要处理图片,您应该使用SDWebImage作为建议的其他答案。通过这种方式处理滚动问题,您可以在第一次看到时在窗口上添加大量数据。也许它不是处理你的想法的正确答案(如果你真的需要将所有数据加载到tableView),但它告诉你如何避免加载数据,以便在滚动到tableView时提供性能。
p.s我在我的社交网络应用程序中使用此示例,它工作得很好。 (此外,Instagram,Facebook和其他应用程序正在处理以这种方式加载数据。)
希望有所帮助:)
答案 1 :(得分:3)
请参阅下面的延迟加载表的示例。
此示例提供了包含无限数量单元格的表视图;仅当满足以下所有条件时,才会在后台线程中加载每个单元格的数据:
即,消除了快速滚动期间系统的过度负载,并且仅为用户真正需要查看的单元加载数据。
在此示例中,为每个单元模拟耗时和线程阻塞的数据加载操作,并使后台线程休眠0.2秒。要在您的实际应用程序中使用此示例,请执行以下操作:
tableElementPlaceholder
getter; performActualFetchTableCellDataForIndexPaths:
方法中,将以下行替换为实际的加载单元数据:
[NSThread sleepForTimeInterval:0.2]; // emulation of time-consuming and thread-blocking operation
tableView:cellForRowAtIndexPath:
方法的实现。atomic
属性,可能应该使用NSLock
您的耗时线程阻止代码替换performActualFetchTableCellDataForIndexPaths:
调用的[NSThread sleepForTimeInterval:0.2]
方法内部。
您可以从here下载完整的Xcode项目。
#import "ViewController.h"
#import "TableViewCell.h"
static const NSUInteger kTableSizeIncrement = 20;
@interface ViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) NSMutableArray* tableData;
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (nonatomic, readonly) id tableElementPlaceholder;
@property (nonatomic, strong) NSTimer* tableDataLoadDelayTimer;
- (void)fetchTableCellDataForIndexPath:(NSIndexPath*)indexPath;
- (void)performActualFetchTableCellDataForIndexPaths:(NSArray*)indexPaths;
- (void)tableDataLoadDelayTimerFired:(NSTimer*)timer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableData = [NSMutableArray arrayWithCapacity:kTableSizeIncrement];
for (NSUInteger i = 0; i < kTableSizeIncrement; i++) {
[self.tableData addObject:self.tableElementPlaceholder];
}
// Do any additional setup after loading the view, typically from a nib.
}
- (id)tableElementPlaceholder
{
return @"";
}
- (void)fetchTableCellDataForIndexPath:(NSIndexPath*)indexPath
{
if (self.tableView.decelerating && !self.tableView.tracking) {
if (self.tableDataLoadDelayTimer != nil) {
[self.tableDataLoadDelayTimer invalidate];
}
self.tableDataLoadDelayTimer =
[NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:@selector(tableDataLoadDelayTimerFired:)
userInfo:nil
repeats:NO];
} else {
[self performActualFetchTableCellDataForIndexPaths:@[indexPath]];
}
}
- (void)tableDataLoadDelayTimerFired:(NSTimer*)timer
{
[self.tableDataLoadDelayTimer invalidate];
self.tableDataLoadDelayTimer = nil;
NSArray* indexPathsForVisibleRows = [self.tableView indexPathsForVisibleRows];
[self performActualFetchTableCellDataForIndexPaths:indexPathsForVisibleRows];
}
- (void)performActualFetchTableCellDataForIndexPaths:(NSArray*)indexPaths
{
for (NSIndexPath* indexPath in indexPaths) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[NSThread sleepForTimeInterval:0.2]; // emulation of time-consuming and thread-blocking operation
NSString* value = [NSString stringWithFormat:@"Text at cell #%ld", (long)indexPath.row];
dispatch_async(dispatch_get_main_queue(), ^{
self.tableData[indexPath.row] = value;
[self.tableView reloadRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
});
});
}
}
#pragma mark UITableViewDataSource protocol
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.tableData.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == (self.tableData.count - 1)) {
for (NSUInteger i = 0; i < 20; i++) {
[self.tableData addObject:self.tableElementPlaceholder];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TableViewCell" forIndexPath:indexPath];
NSString* text = [self.tableData objectAtIndex:indexPath.row];
if (text.length == 0) {
cell.activityIndicator.hidden = NO;
[cell.activityIndicator startAnimating];
cell.label.hidden = YES;
[self fetchTableCellDataForIndexPath:indexPath];
} else {
[cell.activityIndicator stopAnimating];
cell.activityIndicator.hidden = YES;
cell.label.hidden = NO;
cell.label.text = text;
}
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
@end
对于您的项目,performActualFetchTableCellDataForIndexPaths:
方法如下所示:
- (void)performActualFetchTableCellDataForIndexPaths:(NSArray*)indexPaths
{
for (NSIndexPath* indexPath in indexPaths) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData* data = [DKStoreManager loadFileWithName:[finalArray objectAtIndex:indexPath.row] forFolderNumber:[self.FolderCode intValue] forUser:[self.PassedUserID intValue] andType:self.FolderType];
NSDictionary *myDictionary = (NSDictionary*) [NSKeyedUnarchiver unarchiveObjectWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
self.tableData[indexPath.row] = myDictionary;
[self.tableView reloadRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
});
});
}
}
请注意,您需要使用原子属性self.FolderCode
和self.PassedUserID
而不是实例变量_FolderCode
和_PassedUserID
,因为正在加载文件在一个单独的线程中执行,你需要使这个数据线程安全。
对于tableElementPlaceholder
方法,它可能如下所示:
- (id)tableElementPlaceholder
{
return [NSNull null];
}
相应地,在tableView: cellForRowAtIndexPath:
方法中检查数据加载是否完成如下:
NSObject* data = [self.tableData objectAtIndex:indexPath.row];
if (data == [NSNull null]) {
cell.activityIndicator.hidden = NO;
[cell.activityIndicator startAnimating];
cell.label.hidden = YES;
[self fetchTableCellDataForIndexPath:indexPath];
} else if ([data isKindOfClass:[NSData class]]) {
[cell.activityIndicator stopAnimating];
cell.activityIndicator.hidden = YES;
NSData* actualData = (NSData*)data;
// Initialize your cell with actualData...
}
答案 2 :(得分:1)
在过去的几年里,我见过类似的方法,但都错了。如果我是你,我会使用两种不同的方法:
1)如果从远程服务器加载图像,请使用名为SDWebImage
的优秀库或编写自己的库。它会变得像[self.imgViewBack setImageWithURL:[NSURL URLWithString:data.eventImageURL];
2)您也可以将所有图片保存到NSDocumentDirectory
,然后存储在那里 - 只需使用NSDictionary
个路径。
我的一般建议如下:永远不要创建大小超过几千字节的对象的原始数据的数组或字典。这将导致整个地方出现可怕的冻结和记忆浪费。始终使用对象的路径并将其存储在其他地方 - 您将受益。
通常cellForRowAtIndexPath
方法是UITableViewDataSource
协议中最常用的方法,因此它应该以闪电般的速度执行。
答案 3 :(得分:0)
如果您必须在本地存储所有内容而不是像SergiusGee所说的那样存储在服务器上,我建议您在内存中仅保留50张图像。
有tableView:willDisplayCell:forRowAtIndexPath:
委托方法,您可以控制哪些元素被加载。
例如,当显示第50个单元格时,您有索引30-70之间的图像。在每10个单元格之后,您可以卸载不需要的单元格并重新加载新单元格以保持[x-20, x+30]
范围,其中x是当前单元格。
显然,这需要付出相当大的努力才能实现,但加载所有内容并将其保存在内存中肯定不是答案。
此外,这些数字只是为了举例。
答案 4 :(得分:-1)
请勿从-cellForRowAtIndexPath:
中的文件加载数据。
尝试同步或异步(由您自己决定)将它们加载到UIImage
-viewDidLoad:
数组中,因为UIImage
不需要花费太多时间但加载原始数据以构建UIImage