2

我有一个简单的 iPhone 应用程序,它从 rss 提要解析数据(标题、图像等)并显示在 tableview 中。

viewDidLoad 有一个初始计数器值,通过调用 fetchEntriesNew 方法到达提要的第一页并加载到 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];
}

当用户到达表格视图的底部时,我正在使用以下内容到达提要的下一页并在首先加载的第一页的底部加载:

- (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 提要的每一页都有 10 个条目。应用程序启动,标题和其他标签立即加载,图像开始延迟加载,最后完成。到目前为止,一切都很好。用户滚动到底部,到达底部将使用滚动委托方法,计数器从 1 增加到 2,告诉 fetchEntriesNew 方法到达 rss 提要的第二页。该程序将开始在之前获取的前 10 个条目的底部加载接下来的 10 个条目。这可以继续,每次用户滚动并到达底部时,程序将再获取 10 个条目,并且新行将放置在先前获取的行下方。到目前为止,一切都很好。

现在让我们说用户当前在页面 3 上,该页面已经完全加载了图像。由于第 3 页已完全加载,这意味着当前表格视图中有 30 个条目。用户现在滚动到底部,计数器增加,并且 tableview 开始从前 30 个条目底部的 rss 提要的第 4 页填充新行。标题快速填充,从而构建行,当图像被下载(尚未完全下载)时,用户再次快速移动到底部,而不是在第 4 页底部加载第 5 页,它将破坏第 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)我也意识到我处理整个问题的方式是性能不是更好。虽然我不知道如何在缓存之外使用图像并将它们作为缓存处理。您是否建议对图像使用单独的存档文件?

非常感谢所有为解决我的问题做出贡献的人。

4

3 回答 3

3

如果您考虑您的这个较旧的问题我提出的解决方案,您的问题可以理解。

具体来说,关键点与您保存信息(RSS 信息 + 图像)的方式有关,即通过将整个信息归档channel到磁盘上的文件:

        [channelCopy addItemsFromChannel:obj];
        [NSKeyedArchiver archiveRootObject:channelCopy toFile:pathOfCache];

现在,如果您查看fetchEntriesNew:,您在那里做的第一件事就是破坏您当前的频道。如果这种情况发生在通道被持久化到磁盘之前,您将进入一种无限循环。

我了解您目前在图片下载的最后阶段保留您的频道(根据我最初的建议)。

您应该做的是在读取提要之后和开始下载图像之前保留通道(当然,您也应该在图像下载结束时保留它)。

因此,如果您从我的旧要点中获取此片段:

[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);

只要您滚动速度不够快,以至于在读取提要(没有图像)之前通道被破坏,这将使事情正常进行。要解决此问题,您应该在您的类中添加一个 bool,您在调用时TheFeedStore设置为该类,并在执行新添加的.YESfetchWebServicearchiveRootObject:channelCopy

这将解决您的问题。

我还要说,从设计/架构的角度来看,管理持久性的方式存在很大问题。实际上,您在磁盘上有一个文件,您可以使用archiveRootObject. 从多线程的角度来看,这种架构本质上是“有风险的”,您还应该设计一种方法来避免对共享存储的并发访问没有破坏性影响(例如:您同时将第 4 页的通道存档到磁盘第 1 页的图像已完全下载的时间,因此您尝试将它们也保存到同一个文件中)。

图像处理的另一种方法是将图像存储在存档文件之外,并将它们视为一种缓存。这将解决并发问题,并且还将消除因为每个页面两次归档通道而导致的性能损失(当第一次读取提要时,然后当图像进入时)。

希望这可以帮助。

更新:

在这一点上,它不会一次又一次地破坏和重新加载同一批次,这正是我想要的。但是,如果我多次滚动以到达下一页而没有完全加载上一页的图像,它不会保留图像。

这正是我的意思是说您的架构(共享存档/并发访问)可能会导致问题。

你有几个选择:使用 Core Data/sqlite;或者,更容易的是,将每个图像存储在其自己的文件中。在后一种情况下,您可以执行以下操作:

  1. 在检索时,为每个图像分配一个文件名(这可以是提要条目的 id 或序列号或其他)并将图像数据存储在那里;

  2. 将图像的 URL 和应存储的文件名存储在存档中;

  3. 当您需要访问图像时,您不会直接从存档字典中获取它;相反,您从中获取文件名,然后从磁盘读取文件(如果可用);

  4. 此更改不会影响您当前的 rss/图像检索实现,但只会影响您保留图像并在需要时访问它们的方式(我的意思是,这似乎是一个非常容易的更改)。

2)我不确定如何使用 Bool,正如他在答案中解释的那样。

  1. isDownloading向 TheFeedStore添加一个布尔值;

  2. YES在你的方法中设置它fetchWebService:,就在做之前[connection start]

  3. 在第一次归档提要后立即将其设置为NO在传递给连接对象的完成块中(再次在 中fetchWebService:)(您已经在这样做);

  4. 在你的scrollViewDidEndDecelerating:, 一开始,做:

        if ([TheFeedStore sharedStore].isDownloading)
            return;
    

    这样您就不会在刷新时刷新 rss 提要。

让我知道这是否有帮助。

新更新:

让我概述一下如何处理将图像存储在文件中。

在您的 RSSItem 类中,定义:

@property (nonatomic, readonly) UIImage *thumbnail;
@property (nonatomic, strong) NSString *thumbFile;

thumbFile是托管图像的本地文件的路径。获得图像 URL ( getFirstImageUrl) 后,您可以获取它的例如 MD5 哈希值,并将其用作本地图像文件名:

NSString* imageURLString = [self getFirstImageUrl:someString];
....
self.thumbFile = [imageURLString MD5String];

(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;
}

当然,thumbFile应该坚持这个工作。

如您所见,这种方法非常“容易”实施。这不是一个优化的解决方案,只是一种让您的应用程序在其当前架构下工作的快速方法。

为了完整起见,MD5String类别:

@interface NSString (MD5)

- (NSString *)MD5String;

@end

@implementation NSString (MD5)

- (NSString *)MD5String {
const char *cstr = [self UTF8String];
unsigned char result[16];
CC_MD5(cstr, strlen(cstr), result);

return [NSString stringWithFormat:
        @"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
        result[0], result[1], result[2], result[3],
        result[4], result[5], result[6], result[7],
        result[8], result[9], result[10], result[11],
        result[12], result[13], result[14], result[15]
        ];  
}

@end
于 2013-04-24T19:14:29.197 回答
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];
于 2013-04-24T23:17:54.887 回答
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重新设置为。

于 2013-04-22T09:37:53.040 回答