NSURLSession с потоком загрузки - наследование NSInputStream - исключение com.apple.NSURLConnectionLoader

Основная задача

У меня есть мультиплатформенная библиотека, которая использует некоторый интерфейс потока C++. Я должен использовать этот интерфейс потока для загрузки данных NSURLSession, Моя реализация должна работать на OS X и iOS (в настоящее время я тестирую на OS X)

Что я сделал

Задача выглядит довольно просто, и я был уверен, что выполню это довольно быстро. Я настроил NSURLSession который работает нормально, если я использую NSURLRequest с простым NSData, Я пытаюсь использовать поток, как это:

        NSURLSessionDataTask *dataTask = [m_Private.session uploadTaskWithStreamedRequest: request];
        HTTPDownoadTaskProxy *dataTaskProxy = [HTTPDownoadTaskProxy new];
        // store data to properly handle delegate
        dataTaskProxy.coreTask = dataTask;
        dataTaskProxy.cppRequest= req;
        dataTaskProxy.cppResponseHandler = handler;
        dataTaskProxy.cppErrorHandler = errorHandler;

        m_Private.streamedDataTasks[dataTask] = dataTaskProxy;

        [dataTask resume];

Все идет нормально. Согласно документации uploadTaskWithStreamedRequest Я должен получить уведомление от делегата, и я получаю его:

- (void)URLSession: (NSURLSession *)session
              task: (NSURLSessionTask *)task
 needNewBodyStream: (void (^)(NSInputStream *bodyStream))completionHandler
{
    HTTPDownoadTaskProxy *proxyTask = self.streamedDataTasks[task];
    CppInputStreamWrapper *objcInputStream = [[CppInputStreamWrapper alloc] initWithCppInputStream:proxyTask.cppRequest.GetDataStream()];
    completionHandler(objcInputStream);
}

Теперь я должен принимать звонки в подклассе NSInputStream что в моем случае CppInputStreamWrapper, а также это довольно просто:

@implementation CppInputStreamWrapper

- (void)dealloc {
    NSLog(@"%s", __PRETTY_FUNCTION__);
}

- (instancetype)initWithCppInputStream: (const std::tr1::shared_ptr<IInputStream>&) cppInputStream
{
    if (self = [super init]) {
        _cppInputStream = cppInputStream;
    }
    return self;
}

#pragma mark - overrides for NSInputStream
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
    return (NSInteger)self.cppInputStream->Read(buffer, len);
}

- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len {
    return NO;
}

- (BOOL)hasBytesAvailable {
    return !self.cppInputStream->IsEOF();
}

#pragma mark - this methods are need to be overridden to make stream working
- (void)scheduleInRunLoop:(__unused NSRunLoop *)aRunLoop
                  forMode:(__unused NSString *)mode
{}

- (void)removeFromRunLoop:(__unused NSRunLoop *)aRunLoop
                  forMode:(__unused NSString *)mode
{}

#pragma mark - Undocumented CFReadStream Bridged Methods
- (void)_scheduleInCFRunLoop:(__unused CFRunLoopRef)aRunLoop
                     forMode:(__unused CFStringRef)aMode
{}

- (void)_unscheduleFromCFRunLoop:(__unused CFRunLoopRef)aRunLoop
                         forMode:(__unused CFStringRef)aMode
{}

- (BOOL)_setCFClientFlags:(__unused CFOptionFlags)inFlags
                 callback:(__unused CFReadStreamClientCallBack)inCallback
                  context:(__unused CFStreamClientContext *)inContext {
    return NO;
}

@end

Поэтому я использую обходной путь, необходимый при создании подклассов NSInputStream,

проблема

Теперь это должно работать. Но я не получаю никакого вызова методов CppInputStreamWrapper (кроме моего звонка при строительстве объекта).

Об ошибках нет, предупреждений нет, ничего!

Когда я добавил точку останова исключения, я ловлю

thread #8: tid = 0x155cb3, 0x00007fff8b770743 libobjc.A.dylib`objc_exception_throw, name = 'com.apple.NSURLConnectionLoader', stop reason = breakpoint 1.1

Это исходит из темы com.apple.NSURLConnectionLoader который я не создал.

Я полностью озадачен и понятия не имею, что еще я могу сделать.

Обновить

Я использовал ссылку кода формы в комментарии, который размещен на github. Теперь, по крайней мере, некоторые части моего класса вызываются фреймворком, но я вижу странный сбой.

Crash находится в этом методе:

- (BOOL)_setCFClientFlags:(CFOptionFlags)inFlags
                 callback:(CFReadStreamClientCallBack)inCallback
                  context:(CFStreamClientContext *)inContext {

    if (inCallback != NULL) {
        requestedEvents = inFlags;
        copiedCallback = inCallback;
        memcpy(&copiedContext, inContext, sizeof(CFStreamClientContext));

        if (copiedContext.info && copiedContext.retain) {
            copiedContext.retain(copiedContext.info);
        }

        copiedCallback((__bridge CFReadStreamRef)self, kCFStreamEventHasBytesAvailable, &copiedContext); // CRASH HERE
    } else {
        requestedEvents = kCFStreamEventNone;
        copiedCallback = NULL;
        if (copiedContext.info && copiedContext.release) {
            copiedContext.release(copiedContext.info);
        }

        memset(&copiedContext, 0, sizeof(CFStreamClientContext));
    }

    return YES;

}

Авария EXC_BAD_ACCESS (при запуске тестов на OS X). когда я вижу этот код, все выглядит хорошо. Он должен работать! self указывает на правильный объект с сохранением счетчика 3, поэтому я понятия не имею, почему он падает.

2 ответа

Решение

Недокументированный API частного моста - не единственная проблема в пользовательской реализации NSInputStream, особенно в контексте интеграции CFNetworking. Я хотел бы рекомендовать использовать мой POSInputStreamLibrary в качестве основного строительного блока. Вместо того, чтобы реализовывать множество методов NSInputStream и поддерживать асинхронные уведомления, вы должны реализовать гораздо более простой интерфейс POSBlobInputStreamDataSource. По крайней мере, вы можете посмотреть на POSBlobInputStream, чтобы узнать, какую функциональность вы должны реализовать для полной поддержки контракта NSInputStream.

POSInputStreamLibrary используется в самом популярном российском облачном хранилище Cloud Mail.Ru и загружает более 1 млн файлов в день без сбоев.

Удачи и не стесняйтесь задавать любые вопросы.

Я вижу, у вас есть реализации недокументированных методов моста CFReadStream - это одна из наиболее распространенных проблем. Однако... обратите внимание на комментарий в заголовке NSStream.h для класса NSStream:

// NSStream is an abstract class encapsulating the common API to NSInputStream and NSOutputStream.
// Subclassers of NSInputStream and NSOutputStream must also implement these methods.

Это означает, что вам также необходимо реализовать -open, -close, -propertyForKey:, -streamStatus и т. Д. - каждый метод, который в основном объявляется в NSStream и NSInputStream. Попробуйте вызвать -open самостоятельно в своем коде (что в конечном итоге сделает NSURLConnection) - вы поймете, что идея должна произойти сбой прямо здесь. Вероятно, вам понадобится хотя бы минимальная обработка статуса, чтобы -streamStatus не возвращал NSStreamStatusNotOpen после вызова, например, -open. По сути, каждый конкретный подкласс должен реализовывать весь API. Это не похоже на обычный кластер классов, где нужно переопределить только пару основных методов - даже должны быть реализованы методы -delegate и -setDelegate: (у суперкласса нет хранилища переменных экземпляра для него, я вполне уверен),

AFNetworking имеет внутренний AFMultipartBodyStream, который имеет минимальные необходимые реализации - вы можете увидеть этот пример внутри AFURLRequestSerialization.m. Другой пример кода - https://github.com/bjhomer/HSCountingInputStream.

Другие вопросы по тегам