Подтвердить что ты не робот

NSInputStream перестает работать, иногда бросает EXC_BAD_ACCESS

(ОБНОВЛЕНО). Это проблема в двух словах: в iOS я хочу прочитать большой файл, выполнить некоторую обработку на нем (в этом конкретном случае кодировать как строку Base64() и сохранять в временный файл на устройстве. Я настроил 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] ;
}

Здесь задано значениеUpStreamsForInputFile:

- (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. Как ответ Роб ответил ниже, я удалил код GCD, и проблема не исчезла.

4b9b3361

Ответ 1

Сначала: в исходном коде вы не использовали фоновый поток, а основной поток (dispatch_async, но в главной очереди).

Когда вы планируете запуск NSInputStream в runloop по умолчанию (так, runloop основного потока), события принимаются, когда основной поток находится в режиме по умолчанию (NSDefaultRunLoopMode).

Но: если вы отметите, режим выполнения runloop по умолчанию в некоторых ситуациях (например, во время прокрутки UIScrollView и некоторых других обновлений пользовательского интерфейса). Когда основная runloop находится в режиме, отличном от NSDefaultRunLoopMode, ваши события не принимаются.

Ваш старый код с dispatch_async был почти хорош (но переместите обновления пользовательского интерфейса в основном потоке). Вы должны добавить только несколько изменений:

  • Отправка в фоновом режиме, с чем-то вроде этого:

:

 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

Попробуйте и дайте мне знать

EDIT - добавлен пример кода:

Чтобы быть более ясным, я скопирую-скопирую обновленный исходный код:

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

будет вызываться в том же потоке, где вы запустили цикл выполнения, фоновый поток: если вам нужно обновить пользовательский интерфейс, важно, чтобы вы снова отправили основной поток.

Дополнительная информация:

В моем коде я использую dispatch_async в случайной фоновой очереди (которая отправляет ваш код на один из доступных потоков фона или запускает новый, если нужно, все "автоматически" ). Если вы предпочитаете, вы можете начать свой собственный поток вместо использования async для отправки.

Кроме того, я не проверяю, запущена ли runloop перед отправкой сообщения "run" (но вы можете проверить его с помощью метода currentMode, посмотрите ссылку NSRunLoop для получения дополнительной информации). Это не должно быть необходимо, потому что каждый поток имеет только один связанный с ним экземпляр NSRunLoop, поэтому отправка другого запуска (если он уже выполняется) ничего не делает: -)

Вы даже можете избежать прямого использования runLoops и перейти на полный подход GCD, используя dispatch_source, но я никогда не использовал его напрямую, поэтому теперь я не могу дать вам "хороший пример кода"

Ответ 2

NSStream требуется цикл выполнения. GCD не обеспечивает его. Но здесь вам не нужен GCD. NSStream уже асинхронен. Просто используйте его в основной теме; для чего он предназначен.

Вы также выполняете несколько взаимодействий пользовательского интерфейса в фоновом потоке. Вы не можете этого сделать. Все взаимодействия с пользовательским интерфейсом должны происходить в основном потоке (что легко, если вы удалите код GCD).

Где GCD может быть полезным, если чтение и обработка данных занимает много времени, вы можете передать эту операцию GCD во время NSStreamEventHasBytesAvailable.