使用AFNetworking在一个流请求中上传大量文件(最多100个),并提供上传进度。

8

使用AFNetworking 2.0进行流请求时,我遇到了几个挑战。

我想要上传大量文件(约50MB)到服务器,并且请求必须是流式的。(否则应用程序将因为内存压力而崩溃)

我已经尝试了各种方法在AFNetworking 2.0中实现,但都没有成功。 目前我正在使用以下方式:

NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:baseServiceUrl parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
    int imageIndex = 0;
    for (NSURL *tempFileUrl in tempFilesUrlList) {
        NSString* fileName = [NSString stringWithFormat:@"image%d", imageIndex];
        NSError *appendError = nil;
        [formData appendPartWithFileURL:tempFileUrl name:fileName error:&appendError];
        imageIndex++;
    }
} error:&error];

AFHTTPRequestOperation *requestOperation = nil;
requestOperation = [manager HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
    DLog(@"Uploading files completed: %@\n %@", operation, responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    DLog(@"Error: %@", error);
}];

[requestOperation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
    double percentDone = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
    DLog(@"progress updated(percentDone) : %f", percentDone);
}];
[requestOperation start];

这个代码可以正常工作,但它并不是流式的。它在内存中准备请求,如果请求过大就会崩溃。我曾尝试使用本机套接字和流的方法,但苹果公司表示可能因此拒绝应用程序。
我尝试了另一种方法:
AFHTTPSessionManager* manager = [AFHTTPSessionManager manager];
NSString *appID = [[NSBundle mainBundle] bundleIdentifier];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:appID];
NSURL* serviceUrl = [NSURL URLWithString:baseServiceUrl];
manager = [manager initWithBaseURL:serviceUrl sessionConfiguration:configuration];


NSURLSessionDataTask *uploadFilesTask = [manager POST:baseServiceUrl parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
    int imageIndex = 0;
    for (NSURL *tempFileUrl in tempFilesUrlList) {
        NSString* fileName = [NSString stringWithFormat:@"image%d", imageIndex];
        NSError *appendError = nil;
        [formData appendPartWithFileURL:tempFileUrl name:fileName error:&appendError];
        imageIndex++;
    }
} success:^(NSURLSessionDataTask *task, id responseObject) {
    DLog(@"Files uploaded successfully: %@\n %@", task, responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    DLog(@"Error: %@", error);
}];

[progressBar setProgressWithUploadProgressOfTask:uploadFilesTask animated:true];

但是这给我带来了运行时错误,显示:

终止应用程序,原因:未捕获的异常'NSGenericException', 原因:后台会话中的上传任务必须来自文件 *** 第一次抛出调用堆栈:( 0 CoreFoundation 0x02cc75e4 __exceptionPreprocess + 180 1 libobjc.A.dylib
0x02a4a8b6 objc_exception_throw + 44 2 CFNetwork
0x0459eb6c -[__NSCFBackgroundSessionBridge uploadTaskForRequest:uploadFile:bodyData:completion:] + 994 3
CFNetwork 0x045f2f37 -[__NSCFURLSession uploadTaskWithStreamedRequest:] + 73

(必须进行流式传输,并能够在后台继续)


据我所知,iOS不支持后台上传流。您需要在后台还是前台支持此功能?如果在前台,理论上应该能够进行流式传输?另一个选择是查看服务器端对上传文件进行分段的支持。您的服务器是否支持多部分上传(即Amazon S3的多部分上传http://docs.aws.amazon.com/AmazonS3/latest/dev/mpuoverview.html)。使用部分,您将能够以较小的部分上传文件,从而避免内存开销。 - Glen T
1个回答

6

最近我自己解决了这个问题。

我建议使用诸如AFNetworking之类的网络库。它可以避免你做已经被解决过的单调任务。

为了真正地在后台发送请求,你需要将整个请求写入文件,然后使用NSURLSession流式传输到服务器。(这是被接受的答案--链接如下)因此,你需要将请求写入磁盘而不是读取整个文件到内存中。

如何使用AFNetworking在后台上传任务

你可以像这样流式传输文件(但不在后台):

NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:@"http://example.com/upload" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        [formData appendPartWithFileURL:[NSURL fileURLWithPath:@"file://path/to/image.jpg"] name:@"file" fileName:@"filename.jpg" mimeType:@"image/jpeg" error:nil];
    } error:nil];

AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSProgress *progress = nil;

NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithStreamedRequest:request progress:&progress completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
    if (error) {
        NSLog(@"Error: %@", error);
    } else {
        NSLog(@"%@ %@", response, responseObject);
    }
enter code here
}];

[uploadTask resume];

http://cocoadocs.org/docsets/AFNetworking/2.5.0/

注意: 一个简单的解决方案是请求后台运行应用程序的时间。当您的应用代理收到 applicationDidEnterBackground: 的调用时,您可以调用 [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:] 请求一些后台运行时间。(调用[[UIApplication sharedApplication] endBackgroundTask:]; 来表示您已完成。)你会问有什么缺点?你将获得有限的时间。“如果在时间到期之前未为每个任务调用endBackgroundTask:,系统将终止应用程序。如果在处理程序参数中提供块对象,则系统在时间到期之前调用您的处理程序,以给您结束任务的机会。”

https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplication_Class/#//apple_ref/occ/instm/UIApplication/beginBackgroundTaskWithExpirationHandler:

进度

您可以使用子/父关系跟踪多个NSProgress对象的进度。 您可以创建一个NSProgress *overallProgress对象,然后在创建新请求之前调用[overallProgress becomeCurrentWithPendingUnitCount:<unit size>];,之后调用[overallProgress resignCurrent];。您的overallProgress对象将反映所有子项的进度。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接