我有一个简单的iPhone应用程序,它从rss feed解析数据(标题,图像等)并显示在tableview中。
viewDidLoad有一个初始计数器值,可以通过调用fetchEntriesNew方法到达feed的第一页并加载到tableview中:
- (void)viewDidLoad
{
[super viewDidLoad];
counter = 1;
[self fetchEntriesNew:counter];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(dataSaved:)
name:@"DataSaved" object:nil];
}
- (void) fetchEntriesNew:(NSInteger )pageNumber
{
channel = [[TheFeedStore sharedStore] fetchWebService:pageNumber withCompletion:^(RSSChannel *obj, NSError *err){
if (!err) {
int currentItemCount = [[channel items] count];
channel = obj;
int newItemCount = [[channel items] count];
NSLog(@"Total Number Of Entries Are: %d", newItemCount);
counter = (newItemCount / 10) + 1;
NSLog(@"New Counter Should Be %d", counter);
int itemDelta = newItemCount - currentItemCount;
if (itemDelta > 0) {
NSMutableArray *rows = [NSMutableArray array];
for (int i = 0; i < itemDelta; i++) {
NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
[rows addObject:ip];
}
[[self tableView] insertRowsAtIndexPaths:rows withRowAnimation:UITableViewRowAnimationBottom];
[aiView stopAnimating];
}
}
}];
[[self tableView] reloadData];
}
当用户到达tableview的底部时,我使用以下内容到达Feed的下一页并加载到首先加载的第一页的底部:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height)
{
NSLog(@"Scroll End Called");
NSLog(@"New Counter NOW is %d", counter);
[self fetchEntriesNew:counter];
}
}
UPDATE2:以下是一个更容易理解的错误描述,我无法解决:例如,rss Feed的每个页面中有10个条目。应用程序启动,标题和其他标签立即加载,图像开始懒洋洋地加载,最后完成。到现在为止还挺好。用户滚动到达底部,到达底部将使用滚动委托方法,计数器从1增加到2,告诉fetchEntriesNew方法到达rss feed的第二页。程序将开始加载之前获取的前10个底部的下10个条目。这可以继续,程序将在每次用户滚动并到达底部时再获取10个条目,并且新行将被放置在先前获取的那些下面。到目前为止一切都很好。
现在让我们说当前第3页的用户已经完全加载了图像。由于第3页被完全加载,这意味着目前在tableview中有30个条目。用户现在滚动到底部,计数器增加,并且tableview开始填充前30个条目底部的rss feed的第4页的新行。标题快速填充,从而构建行,当图像被下载(尚未完全下载)时,用户再次快速移动到底部,而不是在第4页底部加载第5页,它将破坏第4页目前正在下载并开始再次加载第4个。
它应该做的是,当用户到达桌面视图的底部时,它应该保留下一页的标题等,无论前一页的图像是否处于下载过程中。
我的项目中的下载和持久数据没有问题,所有数据都在应用程序运行之间保持不变。
有人可以帮我指出正确的方向。提前谢谢。
更新3:根据@ Sergio的回答,这就是我的所作所为:
1)添加了对archiveRootObject的另一个调用[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];在[channelCopy addItemsFromChannel:obj];
之后此时,它不会一次又一次地破坏和重新加载同一批次,正是我想要的。但是,如果我多次滚动到达下一页而没有完全加载上一页的图像,则它不会保留图像。
2)我不知道如何在答案中解释如何使用Bool。这就是我做的:添加@property Bool myBool;在TheFeedStore中,合成它并在新添加的archiveRootObject:channelCopy之后将其设置为NO,并在fetchEntries方法的最开始在ListViewController中将其设置为YES。它不起作用。
3)我也意识到我处理整个问题的方式是性能不好。虽然我不知道如何使用缓存外的图像并将其作为缓存处理。您是否建议为图像使用单独的归档文件?非常感谢所有为解决我的问题做出贡献的人。
答案 0 :(得分:3)
如果您考虑this older question of yours和the solution I proposed,则可以理解您的问题。
具体来说,关键位与您持久保存信息(RSS信息+图像)的方式有关,即通过将整个channel
存档到磁盘上的文件:
[channelCopy addItemsFromChannel:obj];
[NSKeyedArchiver archiveRootObject:channelCopy toFile:pathOfCache];
现在,如果你看一下fetchEntriesNew:
,你在那里做的第一件事是摧毁你当前的频道。如果在频道被保存到磁盘之前发生这种情况,则会进入一种无限循环。
据我所知,您目前正在图片下载结束时保留您的频道(根据我的原始建议)。
您应该做的是在读取Feed之后和开始下载图像之前保留频道(当然,在图像下载结束时也应该保留它)。
所以,如果你从旧的要点中获取这个片段:
[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
if (!err) {
[channelCopy addItemsFromChannel:obj];
// ADDED
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_group_wait(obj.imageDownloadGroup, DISPATCH_TIME_FOREVER);
[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
});
}
block(channelCopy, err);
你应该做的是再增加一次archiveRootObject
电话:
[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
if (!err) {
[channelCopy addItemsFromChannel:obj];
[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
// ADDED
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_group_wait(obj.imageDownloadGroup, DISPATCH_TIME_FOREVER);
[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
});
}
block(channelCopy, err);
只要您没有足够快地滚动以便在读取Feed(没有图像)之前销毁通道,这将使工作正常工作。要解决此问题,您应该在致电TheFeedStore
时为YES
课程添加一个bool,并在执行新添加的fetchWebService
后立即重置。
这将解决您的问题。
我还要说,从设计/架构的角度来看,管理持久性的方式存在很大问题。实际上,您在磁盘上有一个文件,您使用archiveRootObject:channelCopy
原子编写。这种架构本质上是有风险的&#34;从多线程的角度来看,你还应该设法避免对共享存储的并发访问没有破坏性影响(例如:你将你的频道存档到第4页的磁盘,与第1页的图像同时存档已完全下载,因此您尝试将它们保存到同一文件中。)
图像处理的另一种方法是将图像存储在存档文件之外,并将它们视为一种缓存。这样可以解决并发问题,并且还可以消除每个页面两次存档通道所带来的性能损失(首次读取数据时以及后来图像进入时)。
希望这有帮助。
更新:
此时,它不会一次又一次地破坏和重新加载同一批次,正是我想要的。但是,如果我多次滚动到达下一页而没有完全加载上一页的图像,它就不会保留图像。
这正是我的意思,说您的架构(共享存档/并发访问)可能会导致问题。
您有几种选择:使用Core Data / sqlite;或者,更轻松地将每个图像存储在自己的文件中。在后一种情况下,您可以执行以下操作:
检索时,为每个图像分配一个文件名(这可以是提要条目的ID或序号或其他)并将图像数据存储在那里;
在档案中存储图像的URL和应存储的文件名;
当您需要访问图片时,您无法直接从存档字典中获取图片;相反,你从它获得文件名然后从磁盘读取文件(如果可用);
此更改不会影响您当前的rss /图像检索实现,但只会影响您保存图像的方式,并在需要时访问它们(我的意思是,这似乎很容易改变)。
2)我不知道如何在答案中解释如何使用Bool。
向TheFeedStore添加archiveRootObject
bool;
在您isDownloading
方法中将其设置为YES
,就在fetchWebService:
之前;
在第一次归档Feed后,在完成后将其设置为[connection start]
,然后传递给连接对象(再次在NO
中)(这是你已经在做的); < / p>
在您的fetchWebService:
中,一开始就执行:
scrollViewDidEndDecelerating:
以便在刷新过程中不刷新rss提要。
如果有帮助,请告诉我。
新更新:
让我简要介绍一下如何处理将图像存储在文件中。
在您的RSSItem类中,定义:
if ([TheFeedStore sharedStore].isDownloading)
return;
@property (nonatomic, readonly) UIImage *thumbnail;
@property (nonatomic, strong) NSString *thumbFile;
是托管图像的本地文件的路径。获得图片网址(thumbFile
)后,您可以获取,例如和MD5哈希值,并将其用作本地图片文件名:
getFirstImageUrl
(MD5String是您可以谷歌搜索的类别)。
然后,在NSString* imageURLString = [self getFirstImageUrl:someString];
....
self.thumbFile = [imageURLString MD5String];
中,您将在本地存储图像文件:
downloadThumbnails
现在,诀窍是,当您访问 NSMutableData *tempData = [NSData dataWithContentsOfURL:finalUrl];
[tempData writeToFile:[self cachedFileURLFromFileName:self.thumbFile] atomically:YES];
[[NSNotificationCenter defaultCenter] postNotificationName:@"DataSaved" object:nil];
属性时,您从文件中读取图像并将其返回:
thumbnail
在此代码段中,- (UIImage *)thumbnail
{
NSData* d = [NSData dataWithContentsOfURL:[self cachedFileURLFromFileName:self.thumbFile]];
return [[UIImage alloc] initWithData:d];
}
定义为:
cachedFileURLFromFileName:
当然,应该保留- (NSURL*)cachedFileURLFromFileName:(NSString*)filename {
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSArray *fileArray = [fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask];
NSURL* cacheURL = (NSURL*)[fileArray lastObject];
if(cacheURL)
{
return [cacheURL URLByAppendingPathComponent:filename];
}
return nil;
}
以使其正常工作。
如你所见,这种方法非常简单&#34;实施。这不是一个优化的解决方案,只是让您的应用程序使用其当前架构的快速方法。
为了完整性,thumbFile
类别:
MD5String
答案 1 :(得分:2)
您实际上要做的是在UITableView
现在这非常简单,最好的办法是在UITableView
委托cellForRowAtIndexPath
方法中实施分页,而不是在UIScrollView
scrollViewDidEndDecelerating
委托方法上执行此操作
这是我的分页实现,我相信它也应该适合你:
首先,我有一个与分页相关的实现常量:
//paging step size (how many items we get each time)
#define kPageStep 30
//auto paging offset (this means when we reach offset autopaging kicks in, i.e. 10 items before the end of list)
#define kPageBegin 10
我这样做的原因是可以轻松更改.m文件中的分页参数。
以下是我的分页方式:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSUInteger row = [indexPath row];
int section = indexPath.section-1;
while (section>=0) {
row+= [self.tableView numberOfRowsInSection:section];
section--;
}
if (row+kPageBegin>=currentItems && !isLoadingNewItems && currentItems+1<maxItems) {
//begin request
[self LoadMoreItems];
}
......
}
currentItems
是一个整数,其中包含tableView数据源当前项的数量。
isLoadingNewItems
是一个布尔值,用于标记此时是否正在提取项目,因此在我们从服务器加载下一批时,我们不会实例化另一个请求。
maxItems
是一个整数,表示何时停止分页,是我从服务器检索并在初始请求中设置的值。
如果您不想限制,可以省略maxItems检查。
在我的分页加载代码中,我将isLoadingNewItems
标志设置为true,并在从服务器检索数据后将其设置为false。
所以在你的情况下,这看起来像是:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSUInteger row = [indexPath row];
int section = indexPath.section-1;
while (section>=0) {
row+= [self.tableView numberOfRowsInSection:section];
section--;
}
if (row+kPageBegin>=counter && !isDowloading) {
//begin request
isDowloading = YES;
[self fetchEntriesNew:counter];
}
......
}
此外,添加新行后无需重新加载整个表格。 只需使用:
for (int i = 0; i < itemDelta; i++) {
NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
[rows addObject:ip];
}
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:rows withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
答案 2 :(得分:1)
简单的BOOL
足以避免重复调用:
BOOL isDowloading;
下载完成后,将其设置为NO
。当它进入这里时:
if (endScrolling >= scrollView.contentSize.height)
{
NSLog(@"Scroll End Called");
NSLog(@"New Counter NOW is %d", counter);
[self fetchEntriesNew:counter];
}
把它放到YES
。当请求失败时,也不要忘记将其设置为NO
。
修改1:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height)
{
if(!isDowloading)
{
isDownloading = YES;
NSLog(@"Scroll End Called");
NSLog(@"New Counter NOW is %d", counter);
[self fetchEntriesNew:counter];
}
}
}
完成提取后,只需再次将其设置为NO
。