iOS如何通过流式传输将大型资产文件上传到服务器

pre*_*dog 5 streaming upload ios

我是新的iOS程序员.
我想从资产库上传一个大文件(视频或图像)到服务器,我的原始方式只是使用NSMutableURLRequest并附加NSData(大视频或大图像),并在以下代码中发生崩溃:

    ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *asset){
        //.......some code I just skip it...
        ALAssetRepresentation *rep = [asset defaultRepresentation];
        void *buffer = [rawData mutableBytes];
        [rep getBytes:buffer fromOffset:0 length:size error:nil];
        NSData *videoData = [[NSData alloc] initWithBytes:buffer length:size];//crash here
        [self startUploading:videoData];
    }
Run Code Online (Sandbox Code Playgroud)

我知道这次崩溃是因为内存不够,视频文件不能只分配给NSData.
我已经谷歌这2天了,听起来有几种方法可以解决这个问题.

  1. 使用第三方库:如AFNetworking,ASIHTTPRequest(但我不想使用它,因为不知道何时会停止维护或更新)
  2. 使用流媒体上传大文件

我想用流媒体方式(点2)做上传的东西,
我发现这个链接:http: //zh.scribd.com/doc/51504708/10/Upload-Files-Over-HTTP
看起来可以解决我的问题,但仍然不是很清楚知道该怎么做

问题1:该链接中有一个示例,上传文件来自bundle
如何将资产转换为流?或将资产复制到APP的文件夹?
我发现此链接将图像从assets-library复制到app文件夹
但仍无法找到方法.

问题2:还是有其他更清晰的流媒体示例来上传大文件吗?



谢谢你的激情

更新1:在我实现了needNewBodyStream委托后,"请求流耗尽"消息似乎解决了,而是遇到另一个"错误域= kCFErrorDomainCFNetwork代码= 303"操作无法完成."如何解决它?

-(NSInputStream *)connection:(NSURLConnection *)connection needNewBodyStream:(NSURLRequest *)request
{
    [NSThread sleepForTimeInterval:2];
    NSInputStream *fileStream = [NSInputStream inputStreamWithFileAtPath:pathToBodyFile];
    if (fileStream == nil) {
        NSLog(@"NSURLConnection was asked to retransmit a new body stream for a request. returning nil!");
    }
    return fileStream;
}
Run Code Online (Sandbox Code Playgroud)

Cou*_*per 7

假设您的数据太大而无法放入内存:

一种有效且可靠的方法将利用有界CFStream对(参见CFStreamCreateBoundPair).

有界流对的输入流设置为NSMutableURLRequest's HTTPBodyStream属性.有界流对的输出流将被用来写从已经填充有固定大小的存储器缓冲器获得字节ALAssetRepresentationgetBytes:fromOffset:length:error:方法.

有界流对的传输缓冲区的大小应与资产表示的缓冲区大小相同.

设置代码需要几行代码和NSStreams的一些经验并处理事件(NSStreams经常有一些细微之处).

这种方法的工作原理如下:

  1. 创建一个流的委托,处理所有流事件.

  2. 为传输缓冲区设置具有特定大小的配对流,设置委托并在运行循环上安排它们.

  3. 为具有相同大小的资产数据设置内存缓冲区.

  4. 当您打开流时,您会收到一个NSStreamEventHasSpaceAvailable事件.您可以通过读取资产数据来处理该事件getBytes:fromOffset:length:error:,并写入内存缓冲区.当您使用一大块资产数据填充缓冲区时,请将此缓冲区写入有界流对的输出流.适当地跟踪偏移!

  5. 现在,有界流对的输入流很好地由底层连接提取(它将字节从内部传输缓冲区移动到网络套接字),然后您将获得另一个NSStreamEventHasSpaceAvailable事件,因为现在内部传输缓冲区中有空间可用.写入适合从资产数据缓冲区到输出流的有界流对输出流的字节数,以及资产数据缓冲区中可用的字节数.如果资产数据缓冲区已完全写入,请重新填充.仔细跟踪偏移和范围!

  6. 您可以处理事件,直到写入整个资产数据.然后关闭输出流.

  7. 您还需要处理其他流事件,请参阅:流编程指南

注意:您可能会注意到您的内存缓冲区只能部分写入输出流.通过跟踪偏移来处理这一问题,以便始终在缓冲区中保留连续的资产数据流,并将适当的数据范围从缓冲区写入输出流!

警告:

使用有限的一对流设置正确的实现可能很棘手并且可能容易出错.我有一个通用版本的"InputStreamSource"(它暴露了一个普通的NSInputStream,它将用于设置HTTPBodyStream属性),它可以很容易地扩展为与任何输入源一起使用 - 例如资产数据.如果你有兴趣,我可以把这个代码放在gist上.

AFNetworking或任何其他网络库将不会解决你.说实话,我不建议将AFNetworking与一起用作身体部分 - 因为AFNetworking的实施在这方面仍然是可疑的.我建议您NSURLConnection自己实现代理,或者使用另一个第三方库来正确处理POST请求的输入正文流.

短(不完整)示例

这个想法是,创建某种"资产输入源"类,它暴露了一个NSInputStream(可用于设置其HTTPBodyStream属性NSURLRequest)并提供资产数据.

如果"资产输入源"是文件,则任务很简单:只需创建NSInputStream与该文件关联的对象即可.但是,我们的资产只能通过某种表示形式的字节范围访问,该字节存在于某个临时缓冲区中.

因此,任务是用适当的字节范围填充该临时缓冲区.然后,分段,将这些字节写入绑定到输入流的专用输出.此输入流输出流对将通过函数创建CFStreamCreateBoundPair.

输入流将成为我们暴露NSInputStream的"资产输入源"的.

输出流在内部仅使用."资产输入源"将使用资产进行初始化.

我们的"资产输入源"类需要处理流事件,因此它将成为流委托.

现在,我们有了实现它的一切.

CFStreamCreateBoundPair函数创建CFStream对象.但是,由于NSStreams是免费桥接器,我们可以轻松地将它们"转换"为NSStreams.

"资产输入源"类的一部分startinit方法可以如下实现:

    _buffer = (uint8_t)malloc(_bufferSize);
    _buffer_end = _buffer + _bufferSize;
    _p = _buffer;
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    CFStreamCreateBoundPair(NULL, &readStream, &writeStream, _bufferSize);
    self.inputStream = CFBridgingRelease(readStream);
    self.internalOutputStream = CFBridgingRelease(writeStream);        

    [self.internalOutputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:self.runLoopMode];
    [self.internalOutputStream open];
    // (Note: inputStream will be opened and scheduled by the request!)
Run Code Online (Sandbox Code Playgroud)

inputStream 是该类的公共@property(公开的输入流).

internalOutputStream是该类的私人财产.

_buffer 是内部缓冲区,包含资产表示的字节范围.

请注意,有界流对的内部缓冲区大小等于保存资产数据的缓冲区.

流委托方法stream:handleEvent:可以实现如下:

- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent
{
    if (_isCancelled) {
        return;
    }
    switch (streamEvent) {
        case NSStreamEventNone:
            break;
        case NSStreamEventOpenCompleted:
            DLogInfo(@"internal output stream: open completed");
            break;
        case NSStreamEventHasBytesAvailable:
            // n.a.
            NSAssert(0, @"bogus stream event");
            break;
        case NSStreamEventHasSpaceAvailable:
            NSAssert(theStream == _internalOutputStream, @"bogus stream event");
            DLogInfo(@"destination stream: has space available");
            [self write];
            break;
        case NSStreamEventErrorOccurred:
            DLogInfo(@"destination stream: error occurred");
            [self finish];
            break;
        case NSStreamEventEndEncountered:
            // weird error: the output stream is full or closed prematurely, or canceled.
            DLogWarn(@"destination stream: EOF encountered");
            if (_result == nil) {
                self.result = [NSError errorWithDomain:NSStringFromClass([self class])
                                                  code:-2
                                              userInfo:@{NSLocalizedDescriptionKey: @"output stream EOF encountered"}];
            }
            [self finish];
            break;
    }
}
Run Code Online (Sandbox Code Playgroud)

如你所见,秘诀在于方法write.还有一种finish方法和cancel方法.

基本上,方法write从_buffer复制到内部输出流,尽可能多地放入流中.当_buffer完全写入输出流时,它将再次从资产数据中填充.

当没有可用于从资产写入输出流的数据时,finish将调用方法.

方法finish关闭内部输出流并取消调度流.

完整可靠的实现可能有点棘手."资产输入源"也应该是可取消的.

如上所述,我确实有一个"抽象输入源"类,它实现除了使用资产数据填充_buffer之外的所有内容,如果需要,我可以将其作为Gist上的代码片段提供.