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

(更新)这是一个简单的问题:在iOS我想读一个大文件,做一些处理(在这种情况下编码为Base64string()并保存到设备上的临时文件。一个NSInputStream从文件中读取,然后进入

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

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

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

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

 stream <__NSCFInputStream: 0x1f020b00> got event 2 

(对应事件NSStreamEventHasBytesAvailable),然后什么也没有。 不是事件10,它对应于NSStreamEventEndEncountered,不是一个错误事件,什么都没有! 有时候我甚至会得到一个EXC_BAD_ACCESSexception,我现在还不知道如何debugging。 任何帮助,将不胜感激。

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

 - (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代码,问题依然存在。

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

当您计划NSInputStream在默认runloop上运行(所以,主线程的runloop),当主线程处于默认模式(NSDefaultRunLoopMode)时,会收到事件。

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

您的旧代码,dispatch_async几乎是好的(但移动主线程上的UI更新)。 您只需添加less许更改:

  • 在后台调度,如下所示:

  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 }); 
  • 在该线程上启动一个运行循环。 这个必须在调度asynchronous调用的末尾(最后一行)用这个代码完成

  [[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(在一个可用的后台线程上调度你的代码,或者如果需要的话,全部“自动地”启动一个新的)。 如果你愿意,你可以启动自己的线程,而不是使用调度asynchronous。

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

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

NSStream需要一个运行循环。 GCD不提供一个。 但是这里不需要GCD。 NSStream已经是asynchronous的了。 只要在主线上使用它, 这就是它的devise目的。

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

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