2011-02-27 40 views
5

我有一个带有UISearchBar的书籍应用程序,用户输入任何书名并在键入时获取搜索结果(来自外部API调用)。使用带有NSThread的单例同步数组

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

@interface Shared : NSObject { 
    NSMutableArray *books; 
} 

@property (nonatomic, retain) NSMutableArray *books; 

+ (id)sharedManager; 

@end 

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

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

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

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

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

  1. 列表视图显示的是一组结果,其包括在索引N.
  2. 用户类型的东西。 XML解析器开始逐步更新共享数组。列表视图尚未更新。
  3. 用户触摸列表视图中位于索引N处的项目。列表视图指示“详细信息”视图在索引N处显示项目。
  4. “细节”视图从共享数组中提取索引N处的项目,但由于在步骤2中启动的更新,索引N现在包含B.其中,查看显示。
  5. 在某些时候,XML解析完成,现在更新列表。

如果来自Web服务的加载和解析速度足够慢,则步骤4也可能会因NSRangeException而导致崩溃。

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

或者我可能完全错误的猜测更新是如何工作的,在这种情况下,您应该提供更多的细节。

+0

嘿,失恋......我很难在这里解释这个问题。但你似乎很了解这个问题非常准确...... 现在来解决问题,我想要走向你提出的第二种方法。 “ ”另一种方法是解析器将结果累积到私有数组中,并且在发送List视图以更新自身之前一次更新共享数组“ ”您是否可以提供伪代码你试图说。我可以在我的应用程序中实现相同的功能,并查看它是否有效。 – testndtv 2011-02-28 18:53:01

+0

但是,正如我所说的,因为只有当用户键入的速度非常快时才会出现问题,这似乎与更新2个地方的阵列所花费的时间有关。再次感谢您对此的所有帮助。我已尽最大努力解决这个问题,没有任何运气,现在我真的很想解决这个问题。 – testndtv 2011-02-28 18:53:22

+0

在您的NSXMLParserDelegate方法中,您必须将对象添加到retrieveArray中。 相反,在retrieveArray字段旁边添加'temporaryArray'字段,在调用'[parser parse]之前将其设置为新的NSMutableArray,将结果添加到delegate方法中的temporaryArray中,然后在[[parser parse]后面]返回调用'[retrieveArray replaceObjectsInRange:NSMakeRange(0,retrieveArray.count)withObjectsFromArray:temporaryArray]'。 – Anomie 2011-02-28 19:05:35

2

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

不幸的是,很多人已经学会了将nonatomic放在他们的代码中,但没有真正理解它。我一直认为这是通过复制/粘贴Apple示例代码 - 它们经常用于UI相关的东西 - 请记住UIKit不是线程安全的。

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

+0

感谢您的回复...我从Shared.h文件中删除了nonatomic @property(retain)NSMutableArray * books; 但仍然存在问题... – testndtv 2011-02-27 13:33:27

+1

拥有一个原子getter和setter意味着获取和设置数组将是原子的,但访问数组的内容仍然不会。你说得对,它应该是原子的,但他也需要围绕修改/读取数组的代码使用@synchronize。使用@ Anomie在更新期间使用单独数组的解决方案会更简单和更高效,在这种情况下,原子设置器就足够了。 – ughoavgfhw 2011-03-08 05:31:13

+0

如果Shared对象具有@synchronized方法来操作books数组,则可以解决对数组内容的并发访问。 – 2011-03-11 01:40:29

0

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

查看NSRecursiveLock文档。从概览:

NSRecursiveLock定义了可以由同一个线程,而不会引起死锁,其中一个线程被永久阻塞,等待本身放弃一个锁的情况下获取多次的锁。虽然锁定线程有一个或多个锁,但是所有其他线程都无法访问由锁保护的代码。

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

+0

我从来没有听说过或使用过NSRecursiveLock。您能否详细说明如何在我的应用程序中实现相同的功能。 – testndtv 2011-03-04 05:11:16

+0

为什么递归锁定?不应该有正常的锁? – ughoavgfhw 2011-03-08 05:28:21

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. updateRetrievedArray:主线程上运行将负责更新retrievedArray代码 - 这种方式只有一个线程改变这个对象:

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

retrieveArray是一个共享数组,在XMLParser中进行如下更新。首先,每次解析完成后都会删除所有对象; (void)parseXMLFile:(NSURL *)url {\t \t [retrieveArray removeAllObjects]; .... } 然后在 解析器:didEndElement( F([的ElementName isEqualToString:@ “BookDetails”]) \t \t [retrievedArray ADDOBJECT:ABOOK]; } 请让我知道如果您需要任何额外的详细信息, – testndtv 2011-03-05 13:19:37

+0

我根据您的评论更新了我的答案。试试看,如果它不能解决您的问题,请告诉我。 – skue 2011-03-06 22:26:16

+0

是的确定..我会尝试在我的应用中执行相同的操作。我会回来的调查结果。 – testndtv 2011-03-07 05:12:20

1

我明白,你已经投入了大量的时间和精力解决这个问题和Anomie的解决方案是最佳的。但也许一种不同的方法可能更容易实施。

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

这是值得一试,我希望它有帮助。