【问题标题】:Using Singleton synchronized array with NSThread使用带有 NSThread 的 Singleton 同步数组
【发布时间】:2011-07-05 05:31:36
【问题描述】:

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

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

@interface Shared : NSObject {
    NSMutableArray *books;
}

@property (nonatomic, retain) NSMutableArray *books;

+ (id)sharedManager;

@end

这是使用 NSMutableArray *retrivedArray 在多个 .m 文件中访问的; ...在头文件中

retrievedArray = [[Shared sharedManager] books];

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

实际上,retrievedArray 中的值是通过 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;
}

请提出一些合适的修复建议。

【问题讨论】:

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


    【解决方案1】:

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

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

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

    1. 列表视图正在显示一组结果,其中包括索引 N 处的 A。
    2. 用户键入内容。 XML 解析器开始以增量方式更新共享数组。列表视图尚未更新。
    3. 用户在列表视图中触摸索引 N 处的项目。列表视图指示详细视图显示索引 N 处的项目。
    4. 详细视图从共享数组中提取索引 N 处的项目,但由于在第 2 步中开始更新,索引 N 现在包含 B。详细视图显示该 B。
    5. 在某个时候,XML 解析完成,现在 List 已更新。

    如果来自 Web 服务的加载和解析速度足够慢,那么步骤 4 也应该可能会简单地崩溃并出现 NSRangeException。

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

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

    【讨论】:

    • Hey Anomie...我很难在这里解释这个问题。但是您似乎已经非常准确地理解了这个问题......现在找到解决方案,我想转向您建议的第二种方法。 “另一种方法是解析器将结果累积到一个私有数组中,并在通知列表视图更新自身之前一次更新共享数组”您是否可以提供您正在尝试的伪代码说。我可以在我的应用中实现相同的功能,看看它是否有效。
    • 但是是的,正如我所说,由于该问题仅在用户键入速度非常快时才会出现,因此它似乎与在 2 个位置更新数组所花费的时间有关。再次感谢您对此的所有帮助。我已经尽力解决了这个问题,但没有任何运气,现在我真的很想解决这个问题。
    • 在您的 NSXMLParserDelegate 方法中,您必须将对象添加到检索数组中。相反,在retrievedArray 字段旁边添加一个temporaryArray 字段,在调用[parser parse] 之前将其设置为新的NSMutableArray,将结果添加到委托方法中的temporaryArray,然后在[parser parse] 返回之后调用[retrievedArray replaceObjectsInRange:NSMakeRange(0,retrievedArray.count) withObjectsFromArray:temporaryArray]。跨度>
    • 或者,如果您需要做一些额外的工作来发出信号以从[[Shared sharedManager] books] 重新加载retrievedArray(或者直接访问它而不是到处复制指向retrievedArray 的指针),您可以使用[[Shared sharedManager] setBooks:temporaryArray].
    • 嗯...也许您应该编辑问题以包含用于单击 Item1 的处理程序。
    【解决方案2】:

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

    不幸的是,很多人在没有真正理解的情况下学会了只是将nonatomic 放在他们的代码中。我一直认为这是从 Apple 示例代码中复制/粘贴而来的——他们经常将它用于与 UI 相关的东西——记住 UIKit 不是线程安全的。

    Anomie 在他/她的回答中指出,这不是——很可能——因为您正在从不同的线程中改变一个可变数组。 听起来对我来说是正确的答案 - 我会删除我的答案,但我会把它留在这里,因为我认为我的 cmets 是值得的(但不是 100% 与你的问题相关)。

    【讨论】:

    • 感谢您的回复...我从 Shared.h 文件中删除了 nonatomic @property (retain) NSMutableArray *books;但是问题还是出现了……
    • 拥有原子的 getter 和 setter 意味着获取和设置数组将是原子的,但访问数组的内容仍然不会。你说得对,它应该是原子的,但他还需要在修改/读取数组的代码周围使用@synchronize。使用@Anomie 在更新期间使用单独数组的解决方案会更简单、更有效,在这种情况下,原子设置器就足够了。
    • 如果Shared对象有@synchronized方法来操作books数组,就可以解决对数组内容的并发访问。
    【解决方案3】:

    我了解您已投入大量时间和精力来解决此问题,而 Anomie 的解决方案是解决此问题的最佳选择。但也许另一种方法可能更容易实施。

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

    值得一试,希望能有所帮助。

    【讨论】:

      【解决方案4】:

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

      请参阅NSRecursiveLock 文档。从概述:

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

      CoreVideo 示例代码有正确使用的示例。

      【讨论】:

      • 我以前从未听说过或使用过 NSRecursiveLock。您能否详细说明如何在我的应用中实现相同的功能。
      • 为什么要递归锁?普通的锁不应该足够吗?
      【解决方案5】:

      问题是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. 在主线程上运行的updateRetrievedArray: 将是负责更新retrievedArray 的代码——这样只有一个线程会更改此对象:

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

      【讨论】:

      • retrivedArray 是一个共享数组,在 XMLParser 中更新如下。首先,每次解析完成时都会删除所有对象; - (void) parseXMLFile: (NSURL *) url { [retrivedArray removeAllObjects]; .... } 然后在 parser:didEndElement( f([elementName isEqualToString:@"BookDetails"]) [retrievedArray addObject:aBook]; } 如果您需要任何其他详细信息,请告诉我。
      • 我根据您的评论更新了我的答案。试一试,如果它不能解决您的问题,请告诉我。
      • 是的,当然..我会尝试在我的应用程序中实现相同的功能。我会返回调查结果。
      • 只有 1 个快速问题...对于第 3 步 ...parser:didEndDocument: 你的意思是 parser:didEndElement: 请确认
      • 我确实尝试过上面的代码。但由于某些原因,它无法正常工作。我无法正确解析。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-09-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-04-18
      • 1970-01-01
      相关资源
      最近更新 更多