NSInputStream停止运行,有时会抛出EXC_BAD_ACCESS

时间:2013-03-11 17:06:46

标签: ios objective-c grand-central-dispatch nsinputstream

(更新)简而言之这就是问题:在iOS中我想读取一个大文件,对它进行一些处理(在这种特殊情况下编码为Base64 string()并保存到设备上的临时文件。我设置了一个NSInputStream来读取文件,然后在

(void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode

我正在完成大部分工作。出于某种原因,有时我可以看到NSInputStream停止工作。我知道因为我有一条线

NSLog(@"stream %@ got event %x", stream, (unsigned)eventCode);

(void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode的开头,有时我会看到输出

stream <__NSCFInputStream: 0x1f020b00> got event 2

(对应于事件NSStreamEventHasBytesAvailable)然后没有任何事情。不是事件10,它对应于NSStreamEventEndEncountered,不是错误事件,什么都没有!而且有时我甚至得到一个EXC_BAD_ACCESS异常,我现在不知道如何调试。任何帮助将不胜感激。

这是实施。当我点击“提交”按钮时,一切都开始了,这会触发:

- (IBAction)submit:(id)sender {     
    [p_spinner startAnimating];    
    [self performSelector: @selector(sendData)
           withObject: nil
           afterDelay: 0];   
}

这是sendData:

-(void)sendData{
    ...
    _tempFilePath = ... ;
    [[NSFileManager defaultManager] createFileAtPath:_tempFilePath contents:nil attributes:nil];
    [self setUpStreamsForInputFile: [self.p_mediaURL path] outputFile:_tempFilePath];
    [p_spinner stopAnimating];
    //Pop back to previous VC
    [self.navigationController popViewControllerAnimated:NO] ;
}

这是上面调用的setUpStreamsForInputFile:

- (void)setUpStreamsForInputFile:(NSString *)inpath outputFile:(NSString *)outpath  {
    self.p_iStream = [[NSInputStream alloc] initWithFileAtPath:inpath];
    [p_iStream setDelegate:self];
    [p_iStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                           forMode:NSDefaultRunLoopMode];
    [p_iStream open];   
}

最后,这是大多数逻辑发生的地方:

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {

    NSLog(@"stream %@ got event %x", stream, (unsigned)eventCode);

    switch(eventCode) {
        case NSStreamEventHasBytesAvailable:
        {
            if (stream == self.p_iStream){
                if(!_tempMutableData) {
                    _tempMutableData = [NSMutableData data];
                }
                if ([_streamdata length]==0){ //we want to write to the buffer only when it has been emptied by the output stream
                    unsigned int buffer_len = 24000;//read in chunks of 24000
                    uint8_t buf[buffer_len];
                    unsigned int len = 0;
                    len = [p_iStream read:buf maxLength:buffer_len];
                    if(len) {
                        [_tempMutableData appendBytes:(const void *)buf length:len];
                        NSString* base64encData = [Base64 encodeBase64WithData:_tempMutableData];
                        _streamdata = [base64encData dataUsingEncoding:NSUTF8StringEncoding];  //encode the data as Base64 string
                        [_tempFileHandle writeData:_streamdata];//write the data
                        [_tempFileHandle seekToEndOfFile];// and move to the end
                        _tempMutableData = [NSMutableData data]; //reset mutable data buffer 
                        _streamdata = [[NSData alloc] init]; //release the data buffer
                    } 
                }
            }
            break;
        case NSStreamEventEndEncountered:
        {
            [stream close];
            [stream removeFromRunLoop:[NSRunLoop currentRunLoop]
                              forMode:NSDefaultRunLoopMode];
            stream = nil;
            //do some more stuff here...
            ...
            break;
        }
        case NSStreamEventHasSpaceAvailable:
        case NSStreamEventOpenCompleted:
        case NSStreamEventNone:
        {
           ...
        }
        }
        case NSStreamEventErrorOccurred:{
            ...
        }
    }
}

注意:当我第一次发布时,我的错误印象是该问题与使用GCD有关。根据Rob的回答,我删除了GCD代码,问题仍然存在。

2 个答案:

答案 0 :(得分:19)

首先:在您的原始代码中,您没有使用后台线程,而是使用主线程(dispatch_async但在主队列中)。

当您安排NSInputStream在默认的runloop上运行时(因此,主线程的runloop),当主线程处于默认模式(NSDefaultRunLoopMode)时会收到事件。

但是:如果你检查,默认的runloop会在某些情况下改变模式(例如,在UIScrollView滚动和其他一些UI更新期间)。当主runloop处于与NSDefaultRunLoopMode不同的模式时,不会收到您的事件。

使用dispatch_async的旧代码几乎不错(但在主线程上移动UI更新)。您只需添加一些更改:

  • 在后台发送,如下所示:

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
 dispatch_async(queue, ^{ 
     // your background code

     //end of your code

     [[NSRunLoop currentRunLoop] run]; // start a run loop, look at the next point
});
  • 在该线程上启动一个运行循环。这必须在调度异步调用的结尾(最后一行)完成,使用此代码

 [[NSRunLoop currentRunLoop] run]; // note: this method never returns, so it must be THE LAST LINE of your dispatch

试着让我知道

编辑 - 添加了示例代码:

为了更清楚,我复制粘贴您更新的原始代码:

- (void)setUpStreamsForInputFile:(NSString *)inpath outputFile:(NSString *)outpath  {
    self.p_iStream = [[NSInputStream alloc] initWithFileAtPath:inpath];
    [p_iStream setDelegate:self];

    // here: change the queue type and use a background queue (you can change priority)
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
    dispatch_async(queue, ^ {
        [p_iStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                       forMode:NSDefaultRunLoopMode];
        [p_iStream open];

        // here: start the loop
        [[NSRunLoop currentRunLoop] run];
        // note: all code below this line won't be executed, because the above method NEVER returns.
    });    
}

进行此修改后,您的:

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {}

方法,将在你启动运行循环的同一个线程上调用,后台线程:如果你需要更新UI,重要的是你再次调度到主线程。

额外信息:

在我的代码中,我在随机后台队列上使用dispatch_async(在一个可用的后台线程上调度你的代码,或者如果需要的话,启动一个新的,全部“自动”)。如果您愿意,可以启动自己的线程,而不是使用dispatch async。

此外,在发送“run”消息之前,我不检查runloop是否已经在运行(但您可以使用currentMode方法检查它,查看NSRunLoop参考以获取更多信息)。它不应该是必要的,因为每个线程只有一个关联的NSRunLoop实例,所以发送另一个运行(如果已经运行)没有任何不好: - )

你甚至可以避免直接使用runLoops并使用dispatch_source切换到完整的GCD方法,但我从未直接使用它,所以我现在不能给你一个“好的示例代码”

答案 1 :(得分:4)

NSStream需要一个运行循环。 GCD没有提供。但是你不需要这里的GCD。 NSStream已经异步。只需在主线程上使用它;这就是它的设计目标。

您还在后台线程上进行了多次UI交互。你不能这样做。所有UI交互都必须在主线程上进行(如果删除GCD代码,这很容易)。

GCD可能有用的地方是,如果阅读和处理数据非常耗时,您可以在NSStreamEventHasBytesAvailable期间将该操作交给GCD。