带上传流的 NSURLSession - 子类化 NSInputStream - com.apple.NSURLConnectionLoader 异常

NSURLSession with upload stream - subclassing NSInputStream - com.apple.NSURLConnectionLoader exception

基本任务

我有一些使用某些 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 的 subclass 中接听电话,在我的情况下 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

所以我正在使用 subclassing 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

我很困惑,不知道我还能做什么。

更新

我使用了代码形式link in comment which is hosted on github。 现在至少我的 class 的某些部分被框架调用,但我看到奇怪的崩溃。

崩溃位于此方法中:

- (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 的正确对象,所以我不知道它为什么会崩溃。

我看到您确实实现了未记录的 CFReadStream 桥接方法——这是更常见的问题之一。但是...请注意 NSStream class:

NSStream.h header 中的注释
// 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 在调用 -open 后不会 return NSStreamStatusNotOpen。基本上,每个具体的 subclass 都需要实现整个 API。它不像普通的 class 集群,其中只需要覆盖几个核心方法——甚至必须实现 -delegate 和 -setDelegate: 方法(superclass 没有实例变量存储为此,我很确定)。

AFNetworking 有一个内部 AFMultipartBodyStream,它具有所需的最少实现——您可以在 AFURLRequestSerialization.m. Another code example is HSCountingInputStream.

中看到该示例

未记录的私有桥接 API 不是自定义 NSInputStream 实现中的唯一问题,尤其是在 CFNetworking 集成的上下文中。我想推荐使用我的 POSInputStreamLibrary as basic building block. Instead of implementing a lot of NSInputStream methods and supporting async notifications you should implement much simpler POSBlobInputStreamDataSource interface. At least you can look at POSBlobInputStream 来咨询你应该实现什么样的功能来完全支持 NSInputStream 合同。

POSInputStreamLibrary is used in the most popular Russian cloud storage service Cloud Mail.Ru 并且每天上传超过 100 万个文件而没有任何崩溃。

祝你好运,欢迎随时提问。