ios - NSURLSession 未下载完整数据,但成功完成
问题描述
场景:我正在使用 NSURLSession从EWS API下载一些大附件(30-50 mb)。并将下载的 xml 数据保存到文件中。
我创建了使用 NSURLSession、处理委托回调并具有完成处理程序的 HTTP 类。HTTP 类创建自己的 NSURLSession 并开始下载数据。这是我的 HTTP.m
//
// HTTP.m
// Download
//
// Created by Ankush Kushwaha on 7/6/18.
// Copyright © 2018 Ankush Kushwaha. All rights reserved.
//
#import "HTTP.h"
typedef void (^httpCompletionBlock)(NSData* result);
@interface HTTP()
@property (nonatomic) NSMutableData * data;
@property (nonatomic) NSString *fileNametoSaved;
@property (nonatomic) httpCompletionBlock completion;
@end
@implementation HTTP
- (instancetype)initWithAttachmntId:(NSString *)attachmentId
fileName:(NSString *)fileName
completion:(void (^)(NSData* result))completion
{
self = [super init];
if (self) {
self.data = [NSMutableData data];
self.completion = completion;
self.fileNametoSaved = fileName;
NSURL *requestUrl = [NSURL URLWithString:@"https://outlook.office365.com/EWS/Exchange.asmx"];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:requestUrl];
request.HTTPMethod = @"POST";
NSString *soapXmlString = [NSString stringWithFormat:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
"<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
"xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\"\n"
"xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\"\n"
"xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n"
"<soap:Body>\n"
"<m:GetAttachment>\n"
"<m:AttachmentIds>\n"
"<t:AttachmentId Id=\"%@\"/>\n"
"</m:AttachmentIds>\n"
"</m:GetAttachment>\n"
"</soap:Body>\n"
"</soap:Envelope>\n",attachmentId];
if (soapXmlString)
{
NSString *xmlLength = [NSString stringWithFormat:@"%ld", (unsigned long)soapXmlString.length];
request.HTTPBody = [soapXmlString dataUsingEncoding:NSUTF8StringEncoding];
[request addValue:@"text/xml; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
[request addValue:xmlLength forHTTPHeaderField:@"Content-Length"];
}
dispatch_async(dispatch_get_main_queue(), ^{
NSURLSessionConfiguration *defaultConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:defaultConfiguration
delegate:self
delegateQueue:nil];
NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithRequest:request];
[dataTask resume];
});
}
return self;
}
-(void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
{
if (challenge.previousFailureCount == 0)
{
NSURLCredential* credential;
credential = [NSURLCredential credentialWithUser:@"MY_OUTLOOK.COM EMAIL" password:@"PASSWORD" persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
}
else
{
// URLSession:task:didCompleteWithError delegate would be called as we are cancelling the request, due to wrong credentials.
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
}
-(void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
completionHandler(NSURLSessionResponseAllow);
}
-(void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
{
[self.data appendData:data];
// NSLog(@"data : %lu", (unsigned long)self.data.length);
}
-(void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
NSLog(@"didCompleteWithError: %@", error);
if (error)
{
NSLog(@"Error: %@", error);
}
else
{
NSData *data;
if (self.data)
{
data = [NSData dataWithData:self.data];
}
NSLog(@"Success : %lu", (unsigned long)self.data.length);
NSString *filePath = [NSString stringWithFormat:@"/Users/startcut/Desktop/xxx/%@",
self.fileNametoSaved];
NSString *xmlString = [[NSString alloc] initWithData:self.data encoding:NSUTF8StringEncoding];
[xmlString writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
self.completion ? self.completion(self.data) : nil;
}
[session finishTasksAndInvalidate]; // We must release the session, else it holds strong referance for it's delegate (in our case EWSHTTPRequest).
// And it wont allow the delegate object to free -> cause memory leak
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler;
{
NSString *redirectLocation = request.URL.absoluteString;
if (response)
{
completionHandler(nil);
}
else
{
completionHandler(request); // new redirect request
}
}
@end
在我的 ViewController 中,我发出 5 个 HTTP 请求,以下载 5 个不同的附件。
HTTP *http = [[HTTP alloc] initWithAttachmntId:@"AAAaAGFua3VzaC5zdGFyY3V0QG91dGxvb2suY29tAEYAAAAAACd30qZd6oFAvoaMby5vOMUHAOsTbManU6VPoeQkUTl4/J0AAWUo5o0AAOsTbManU6VPoeQkUTl4/J0AAWruTj0AAAESABAAWGs6REUQc02OHF0x6uYJ+g=="
fileName:@"http1"
completion:^(NSData *result) {
NSLog(@"Completion 1");
}];
HTTP *http2 = [[HTTP alloc] initWithAttachmntId:@"AAAaAGFua3VzaC5zdGFyY3V0QG91dGxvb2suY29tAEYAAAAAACd30qZd6oFAvoaMby5vOMUHAOsTbManU6VPoeQkUTl4/J0AAWUo5o0AAOsTbManU6VPoeQkUTl4/J0AAWruTjsAAAESABAAP8zebUI1fkSiE8tQ+RtwiQ=="
fileName:@"http2"
completion:^(NSData *result) {
NSLog(@"Completion 2");
}];
HTTP *http3 = [[HTTP alloc] initWithAttachmntId:@"AAAaAGFua3VzaC5zdGFyY3V0QG91dGxvb2suY29tAEYAAAAAACd30qZd6oFAvoaMby5vOMUHAOsTbManU6VPoeQkUTl4/J0AAWUo5o0AAOsTbManU6VPoeQkUTl4/J0AAWruTjkAAAESABAAiPaJIPjp/k6iQHSMpi6aDw=="
fileName:@"http3"
completion:^(NSData *result) {
NSLog(@"Completion 3");
}];
HTTP *http4 = [[HTTP alloc] initWithAttachmntId:@"AAAaAGFua3VzaC5zdGFyY3V0QG91dGxvb2suY29tAEYAAAAAACd30qZd6oFAvoaMby5vOMUHAOsTbManU6VPoeQkUTl4/J0AAWUo5o0AAOsTbManU6VPoeQkUTl4/J0AAWruTjwAAAESABAA86vBkFlTNU2oEVq/eRtLGQ=="
fileName:@"http4"
completion:^(NSData *result) {
NSLog(@"Completion 4");
}];
HTTP *http5 = [[HTTP alloc] initWithAttachmntId:@"AAAaAGFua3VzaC5zdGFyY3V0QG91dGxvb2suY29tAEYAAAAAACd30qZd6oFAvoaMby5vOMUHAOsTbManU6VPoeQkUTl4/J0AAWUo5o0AAOsTbManU6VPoeQkUTl4/J0AAWruTjoAAAESABAAND6qbOQbnkyoyg0K17T9/Q=="
fileName:@"http5"
completion:^(NSData *result) {
NSLog(@"Completion 5");
}];
问题:由于文件或数据与 5 个单独的 HTTP 对象并行下载,最后当 NSUrlSession 会话委托被调用时,我将数据保存到我的 HTTP.m-(void)URLSession (NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
方法中的文件中。大多数情况下,下载的数据(文件)不包含完整数据(例如,如果附件的大小为 30 mb,我的代码会下载 4 mb 或 10 mb 或 3.2 mb 等数据。数字不一致)。似乎 NSURLSession 终止或停止之间的数据下载并成功关闭连接。如果我一次下载 1 个附件(而不是在我的视图控制器中创建 5 个 HTTP 对象,我一次只创建 1 个对象)在大多数情况下它可以工作并下载完整的数据内容。
感谢任何帮助。我从 2 天起就陷入了困境。
解决方案
没有特别的顺序:
- 您不应该为每个请求创建一个新会话。这会阻止操作系统正确限制同时请求的数量,并且可能会导致其他问题。同样,您不应该
finishTasksAndInvalidate
在每个任务完成后调用。 - 在没有更多未完成的请求之前,您必须保留对会话的引用。如果这不适合您的应用程序架构,您可以考虑使用默认会话而不是提供您自己的会话。
- 您的 Content-Length 标头值不正确。它应该是字节数,而不是字符数。首先将字符串转换为带有编码的 NSData,并将其长度作为Content-Length 发送。否则,只要您在正文中获得一个多字节字符,它就会失败。
- 理想情况下,您的 didReceiveResponse: 方法应该清除您的数据存储,以便它正确处理多部分响应(最后一个获胜),而不是连接它们。
- 您编写的身份验证质询处理程序可能会导致严重问题。你应该检查挑战的保护空间,看看它是否是你关心的,如果不是,你应该触发默认处理。否则,如果用户使用任何类型的代理等,您的应用程序将失败。
解决这些问题,如果仍然无法正常工作,请提出一个关于仍然无法正常工作的新问题。:-)
推荐阅读
- mysql - 如何在 SQL 中 JOIN 后获取最后 N 条记录?
- android - Android studio - 如何获取同步的帐户以及是否启用它们?
- aspnetboilerplate - ABP 其他租户用户在检索功能中不可见
- php - 如何使用 PHP 解决此代码测试?
- php - 在 Centos7 上使用 MySQL 时出现撇号错误
- java - SQLiteException: no such table: When Table Exsists
- python - comparing strings and replacing words
- node.js - Extremely poor performance when launching script through PM2 on raspberry pi
- windows - Programmatically run Microsft Store and sign in to Microsoft account
- angular - 强制 Angular Service Worker 忽略外部图像