12

(UPDATED) this is the problem in a nutshell: in iOS I want to read a large file, do some processing on it (in this particular case encode as Base64 string() and save to a temp file on the device. I set up an NSInputStream to read from a file, then in

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

I'm doing most of the work. For some reason, sometimes I can see the NSInputStream just stops working. I know because I have a line

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

in the beginning of (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode and sometimes I would just see the output

stream <__NSCFInputStream: 0x1f020b00> got event 2

(which corresponds to the event NSStreamEventHasBytesAvailable) and then nothing afterwards. Not event 10, which corresponds to NSStreamEventEndEncountered, not an error event, nothing! And also sometimes I even get a EXC_BAD_ACCESS exception which I have no idea at the moment how to debug. Any help would be appreciated.

Here is the implementation. Everything starts when I hit a "submit" button, which triggers:

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

Here is 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] ;
}

Here is setUpStreamsForInputFile called above:

- (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];   
}

Finally, this is where most logic occurs:

- (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:{
            ...
        }
    }
}

Note: when I posted this first, I was under a wrong impression the issue had something to do with using GCD. As per Rob's answer below I removed the GCD code and the issue persists.

PeterD
  • 642
  • 1
  • 6
  • 17

2 Answers2

20

First: in your original code, you were not using a background thread, but the main thread (dispatch_async but on the main queue).

When you schedule NSInputStream to run on the default runloop (so, the runloop of the main thread), the events are received when the main thread is in the default mode (NSDefaultRunLoopMode).

But: if you check, the default runloop changes mode in some situations (for example, during an UIScrollView scroll and some other UI updates). When the main runloop is in a mode different than the NSDefaultRunLoopMode, your events are not received.

Your old code, with the dispatch_async, was almost good (but move the UI Updates on the main thread). You have to add only few changes:

  • dispatch in the background, with something like this:

:

 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
});
  • start a run loop on that thread. This must be done at the end (last line) of the dispatch async call, with this code

:

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

Try and let me know

EDIT - added example code:

To be more clear, I copy-paste your original code updated:

- (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.
    });    
}

After making this modification, your:

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

method, will be called on the same thread where you started the run loop, a background thread: if you need to update the UI, it's important that you dispatch again to the main thread.

Extra informations:

In my code I use dispatch_async on a random background queue (which dispatch your code on one of the available background threads, or start a new one if needed, all "automagically"). If you prefer, you can start your own thread instead of using a dispatch async.

Moreover, I don't check if a runloop is already running before sending the "run" message (but you can check it using the currentMode method, look NSRunLoop reference for more informations). It shouldn't be necessary because each thread has only one associated NSRunLoop instance, so sending another run (if already running) does nothing bad :-)

You can even avoid the direct use of runLoops and switch to a complete GCD approach, using dispatch_source, but I've never used it directly so I can't give you a "good example code" now

LombaX
  • 17,265
  • 5
  • 52
  • 77
  • Thanks, LombaX. Just to make sure I understand what you're saying. In my original question, I was dipatching "sendData" to dispatch_async(dispatch_get_global_queue(0, 0)... and then when I was setting up the stream to read from the file I was using dispatch_async(dispatch_get_main_queue()... You're saying to keep both and just add [[NSRunLoop currentRunLoop] run]? Not sure I follow the logic so I think I better make sure... thanks a lot! – PeterD Mar 11 '13 at 19:07
  • is this that you meant? `- (void)setUpStreamsForInputFile:(NSString *)inpath outputFile:(NSString *)outpath { self.p_iStream = ... [p_iStream setDelegate:self]; dispatch_async(dispatch_get_main_queue(), ^ { [p_iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [p_iStream open]; **[[NSRunLoop currentRunLoop] run];** }); }` – PeterD Mar 11 '13 at 19:24
  • And this: `- (IBAction)submit:(id)sender { [p_spinner startAnimating]; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0); dispatch_async(queue, ^{ [self sendData]; **[[NSRunLoop currentRunLoop] run];** }); }` Sorry for being obtuse.. :) – PeterD Mar 11 '13 at 19:24
  • I updated the answer with an example based on your original code :-) – LombaX Mar 11 '13 at 19:51
  • thank a lot, I will try this and let you know - will probably update this tomorrow (need to go now) and accept as an answer if indeed this works! – PeterD Mar 11 '13 at 20:04
  • Thanks a lot, LombaX. I am still testing it but so far so good, no exceptions etc. Will update the thread when things change :) – PeterD Mar 12 '13 at 19:13
  • Great answer, helped with an odd intermittent bug, and I learned something about runloops. – Adam Shiemke Jun 12 '13 at 01:48
5

NSStream requires a run loop. GCD doesn't provide one. But you don't need GCD here. NSStream is already asynchronous. Just use it on the main thread; that's what it's designed for.

You're also doing several UI interactions while on a background thread. You can't do that. All UI interactions have to occur on the main thread (which is easy if you remove the GCD code).

Where GCD can be useful is if reading and processing the data is time consuming, you can hand that operation to GCD during NSStreamEventHasBytesAvailable.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thanks! I will do so, but in fact I was using it before without the GCD. I added GCD because I wanted all the task of reading and writing from/to the files to happen in the background. Instead, without the GCD, I so my spinner animating and the log displaying all the stream events. I also thought, as you said, that NSStream should happen asynchronously, but then I am not sure what I was doing wrong. I might update my question with a version that I had before or better maybe ask a separate question. Thanks for the reply! – PeterD Mar 11 '13 at 17:18
  • So, Rob, do you propose wrapping the logic in NSStreamEventHasBytesAvailable in dispatch_async(dispatch_get_main_queue(), ^ {...}); ? I don't think encoding each 24000 block of data is time consuming to justify the overhead of GCD... – PeterD Mar 11 '13 at 17:24
  • I'll need to restate and update my questions. I removed the GCD code and not only do I still see the same issue, but also I just now got a EXC_BAD_ACCESS exception. Something is wrong with the logic in general... – PeterD Mar 11 '13 at 17:35
  • What are your reading that leads to the EXC_BAD_ACCESS? Note that in general, you should access all your ivars through `self.`, not directly (except in `init` and `dealloc`). Many bugs are introduced by direct ivar access. If this is ARC code, that probably isn't the cause, but it's still good practice. Also note that Base64 encoding this way can create bizarre data. Base64 expects data to come in 3-byte units. If you hand it something not % 3, then you're going to get extra padding in the middle, which may not decode correctly (and is definitely non-optimal). – Rob Napier Mar 11 '13 at 18:06
  • I am not sure what causes EXC_BAD_ACCESS - how could I check? Now, all the guys with _ in the name (like _tempFileHandle in my code) are ivars, not declared as properties, so, I cannot access them with self., can I? I could I guess declare them as properties to be on the safe side... Finally, I was reading in chunks of 24000 which is a multiple of 3, so I thought I was doing it right for the Base64 encoding... thanks again! – PeterD Mar 11 '13 at 18:29
  • His first code was not on a background thread: it has a dispatch_async but on the main queue --> main thread, so the run loop was present. Moreover, if you setup the input stream to be executed in the default run loop mode, the events are not catched when the loop mode changes (for example during a scroll). It's better to use nsinputstream in a background thread and start it's own loop. I write an answer on this – LombaX Mar 11 '13 at 18:42
  • (It's better to use in a background thread --> **IMHO** it's better... :-) – LombaX Mar 11 '13 at 18:53