2013-06-04 31 views
0

我在這裏遇到了一些問題。我正在開發一個讀取文件並在UITableView中顯示其內容的應用程序。我最近意識到文件可能會變得非常大,我需要對文件的實際讀取進行異步編碼。我的項目已經很大,今天我設法將我已經擁有的代碼包裝在NSOperation中。使用NSOperation在NSOperationQueue中運行並行代碼

我所做的是我的解析器(打開並閱讀我的文件)現在在NSOperation中調用。就像那樣:

@implementation ReadPcapOperation 

@synthesize parser =_parser; 

- (id) initWithURL:(NSURL *)url linkedTo:(PacketFlowViewController *)packetController 
{ 
    self = [super init]; 
    if (self) { 
     _parser = [[PcapParser alloc] initWithURL:url linkedTo:packetController]; 
    } 
    return self; 
} 

- (void)main { 
    // a lengthy operation 
    @autoreleasepool { 
     if (self.isCancelled) 
      return; 

     [_parser read]; 

    } 
} 

@end 

我只給你這裏的實現,沒有什麼重要的.h文件。此子類的NSOperation被稱爲的NSOperation內:

_queue = [NSOperationQueue new]; 
_queue.name = @"File Parsing Queue"; 
_queue.maxConcurrentOperationCount = 1; 
[_queue addOperation:_readFileOperation]; 

_readFileOperation是上述ReadPcapOperation的一個實例。

現在,當我測試我的代碼時,仍然沒有區別,當我打開一個文件時,UI仍然被阻塞,而文件內容被加載到我的UITableView中。我有這個條件測試:

[NSThread isMainThread] 

該測試在主返回NO從ReadPcapOperation,這是很好的,正是我需要的。但是這個測試返回YES當我把它放在方法「read」中時,從ReadPcapOperation發送到main裏面的對象!所以我的整個代碼仍然在mainthread上運行,並阻止了我的UI。

我在這裏錯過什麼,夥計們?

讓我知道如果你需要更多的解釋!

編輯:

這裏是很奇怪:我會發佈一個本應在後臺線程中執行我的代碼的一部分。

- (void) read 
{ 
    if ([NSThread isMainThread]) 
     NSLog(@"read: IT S MAIN THREAD"); 
    else 
     NSLog(@"read: IT S NOT MAIN THREAD"); 

    [_fileStream open]; 
} 

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode { 
    switch(eventCode) 
    { 
     case NSStreamEventOpenCompleted: 
     { 
      //We read the pcap file header 
      [self readGlobalHeader]; 
      [self readNextPacket]; 
      break; 
     } 
     case NSStreamEventHasBytesAvailable: 
     { 
      //We read all packets 
      [self readNextPacket]; 
      break; 
     } 
     case NSStreamEventNone: 
     { 
      break; 
     } 
     case NSStreamEventHasSpaceAvailable: 
     { 
      break; 
     } 
     case NSStreamEventEndEncountered: 
     { 
      NSLog(@"End encountered !"); 
      [_fileStream close]; 
      [_fileStream removeFromRunLoop:[NSRunLoop currentRunLoop] 
           forMode:NSDefaultRunLoopMode]; 
      //_fileStream = nil; 
      break; 
     } 
     case NSStreamEventErrorOccurred: 
     { 
      NSError *theError = [stream streamError]; 
      NSLog(@"Error %i stream event occured. Domain : %@.", theError.code, theError.domain); 
      [stream close]; 
      break; 
     } 
    } 
} 

- (void) readGlobalHeader 
{ 
    if ([NSThread isMainThread]) 
     NSLog(@"readGlobalHeader: IT S MAIN THREAD"); 
    else 
     NSLog(@"readGlobalHeader: IT S NOT MAIN THREAD"); 
    int sizeOfGlobalHeader = 24; 

在這裏你可以看到我直接從NSOperation調用的讀取方法。那裏的日誌說:「沒有主線」。到現在爲止還挺好。讀取打開NSInputStreamObject,然後委託將調用「handleEvent」,並在此刻讀取字節。當我調用「readGlobalHeader」並讀取我的文件的第一個字節時,日誌是「MAIN THREAD」。它不應該在後臺線程中嗎?我真的迷失了!

可能是什麼重要的是要注意的是,當我初始化流,caling前「讀」我將它設置了這行代碼(我不知道它的原因):

[_fileStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 

編輯2: 這是斷點後的回溯。行爲就像我上面描述的那樣。 (沒有足夠多的聲譽,發表圖片)

Breakpoint at the read method Breakpoint after the stream is open

+0

'read'方法做了什麼,你在哪裏登錄它是否在主線程上?在返回** YES **的日誌語句中設置一個斷點併發布回溯。 – bbum

+0

read是一種簡單地向NSInputStream對象發送「open」的方法。我使用NSInputStream來讀取我的文件。一旦流打開,我解析文件,讀取字節後的字節和類似的東西。我編輯了我的文章,看看吧! – Starscream

回答

0

謝謝大家的回答,他們一直很有幫助。我解決了我的問題,這裏是如何:

首先我的流委託方法仍然在主線程中調用,我不知道爲什麼。這是因爲我初始化了我的NSOperation類的init方法中的流,而不是在main中。所以當我調用[_fileStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];時,它在主線程循環中進行調度。我通過初始化NSOperation main中的_filestream解決了這個問題,並且它在後臺線程中正確運行。

然後我意識到在調用委託方法之前,後臺線程實際上已經完成並銷燬了。在這一點上,我完全改變了我的後臺處理方式。我簡單地使用了一個dispatch_async作爲你的一些建議中的GCD。這實際上是我使用它的地方:

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode { 
    switch(eventCode) 
    { 
     case NSStreamEventOpenCompleted: 
     { 
      //We read the pcap file header 
      dispatch_async(myqueue,^
      { 
        [self readGlobalHeader]; 
        [self readNextPacket]; 
      }); 
      break; 
     } 
     case NSStreamEventHasBytesAvailable: 
     { 
      //We read all packets 
      [self readNextPacket]; 
      break; 
     } 

它的工作完美。我使用發送到主隊列的方法刷新了我的UI。

0

這是因爲流:爲handleEvent:是一個委託方法,被稱爲主線程。我建議改變你的設計,並且不要使用NSOperationQueue,而是讓這樣的方法讀取在另一個線程上執行一個操作。

您的情況GCD非常適合。所以我建議使用* dispatch_queue_t * ivar來執行需要異步調用的每個方法。在你的情況閱讀會改變這樣:

- (void) read 
{ 
    dispatch_async(self.queue,^
    { 
     if ([NSThread isMainThread]) 
      NSLog(@"read: IT S MAIN THREAD"); 
     else 
      NSLog(@"read: IT S NOT MAIN THREAD"); 

     [_fileStream open]; 
    }); 
} 

PS:請記住,隊列需要創建。

+0

好吧,我明白了。那麼我有什麼選擇?我並不熟悉併發和iOS。 NSThread會做這項工作嗎? – Starscream

+0

是的,會的。但在你的情況下,因爲* read *執行了幾行代碼,而且應用程序很簡單,所以我建議使用GCD,並閱讀關於GCD的這篇文章:http://cocoasamurai.blogspot.it/2009/09/guide-to -blocks-grand-central-dispatch.html –

0

隨着您的文件讀取已經在後臺線程中運行,您不需要像NSStream這樣的異步方法。嘗試使用NSFileHandle和readDataOfLength:

我不知道您的文件格式。如果您可以在讀取記錄之前確定記錄的大小(恆定寬度,或基於您迄今閱讀的內容可預測),則可以使用正確的記錄大小調用readDataOfLength:。當它返回時,你有你的記錄,並將其添加到表視圖。如果您無法預測記錄的大小,請致電readDataOfLength:1024並解析結果。當您找到記錄的結尾時,請添加它。如果在找到完整記錄之前用完了數據,請再次撥打readDataOfLength:1024並繼續從結果開始讀取。

無論採用哪種方式,readDataOfLength:的某些調用都會阻止,但這隻會阻止您的後臺線程,無論如何這個線程都沒有其他任何操作。保持阻塞的背景線程並不是太昂貴。

1

我的猜測是問題在於如何設置流。行:

[_fileStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 

是被稱爲主線程就這麼[NSRunLoop currentRunLoop]是主線程的runloop,因此您的委託回調被稱爲主線程。爲NSStreamDelegate協議stream:handleEvent:方法狀態的文檔:

的委託接收僅當theStream被調度上的 運行循環此消息。消息在流對象的線程上發送。代表應該檢查streamEvent以確定它應該採取的適當的 動作。

我猜測在主線程的runloop該調度裝置的流對象線程然後主線程。 (?)

(我試着回答這個問題時,對文檔中的運行循環和線程有點了解,但是這並不完全清楚,如果@bbum跟進,他肯定會提供一個權威回答)。

假設是這個問題,那麼你可以使用主線程來接收事件,然後通過使用另一個NSOperation將工作傳遞給另一個線程。或者你可能需要創建一個特定的線程和runloop來調度流。

我讀到了與NSURLConnection類似的情況,它在接收委託回調時也使用runloops。在這種情況下很難找到設置和管理runloop的例子,但是最近我查看了AFNetworking的代碼,並且看到這是他們如何管理其處理NSURLConnection的方式。我假設類似的方法可以用於流,所以你可能會發現一個有用的指南。

當然,也可能有更好的示例使用runloops流比NSURLConnection,所以也許先找那些。

編輯: 此外,您還沒有提到任何有關將操作配置爲concurrent操作的內容。除非我在這裏丟失了一些東西,如果委託回調是異步的,如NSURLConnections那麼你有一個併發操作,需要像這樣設置它?

2

大致上重申其他答案:使用類似NSOperation這樣的流API是錯誤的。它們對於大量的計算綁定任務很有用。文件或網絡I/O等事情主要涉及等待並使用涉及完成塊或代理和回調方法的異步API。

我似乎記得NSOperation文檔沒有提及它,但是在使用異步API時它們通常毫無意義。 (除了如提到的Rory O'Bryan所述,在使用「併發」操作時,然而,那些沒有利用操作隊列的後臺線程性質的操作系統,只有它們的操作管理&依賴關係,即如果只有一個操作就沒用)

發生了什麼事[_fileStream open]在您的read方法正在操作隊列中運行,然後它很快返回,操作完成。 委託回調沒有在open中調用,而是在它返回後的一段時間。它們在主線程上的調用與當代碼不使用NSOperation時不同。與morningstar的答案不同,我假設你確實需要堅持使用流API(如果不是,那麼他的答案是有效的 - 轉換爲同步調用,並且您的操作應該按預期工作)。不同於morningstar的答案,

我說你應該回到不使用NSOperation - 它不會購買任何東西。如果您喜歡_fileStream業務封裝在操作對象中,請將您的操作對象轉換爲普通的NSObject子類,然後調用它的read方法。

請看你的readGlobalHeaderreadNextPacket方法。他們可能正在進行同步調用,這肯定是阻止主線程的東西。如果情況確實存在的情況下,你有幾種選擇:

  • 關注Ramy Al Zuhouri的建議,除了使用dispatch_async,而不是換到你的readNextPacket等方法的調用。理論上,你可以按照他的明確的例子,也可以這樣做到[_fileStream open],但我只會這樣做,如果你測量這個調用需要很長時間才能返回。 GCD是讓這些方法在後臺線程中運行的一種簡單方法,只需閱讀正確設置隊列即可。

  • Rory O'Bryan一樣,建議更改流調度,以便在您創建的線程中調用您的委託方法。創建線程和runloops可能會很棘手,但this example看起來顯示了一個簡單的方法來做到這一點,但可能不是最好的。你必須至少添加一些東西來阻止線程。

    我認爲應該出現在最佳順序的事情是:

    1. th = [[NSThread alloc] init...]
    2. [_fileStream scheduleInRunLoop:<thread's-runloop> ...]
    3. [th start]
    4. 線程方法調用[[NSRunLoop currentRunLoop] run]只有一次

    然後流時已關閉,它從t開始不定期他runloop,run方法返回,所以線程的方法返回,線程結束。請注意,雖然我可能是錯的,並且您可能需要將[[NSRunLoop currentRunLoop] run]放入循環中。

  • 轉換readNextPacket等使用異步API。我會說這可能是最有意義的,除非你在這些方法中進行重要的計算,而不僅僅是網絡I/O。

+0

優秀的重申!這個答案應該重新閱讀直到被理解。 ;) – CouchDeveloper

相關問題