我的iPad应用程序具有从Feed中填充的UITableView。与大多数RSS阅读器一样,它以反向时间顺序显示博客帖子的链接列表,其中包含标题和每个帖子的摘要。 Feed经常更新,并且非常大,大约有500个帖子。我正在使用libxml2 push parsing在NSOperation子类中有效地下载和解析feed,构建入口对象并随时更新数据库。但后来我需要更新UITableView。
到目前为止,应用程序一直在为解析的每个帖子更新UITableView。解析器在主线程上执行选择器来完成这项工作。但是如果需要更新很多细胞,这会导致几秒钟的严重滞后。我可以通过在后台线程上运行表更新来缓解这种情况,但似乎这是not a good idea。所以现在我想弄清楚如何在主线程上更有效地更新表。
我可以在解析所有帖子时调用reloadData
,但它不是非常用户友好:没有动画来指示任何内容已经发生变化,只是闪存并且新数据就在那里。我宁愿让它动画显示添加新帖子和删除旧帖子。未从Feed中删除的现有帖子应该通过顶部显示的新帖子向下推送。
我知道这是可能的。举个例子,Byline做得很漂亮。每个帖子一次一个地添加或删除UITableView,没有显示表格背景的间隙。所有这一切都没有让UI在一点也不响应。怎么做?
我最近的尝试是只在解析了所有帖子之后更新表(解析器非常快,所以它没有太大的延迟)。然后,它将现有帖子加载到NSDictionary中,将其ID映射到用作表数据源的数组中的索引。然后迭代遍历新解析的帖子数组中的每个对象,为每个对象添加NSIndexPath,稍后传递给-insertRowsAtIndexPaths:withRowAnimation:
,-deleteRowsAtIndexPaths:withRowAnimation:
和-reloadRowsAtIndexPaths:withRowAnimation:
,以便插入,删除,移动或更新单元格。对于500个帖子,这需要大约4秒才能更新,UI完全没有响应。那个时间几乎专门用于UITableView动画更新;迭代两个帖子数组只需要很少的时间。
然后我对其进行了修改,以便更新而不使用动画,并且我有单独的数组来插入/删除/重新加载动画,仅适用于与当前可见行对应的行位置。这样做会更好,但是会删除帖子并添加新帖子。
对不起,这是如此啰嗦,但这是结果:
如何更新UITableView,推送新单元格,推送其他单元格,还有其他人从一个位置移动到另一个位置,UITableView中最多有500个单元格(一次可见6-8个),以及每个动画按顺序发生,而UI仍然完全响应?
答案 0 :(得分:33)
这个问题实际上有三个答案。也就是说,这个问题有三个部分:
为了解决第一个问题,我现在确保在主事件循环的每次迭代中只能传递一个表更新消息。这可以防止主线程被锁定,如果后台线程正在以比它可以处理它更快的速度进行操作。
这要归功于Byline作者Milo Bird发送给我的示例代码,然后我将其整合到Dave Dribin的DDInvocationGrabber中。这个接口使得在主事件循环的下一个可用迭代上调用方法变得非常容易:
[[(id)delegate queueOnMainThread]
parserParsedEntries:parsedEntries
inPortal:parsedPortal];
我非常喜欢使用这种方法是多么容易。解析器现在使用它来调用所有委托方法,其中大多数方法都会更新UI。我已发布此代码on GitHub。
至于性能,我最初一次更新一个UITableView行。这是有效的,但效率低下。我回去研究了XMLPerformance示例,在那里我注意到解析器在调度到主线程以更新表之前一直等到收集了10个项目。这是保持性能提升的关键,而不是通过一次更新所有500行来锁定UI。我在一次调用中更新了1个,10个和所有500行,而更新10似乎提供了性能和UI锁定之间的最佳权衡。五个人也可能工作得很好。
最后,还有动画。观看“Mastering Table Views” WWDC 2010会话,我意识到我使用deleteRowsAtIndexPaths:withRowAnimation:
和updateRowsAtIndexPaths:withRowAnimation:
方法是错误的。我一直在跟踪表格中应该添加和删除的内容,并根据需要调整索引,但事实证明这不是必需的。在表更新块内部,只需要在更新之前引用中的行的索引,而不管可以插入或删除多少来更改其位置。显然,更新块会为您完成所有簿记。 (转到视频中关于关键示例的8:45标记)。
因此,更新表的解析器传递给它的条目的委托方法(当前每次10次)现在明确地跟踪要从更新或删除的行的位置更新块,如下所示:
NSMutableDictionary *oldIndexFor = [NSMutableDictionary dictionaryWithCapacity:posts.count];
int i = 0;
for (PostModel *e in posts) {
[oldIndexFor setObject:[NSNumber numberWithInt:i++] forKey:e.ident];
}
NSMutableArray *insertPaths = [NSMutableArray array];
NSMutableArray *deletePaths = [NSMutableArray array];
NSMutableArray *reloadPaths = [NSMutableArray array];
BOOL modified = NO;
for (PostModel *entry in entries) {
NSNumber *num = [oldIndexFor objectForKey:entry.ident];
NSIndexPath *path = [NSIndexPath indexPathForRow:currentPostIndex inSection:0];
if (num == nil) {
modified = YES;
[insertPaths addObject:path];
[posts insertObject:entry atIndex:currentPostIndex];
} else {
// Find its current position in the array.
NSUInteger foundAt = [posts indexOfObject:entry];
if (foundAt == currentPostIndex) {
// Reload it if it has changed.
if (entry.savedState != PostModelSavedStateUnmodified) {
modified = YES;
[posts replaceObjectAtIndex:foundAt withObject:entry];
[reloadPaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
}
} else {
// Move it.
modified = YES;
[posts removeObjectAtIndex:foundAt];
[posts insertObject:entry atIndex:currentPostIndex];
[insertPaths addObject:path];
[deletePaths addObject:[NSIndexPath indexPathForRow:num.intValue inSection:0]];
}
}
currentPostIndex++;
}
if (modified) {
[tableView beginUpdates];
[tableView insertRowsAtIndexPaths:insertPaths withRowAnimation:UITableViewRowAnimationTop];
[tableView deleteRowsAtIndexPaths:deletePaths withRowAnimation:UITableViewRowAnimationBottom];
[tableView reloadRowsAtIndexPaths:reloadPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
}
欢迎评论。完全有可能有更有效的方法来做到这一点(使用-[NSArray indexOfObject:]
对我来说特别可疑),而且我可能已经错过了其他一些微妙之处。
但即便如此,这对我的应用来说也是一个巨大的进步。现在,用户界面在同步过程中保持(大部分)响应,同步速度很快,而表格更新动画看起来恰到好处。
答案 1 :(得分:-1)
您是否尝试过[tableView beginUpdates];
和[tableView endUpdate];
?