0

我在这里遇到了一个问题。我正在开发一个读取文件并在 UITableView 中显示其内容的应用程序。我最近意识到文件可能会变得非常大,并且我需要对文件的实际读取进行异步编码。我的项目已经很大了,今天我设法将我已经拥有的代码包装在一个 NSOperation 中。

我所做的是现在在 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]

这个测试从 ReadPcapOperation 返回NO in main,这很好,正是我需要的。但是当我把它放在方法“read”中时,这个测试返回YES ,消息从 ReadPcapOperation 发送到 main 内的对象!所以我的整个代码仍在主线程上运行并阻塞了我的 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 调用的方法 read。那里的日志说:“不是主线程”。到目前为止,一切都很好。读取打开 NSInputStreamObject,然后委托将调用“handleEvent”,此时我读取字节。当我调用“readGlobalHeader”并读取文件的第一个字节时,日志是“MAIN THREAD”。它不应该在后台线程中吗?我真的迷路了!

可能需要注意的是,当我初始化流时,在调用“读取”之前,我用这行代码设置了它(我不确定这是原因):

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

编辑 2: 这是断点后的回溯。行为就像我上面描述的那样。(没有足够的声誉来发布图片)

读取方法处的 断点 流打开后的断点

4

5 回答 5

2

很大程度上重申了其他答案:使用NSOperation类似这样的流 API 是错误的。它们对于大量计算密集型任务最有用。文件或网络 i/o 之类的事情主要涉及等待和使用涉及完成块或委托和回调方法的异步 API。

我似乎记得NSOperation文档没有提及太多,但是在使用异步 API 时它们通常毫无意义。(除了,正如Rory O'Bryan提到的,在使用“并发”操作时。但是,对于那些你没有利用操作队列的后台线程特性的那些,只有它们的操作管理和依赖关系,即如果你只有一个操作,那就没用了)

发生的事情是,[_fileStream open]您的read方法正在操作队列中运行,然后它很快返回,然后操作完成。stream:handleEvent:委托回调不是在内部调用,而是open在它返回后的一段时间内调用。它们在主线程上的调用与您的代码未使用时没有什么不同NSOperation

Morningstar的答案不同,我假设您确实需要坚持使用流 API(如果不是,那么他的答案是有效的 - 转换为同步调用,您的操作应该按预期工作)。

我说你应该恢复到不使用NSOperation- 它没有给你买任何东西。如果您喜欢将_fileStream业务封装在操作对象中的方式,请将您的操作对象转换为普通的NSObject子类,然后调用其read方法即可。

看你的readGlobalHeader方法readNextPacket。他们可能正在进行同步调用,这肯定是阻塞主线程的原因。如果确实如此,那么您有几个选择:

  • 遵循Ramy Al Zuhouri的建议,除了使用dispatch_async来包装对您的readNextPacketetc 方法的调用。从理论上讲,您可以遵循他的明确示例,也可以这样做[_fileStream open],但如果您测量到此调用需要很长时间才能返回,我只会这样做。GCD 是一种让这些方法在后台线程中运行的简单方法,只需阅读正确设置队列的内容即可。

  • 就像Rory O'Bryan建议的那样,更改流调度,以便在您创建的线程中调用您的委托方法。创建线程和运行循环可能很棘手,但这个例子看起来展示了一种简单的方法,尽管可能不是最好的。你必须至少添加一些东西来停止线程。

    我认为最好的顺序应该是:

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

    然后当流关闭时,它从runloop中取消调度,run方法返回,所以线程的方法返回,线程结束。请注意,我可能错了,[[NSRunLoop currentRunLoop] run]毕竟您可能需要循环。

  • 转换readNextPacket等以使用异步 API。我想说这可能是最有意义的,除非您在这些方法中进行大量计算,而不仅仅是网络 i/o。

于 2013-06-05T01:09:39.917 回答
1

我的猜测是问题在于您如何设置流。该行:

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

在主线程上被调用,[NSRunLoop currentRunLoop]主线程的运行循环也是如此,因此您的委托回调在主线程上被调用。NSStreamDelegate协议方法的文档stream:handleEvent:指出:

只有当 theStream 被安排在运行循环上时,代理才会收到此消息。消息在流对象的线程上发送。委托应检查 streamEvent 以确定应采取的适当操作。

我猜想在主线程的运行循环上进行调度意味着流对象线程就是主线程。(?)

(在尝试回答这个问题时,我在文档中阅读了一些关于运行循环和线程的信息,但是否是这种情况并不完全清楚。如果@bbum 跟进,他肯定可以提供明确的答案)。

假设这是问题所在,那么您可以简单地使用主线程来接收事件,然后可能通过使用另一个 NSOperation 将工作传递给另一个线程。或者您可能需要创建一个特定的线程和运行循环来安排流。

我阅读了类似的情况,NSURLConnection在接收委托回调时也使用运行循环。在这种情况下很难找到设置和管理 runloop 的示例,但是最近我查看了 for 的代码,AFNetworking发现这就是他们管理对NSURLConnection. 我认为类似的方法适用于流,因此您可能会发现这是一个有用的指南。

当然,也可能有比以前更好的使用 runloops 流的例子,NSURLConnection所以也许首先寻找那些。

编辑:您也没有提及将您的操作配置为concurrent操作的任何内容。除非我在这里遗漏了什么,如果委托回调是异步NSURLConnections的,那么你有一个并发操作并且需要这样设置它吗?

于 2013-06-04T19:47:37.900 回答
0

发生这种情况是因为stream:handleEvent:作为委托方法,在主线程上被调用。我建议更改您的设计并且不要使用NSOperationQueue,而是让诸如read之类的方法在另一个线程上执行操作。

在您的情况下, 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:记住需要创建队列。

于 2013-06-04T18:38:42.727 回答
0

由于您的文件读取已经在后台线程中运行,您不需要像 NSStream 这样的异步方法。尝试仅使用 NSFileHandle 和readDataOfLength:.

我不知道你的文件格式。如果您可以在读取记录之前确定它的大小(恒定宽度,或根据您目前已阅读的内容可预测),那么您可以readDataOfLength:使用正确的记录大小进行调用。当它返回时,您将获得记录并将其添加到表格视图中。如果您无法预测记录的大小,则只需调用readDataOfLength:1024并解析结果即可。当您找到记录的结尾时,添加它。如果您在找到完整记录之前用完了数据,请readDataOfLength:1024再次调用并从结果的开头继续阅读。

无论哪种方式,一些调用readDataOfLength:都会阻塞,但这只会阻塞你的后台线程,无论如何它没有任何其他事情可做。保持一个被阻塞的后台线程并不太昂贵。

于 2013-06-04T19:42:31.467 回答
0

谢谢大家的回答,他们非常有帮助。我解决了我的问题,方法如下:

首先我的流委托方法仍然在主线程中被调用,我不知道为什么。这是因为我在initNSOperation 类的方法中初始化了流,而不是在main. 因此,当我调用[_fileStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];它时,它正在主线程循环中进行调度。我通过在 NSOperation 中初始化 _filestream 解决了这个问题,main它在后台线程中正确运行。

然后我意识到,在调用委托的方法之前,后台线程实际上已经完成并销毁了。在这一点上,我完全改变了我做后台处理的方式。正如你们中的一些人建议的那样,我只是使用了带有 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。

于 2013-06-06T18:26:39.813 回答