首页 > 解决方案 > 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 天起就陷入了困境。

标签: iosobjective-cexchangewebservicesnsurlsessiondownload

解决方案


没有特别的顺序:

  • 您不应该为每个请求创建一个新会话。这会阻止操作系统正确限制同时请求的数量,并且可能会导致其他问题。同样,您不应该finishTasksAndInvalidate在每个任务完成后调用。
  • 在没有更多未完成的请求之前,您必须保留对会话的引用。如果这不适合您的应用程序架构,您可以考虑使用默认会话而不是提供您自己的会话。
  • 您的 Content-Length 标头值不正确。它应该是字节数,而不是字符数。首先将字符串转换为带有编码的 NSData,并将其长度作为Content-Length 发送。否则,只要您在正文中获得一个多字节字符,它就会失败。
  • 理想情况下,您的 didReceiveResponse: 方法应该清除您的数据存储,以便它正确处理多部分响应(最后一个获胜),而不是连接它们。
  • 您编写的身份验证质询处理程序可能会导致严重问题。你应该检查挑战的保护空间,看看它是否是你关心的,如果不是,你应该触发默认处理。否则,如果用户使用任何类型的代理等,您的应用程序将失败。

解决这些问题,如果仍然无法正常工作,请提出一个关于仍然无法正常工作的新问题。:-)


推荐阅读