iOS использует NSURLSession для реализации фоновой загрузки и выгрузки.

Основная логика фоновой загрузки NSURLSession такова: сначала создайте NSURLSessionConfiguration в фоновом режиме, затем создайте NSURLSession с помощью этой конфигурации, затем создайте соответствующий NSURLSessionTask и, наконец, обработайте соответствующие события прокси.

1. Создайте NSURLSession.

- (NSURLSession *)backgroundURLSession {
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSURLSessionConfiguration* sessionConfig = nil;
        NSString *identifier = [NSString stringWithFormat:@"%@.%@", [NSBundle mainBundle].bundleIdentifier, @"HttpUrlManager"];
        sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
        //请求的缓存策略
        sessionConfig.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
        //数据传输超时,当恢复传输时会清零
        sessionConfig.timeoutIntervalForRequest = 60;
        //单条请求超时,决定一条请求的最长生命周期
        sessionConfig.timeoutIntervalForResource = 60;
        //请求的服务类型
        sessionConfig.networkServiceType = NSURLNetworkServiceTypeDefault;
        //是否允许使用移动网络(电话网络)default is YES
        sessionConfig.allowsCellularAccess = YES;
        //后台模式生效,YES允许自适应系统性能调节
        sessionConfig.discretionary = YES;
        sessionConfig.HTTPMaximumConnectionsPerHost = 20;
        sessionConfig.sessionSendsLaunchEvents = NO;
        sessionConfig.multipathServiceType = NSURLSessionMultipathServiceTypeHandover;

        session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
    });
    
    return session;
}

Конфигурация NSURLSessionConfiguration имеет три режима: 

//默认模式类似于原来的NSURLConnection,可以使用缓存的Cache,Cookie,鉴权
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;

//及时模式不使用缓存的Cache,Cookie,鉴权
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;

//后台模式在后台完成上传下载,创建Configuration对象的时候需要给一个NSString的ID用于追踪完成工作的Session是哪一个
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier

  • СвойствоallowCellularAccess указывает, разрешены ли сотовые соединения.
  • Когда дискреционный атрибут имеет значение ДА, это означает, что система сама выбирает лучшую конфигурацию сетевого подключения, когда программа работает в фоновом режиме.Этот атрибут может сэкономить полосу пропускания через сотовое соединение.

При использовании данных фоновой передачи рекомендуется использовать дискреционный атрибут вместо атрибута разрешенийCellularAccess, поскольку он учитывает доступность Wi-Fi и электропитания. Добавлено: этот флаг позволяет системе выполнять оптимизацию производительности для назначенных задач. Это означает, что устройство будет передавать данные через Wi-Fi только тогда, когда у него достаточно мощности. Если батарея разряжена или имеется только одно сотовое соединение, задача передачи не запустится. Фоновые передачи всегда выполняются в произвольном режиме.​ 

2. Фоновая загрузка

- (void)upload:(NSString *)urlStr data:(NSData *)data headers:(NSDictionary *)headers parameters:(NSDictionary *)parameters name:(NSString *)name filename:(NSString *)filename mimeType:(NSString *)mimeType success:(void (^)(id responseObject))success failure:(void (^)(int code, NSString *message))failure {
    NSURL *url = [NSURL URLWithString:urlStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"POST";
    NSString *string = [NSString stringWithFormat:@"multipart/form-data; charset=utf-8; boundary=%@", kBoundary];
    [request setValue:string forHTTPHeaderField:@"Content-Type"];
    if (headers != nil) {
        for (NSString *key in headers.allKeys) {
            [request setValue:headers[key] forHTTPHeaderField:key];
        }
    }

    NSData *bodyData = [self bodyFormData:data parameters:parameters name:name filename:filename mimeType:mimeType];
    NSString *tempPath = NSTemporaryDirectory();
    NSTimeInterval interval = [NSDate.now timeIntervalSince1970];
    NSString *tempName = [NSString stringWithFormat:@"temp%.0f_%@", interval, filename];
    NSString *tempPath = [tempPath stringByAppendingPathComponent:tempName];
    [bodyData writeToFile:tempPath atomically:YES];

    NSURLSession *session = self.backgroundURLSession;
    NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:tempPath]];
    [uploadTask resume];
}

- (NSData *)bodyFormData:(NSData *)data parameters:(NSDictionary *)parameters name:(NSString *)name filename:(NSString *)filename mimeType:(NSString *)mimeType {
    if (data == nil || data.length == 0) {
        return nil;
    }
    NSMutableData *formData = [NSMutableData data];
    NSData *lineData = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *boundary = [[NSString stringWithFormat:@"--%@", kBoundary] dataUsingEncoding:NSUTF8StringEncoding];
    
    if (parameters != nil) {
        [parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
            [formData appendData:boundary];
            [formData appendData:lineData];
            NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n%@", key, obj];
            [formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding]];
            [formData appendData:lineData];
        }];
    }
    
    [formData appendData:boundary];
    [formData appendData:lineData];
    NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\nContent-Type: %@", name, filename, mimeType];
    [formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding]];
    [formData appendData:lineData];
    [formData appendData:lineData];
    
    [formData appendData:data];
    [formData appendData:lineData];
    [formData appendData:[[NSString stringWithFormat:@"--%@--\r\n", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
    
    return formData;
}

 Есть 4 способа загрузки:

/* Creates an upload task with the given request.  The body of the request will be created from the file referenced by fileURL */
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;

/* Creates an upload task with the given request.  The body of the request is provided from the bodyData. */
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;

- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(nullable NSData *)bodyData completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

Фоновый режим не поддерживает использование методов загрузки с обратными вызовами, иначе будет сообщено об ошибке:

Блоки обработчика завершения не поддерживаются в фоновых сеансах. Вместо этого используйте делегата.

Фоновый режим не поддерживает метод загрузки NSData, в противном случае будет сообщено об ошибке:

Задачи загрузки из NSData не поддерживаются в фоновых сеансах. 

Поэтому, если вы используете фоновый режим для загрузки, выберите метод uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL.

Событие загрузки прокси-сервера NSURLSessionDataDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
    NSLog(@"URLSession didSendBodyData progress: %f" ,totalBytesSent/(float)totalBytesExpectedToSend);
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    NSLog(@"%s", __func__);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    NSMutableData *responseData = self.responsesData[@(dataTask.taskIdentifier)];
    if (!responseData) {
        responseData = [NSMutableData dataWithData:data];
        self.responsesData[@(dataTask.taskIdentifier)] = responseData;
    } else {
        [responseData appendData:data];
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        NSLog(@"URLSession didCompleteWithError %@ failed: %@", task.originalRequest.URL, error);
    }
    NSMutableData *responseData = self.responsesData[@(task.taskIdentifier)];
    if (responseData) {
        NSDictionary *response = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil];
        if (response) {
            NSLog(@"response = %@", response);
        } else {
            NSString *errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
            NSLog(@"responseData = %@", errMsg);
        }
        [self.responsesData removeObjectForKey:@(task.taskIdentifier)];
    } else {
        NSLog(@"responseData is nil");
    }
}

//下载事件
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    NSLog(@"URLSession downloadTask didFinishDownloadingToURL %@", downloadTask.originalRequest.URL);
    NSData *responseData = [NSData dataWithContentsOfURL:location];
    if (responseData != nil) {
        self.responsesData[@(downloadTask.taskIdentifier)] = responseData;
    }
}

3. Справочный запрос

- (void)request:(NSString *)urlStr method:(NSString *)method headers:(NSDictionary *)headers parameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(int code, NSString *message))failure {
    urlStr = [self getFullUrlString:urlStr parameters:parameters];
    NSURL *url = [NSURL URLWithString:urlStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = method;
    if (headers != nil) {
        for (NSString *key in headers.allKeys) {
            [request setValue:headers[key] forHTTPHeaderField:key];
        }
    }

    NSURLSession *session = self.backgroundURLSession;
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
    [task resume];
}

- (NSString *)getFullUrlString:(NSString *)urlStr parameters:(NSDictionary *)parameters {
    NSMutableString *newStr = [NSMutableString stringWithString:urlStr];
    if (parameters.allKeys.count > 0) {
        BOOL isFirst = NO;
        for (NSString *key in parameters) {
            isFirst = YES;
            [newStr appendString:isFirst?@"?":@"&"];
            [newStr appendFormat:@"%@=%@", key, parameters[key]];
        }
    }
    return newStr;
}

4. Фоновая загрузка

- (void)download:(NSString *)urlStr headers:(NSDictionary *)headers parameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(int code, NSString *message))failure {
    urlStr = [self getFullUrlString:urlStr parameters:parameters];
    NSURL *url = [NSURL URLWithString:urlStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    if (headers != nil) {
        for (NSString *key in headers.allKeys) {
            [request setValue:headers[key] forHTTPHeaderField:key];
        }
    }
    NSURLSession *session = self.backgroundURLSession;
    NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request];
    [task resume];
}

5. Взаимодействие сеанса и ApplicationDelegate

При использовании фонового режима BackgroundSession, когда пользователь переключается в фоновый режим во время выполнения задачи, сеанс будет взаимодействовать с ApplicationDelegate, а задача в BackgroundSession продолжит загрузку/выгрузку.

Теперь проанализируйте связь между сеансом и приложением в трех сценариях:

(1) При добавлении нескольких задач программа не переключается в фоновый режим.​ 

В этом случае Task будет загружен нормально в соответствии с настройками NSURLSessionConfiguration и не будет взаимодействовать с ApplicationDelegate.

(2) При добавлении нескольких задач программа переключается в фоновый режим, и все задачи загружаются.

После переключения в фоновый режим делегат сеанса больше не будет получать сообщения, связанные с задачами, пока все задачи не будут завершены. Система вызовет обратный вызов applicationDelegate Application:handleEventsForBackgroundURLSession:completionHandler:, а затем «сообщит» о работе загрузки. Для каждого фона Задача загрузки вызывает URLSession:downloadTask:didFinishDownloadingToURL: (в случае успеха) и URLSession:task:didCompleteWithError: (будет вызываться в случае успеха или неудачи) в делегате сеанса.​ 

AppDelegate:

@property (copy, nonatomic) void(^backgroundSessionCompletionHandler)();

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier 
  completionHandler:(void (^)())completionHandler {
    self.backgroundSessionCompletionHandler = completionHandler;
}

Делегат сеанса

@interface MyViewController()<NSURLSessionDelegate>
@end

@implementation MyViewController

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    if (appDelegate.backgroundSessionCompletionHandler) {
        void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
        appDelegate.backgroundSessionCompletionHandler = nil;
        completionHandler();
    }
    NSLog(@"All tasks are finished");
}

@end

(3) При добавлении нескольких задач программа переключается в фоновый режим, несколько задач загружаются, а затем пользователь переключается на передний план. (Программа не завершается)

После перехода в фоновый режим делегат сеанса по-прежнему не может получать сообщения. После загрузки нескольких задач и последующего перехода на передний план система сначала сообщит о статусе загруженных задач, а затем продолжит загрузку незагруженных задач.Последующий процесс аналогичен первому случаю.​ 

(4) При добавлении нескольких Заданий и переключении программы в фоновый режим. Несколько Заданий выполнено, но есть еще Задачи, которые не были загружены. При выключении принудительного выхода из программы и последующем входе в программу снова. (Программа завершилась)

Поскольку программа уже завершила работу, последующие задачи, которые больше не доступны до загрузки сеанса, должны были завершиться неудачно. Однако те Задачи, которые были успешно загружены, и вновь запущенные программы не имеют возможности прослушать «отчет». После экспериментов было обнаружено, что идентификатор типа NSString, ранее установленный в NSURLSessionConfiguration, работал в это время. Когда идентификаторы одинаковы, как только объект сеанса сгенерирован и установлен делегат, вы можете сразу получить конец выполненной задачи. не сообщать о работе до последнего закрытия программы (успех или неудача).

Guess you like

Origin blog.csdn.net/watson2017/article/details/134240624
ios