使用带有NSThread的Singleton同步数组

时间:2011-02-27 12:36:37

标签: iphone objective-c cocoa-touch nsxmlparser uisearchbar

我有一个带有UISearchBar的书籍应用程序,用户可以在其中键入任何书名并在下面输入搜索结果(来自ext API调用)。

我在我的应用程序中使用了一个名为retrieveArray的单例变量,它存储了所有书籍。

@interface Shared : NSObject {
    NSMutableArray *books;
}

@property (nonatomic, retain) NSMutableArray *books;

+ (id)sharedManager;

@end

使用NSMutableArray * retrieveArray在多个.m文件中访问它; ...在头文件中

retrievedArray = [[Shared sharedManager] books];

我的问题是如何确保ret​​rieveArray中的值在所有类中保持同步。

实际上,retrieveArray中的值是通过NSXMLParser(即通过外部Web服务API)添加的。有一个单独的XMLParser.m文件,我在那里进行所有解析并填充数组。解析是在一个单独的线程上完成的。

    - (void) run: (id) param  {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

        NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL: [self URL]];
        [parser setDelegate: self];
    [parser parse];
        [parser release];

        NSString *tmpURLStr = [[self URL]absoluteString];

        NSRange range_srch_book = [tmpURLStr rangeOfString:@"v1/books"];

        if (range_srch_book.location != NSNotFound) 
            [delegate performSelectorOnMainThread:@selector(parseDidComplete_srch_book) withObject:nil waitUntilDone:YES];

        [pool release];
    } 


    - (void) parseXMLFile: (NSURL *) url
    {   
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        [self setURL: url];
        NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                                     selector:@selector(run:)


object: nil];
    [retrievedArray removeAllObjects];
    [myThread start];
    [pool release];
}

如果用户输入速度非常快,似乎存在一些同步问题(如果用户输入速度很慢,它似乎工作正常)....所以有2个视图,其中此共享数组项中的对象的内容被展示;清单和细节。 如果用户快速键入并在列表视图中单击A,则会在详细视图中显示B ...这是主要问题。

我已经尝试过所有我能想到的解决方案,但仍然无法解决问题。

编辑同步问题示例: 在列表视图中,如果显示3个项目,例如Item1,Item2和Item3,如果用户点击Item2,他将在详细视图中显示Item3(即说明没有正确的详细信息)

下面是单击列表视图中的项目时执行的代码;

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // Navigation logic -- create and push a new view controller

    if(bookdetailCustom == nil)
        bookdetailCustom = [[BookDetailCustom alloc] initWithNibName:@"BookDetailCustom" bundle:[NSBundle mainBundle]];

    //aBook = [retrievedArray objectAtIndex:indexPath.row];

    bookdetailCustom.selectedIndex = indexPath.row;

    [self.navigationController pushViewController:bookdetailCustom animated:YES];
    [bookdetailCustom release];
    bookdetailCustom = nil;
}

以下是searchTabkleView的外观

- (void) searchTableView {
    NSString *searchText = searchBar.text;
    NSMutableArray *searchArray = [[NSMutableArray alloc] init];

    for (int i=0;i<[retrievedArray count];i++)
    {
        Stock *aBookTemp = [retrievedArray objectAtIndex:i];
        NSString *temp = [aBookTemp valueForKey:@"BookName"];
        [searchArray addObject:temp];
    }

    for (NSString *sTemp in searchArray)
    {
        NSRange titleResultsRange = [sTemp rangeOfString:searchText options:NSCaseInsensitiveSearch];

        if (titleResultsRange.length > 0)
            [copyListOfItems addObject:sTemp];
    }

    [searchArray release];
    searchArray = nil;
}

请建议一些合适的修补程序。

5 个答案:

答案 0 :(得分:5)

根据您发布的内容,每个retrieveArray都指向同一个NSMutableArray对象。所以没有任何单独的数组可以保持同步,它们都是相同的数组。

但是,NSMutableArray不是线程安全的;如果一个线程正在改变它而另一个线程正在读取它,事情可能会爆炸。简单地将属性从非原子更改为原子是不够的,因为它只包括获取数组对象本身而不是后续方法调用来访问数组内的元素。但是,我不认为这会导致您的主要问题,并且修复此问题应该可以避免线程安全问题。

我猜事件的顺序是这样的:

  1. 列表视图显示一组结果,其中包括索引为N的<。
  2. 用户输入内容。 XML解析器开始逐步更新共享数组。列表视图尚未更新。
  3. 用户在列表视图中触摸索引N处的项目。列表视图指示“详细信息”视图显示索引为N的项目。
  4. 详细信息视图从共享阵列中提取索引N处的项目,但由于在步骤2中开始更新,索引N现在包含B.详细信息视图显示。
  5. 在某些时候XML解析完成,现在List已更新。
  6. 如果来自Web服务的加载和解析足够慢,那么第4步也应该可以简单地使用NSRangeException崩溃。

    一个解决方案是List中的每个项目都可以保存实际的结果对象并将其传递给Detail视图而不仅仅是索引。在这种情况下,如果List和Detail是唯一的使用者,或者可以更改任何其他使用者以相同的方式获取对象而不是索引,则可能可以完全摆脱共享数组。另一个是解析器将结果累积到私有数组中,并在发出List视图信号以更新自身之前立即更新共享数组;在后台线程的更新和主线程上的方法调用之间的时间内仍有一点可能性,但窗口可能相当小。

    或者我猜测更新的工作方式可能完全错误,在这种情况下,您应该提供更多详细信息。

答案 1 :(得分:2)

我最初建议您从属性声明中删除nonatomic关键字。 Atomic是默认设置(没有atomic设置,省略nonatomic就足够了) - 它将通过将合成的setter包装在@synchronize块中来处理线程安全。

不幸的是,许多人只是在没有真正了解它的情况下将nonatomic全部放在他们的代码上。我一直以为这是来自Apple示例代码的复制/粘贴 - 他们经常将它用于与UI相关的东西 - 记住UIKit不是线程安全的。

Anomie在他/她的回答中指出,这不是 - 可能 - 因为你正在改变来自不同线程的可变数组。 听起来像是对我的正确答案 - 我会删除我的答案,但我会留在这里,因为我认为我的评论值得(但与你的问题没有100%相关)。

答案 2 :(得分:1)

据我所知,您已投入大量时间和精力来解决这个问题,Anomie的解决方案是最佳选择。但也许一种不同的方法可能更容易实现。

例如,您可以让解析器处理数据并将其提供给Core Data存储。该列表将由NSFetchedResultsController提供。控制器自动处理表格内容以及需要完成的任何同步。

值得尝试,我希望它有所帮助。

答案 3 :(得分:0)

尝试在数组的访问器中使用NSRecursiveLock。

请参阅NSRecursiveLock文档。从概述:

  

NSRecursiveLock定义了一个锁,该锁可以被同一个线程多次获取而不会导致死锁,这种情况是线程被永久阻塞,等待自己放弃锁。当锁定线程具有一个或多个锁时,所有其他线程都被阻止访问受锁保护的代码。

CoreVideo示例代码包含正确使用的示例。

答案 4 :(得分:0)

问题是两个线程正在引用retrievedArray。从XML解析代码中删除对retrievedArray的所有引用,只在主线程上更改它。

这是过程:

  1. 更改parseXMLFile:以创建新阵列:parsedArray = [NSMutableArray array]
  2. 更改parser:didEndElement:以附加到此新阵列:[parsedArray addObject:aBook]
  3. parser:didEndDocument:中将新数组传递给主线程:

    [delegate performSelectorOnMainThread: @selector(updateRetrievedArray:)
                               withObject: parsedArray
                            waitUntilDone: NO];
    
  4. 在主线程上运行的
  5. updateRetrievedArray:将是负责更新retrievedArray的代码 - 这样只有一个线程更改此对象:

    - (void) updateRetrievedArray: (NSArray *)parsedArray {
        [retrievedArray setArray:parsedArray];
        [self parseDidComplete_srch_book]; // Be sure to call [tableView reloadData]
    }