Статьи

Сеть с NSURLSession: часть 2

В предыдущем уроке я познакомил вас с NSURLSession . Я говорил о его преимуществах по сравнению с NSURLConnection и о том, как использовать NSURLSession для простых задач, таких как получение данных из веб-службы и загрузка изображений из Интернета. В этом руководстве мы более подробно рассмотрим параметры конфигурации NSURLSession и способы отмены и возобновления задачи загрузки. У нас есть много возможностей, так что давайте начнем.


Как мы видели в предыдущем руководстве , сеанс, экземпляр NSURLSession , является настраиваемым контейнером для размещения сетевых запросов. Конфигурация сеанса обрабатывается экземпляром NSURLSessionConfiguration .

Объект конфигурации сеанса — это не что иное, как словарь свойств, который определяет, как ведет себя сеанс. Сеанс имеет один объект конфигурации сеанса, который задает политики cookie, безопасности и кэширования, максимальное количество подключений к хосту, время NSURLConnection ресурса и сети и т. Д. Это значительное улучшение по сравнению с NSURLConnection , который основывался на глобальном объекте конфигурации с большим меньше гибкости

Как только сеанс создан и настроен экземпляром NSURLSessionConfiguration , конфигурация сеанса не может быть изменена. Если вам нужно изменить конфигурацию сеанса, вы должны создать новый сеанс. Помните, что можно скопировать конфигурацию сеанса и изменить ее, но эти изменения не влияют на сеанс, из которого была скопирована конфигурация.

Класс NSURLSessionConfiguration предоставляет три конструктора фабрики для создания экземпляров стандартных конфигураций, defaultSessionConfiguration , ephemeralSessionConfiguration и backgroundSessionConfiguration . Первый метод возвращает копию объекта конфигурации сеанса по умолчанию , в результате чего сеанс ведет себя подобно объекту NSURLConnection в его стандартной конфигурации. Изменение конфигурации сеанса, полученной с помощью defaultSessionConfiguration фабрики defaultSessionConfiguration , не меняет конфигурацию сеанса по умолчанию, копию которой она представляет.

Объект конфигурации сеанса, созданный путем вызова метода фабрики ephemeralSessionConfiguration гарантирует, что в полученном сеансе не будет постоянного хранилища для файлов cookie, кэшей или учетных данных. Другими словами, файлы cookie, кэши и учетные данные хранятся в памяти. Поэтому эфемерные сеансы идеальны, если вам нужно реализовать приватный просмотр, что было просто невозможно до появления NSURLSession .

backgroundSessionConfiguration: метод backgroundSessionConfiguration: создает объект конфигурации сеанса, который позволяет загружать и выгружать вне процесса. Задачи загрузки и выгрузки управляются фоновым демоном и продолжают выполняться, даже если приложение приостановлено или происходит сбой. Мы поговорим больше о фоновых сессиях позже в этой серии.

Как мы видели в предыдущем уроке, создать объект конфигурации сеанса очень просто. В приведенном ниже примере я использовал фабричный метод defaultSessionConfiguration для создания экземпляра NSURLSessionConfiguration . Настроить объект конфигурации сеанса так же просто, как изменить его свойства, как показано в примере. Затем мы можем использовать объект конфигурации сеанса для создания экземпляра объекта сеанса. Объект сеанса служит фабрикой для данных, задач загрузки и выгрузки, причем каждая задача соответствует одному запросу. В приведенном ниже примере мы запрашиваем API поиска iTunes, как мы делали в предыдущем руководстве.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// Create Session Configuration
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
 
// Configure Session Configuration
[sessionConfiguration setAllowsCellularAccess:YES];
[sessionConfiguration setHTTPAdditionalHeaders:@{ @»Accept» : @»application/json» }];
 
// Create Session
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
 
// Send Request
NSURL *url = [NSURL URLWithString:@»https://itunes.apple.com/search?term=apple&media=software»];
[[session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    NSLog(@»%@», [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]);
}] resume];

В примере также показано, как легко добавить пользовательские заголовки, установив свойство HTTPAdditionalHeaders объекта конфигурации сеанса. Прелесть NSURLSession API в том, что каждый запрос, проходящий через сеанс, настраивается объектом конфигурации сеанса. Например, добавление заголовков аутентификации в набор запросов становится простым делом.


В предыдущем уроке я показал вам, как загрузить изображение с помощью NSURLSession API. Однако сетевые подключения ненадежны, и слишком часто случается, что загрузка завершается неудачно из-за нестабильного сетевого подключения. К счастью, возобновить загрузку не сложно с помощью NSURLSession API. В следующем примере я покажу вам, как отменить и возобновить загрузку изображения.

Прежде чем мы подробнее рассмотрим возобновление задачи загрузки, важно понять разницу между отменой и приостановкой задачи загрузки. Можно приостановить загрузку и возобновить ее позднее. Однако отмена задачи загрузки останавливает задачу, и возобновить ее позже невозможно. Однако есть одна альтернатива. Можно отменить задачу загрузки, вызвав на нем cancelByProducingResumeData: . Он принимает обработчик завершения, который принимает один параметр — объект NSData который используется для возобновления загрузки позднее, вызывая downloadTaskWithResumeData: или downloadTaskWithResumeData:completionHandler: в объекте сеанса. Объект NSData содержит необходимую информацию для возобновления задачи загрузки с того места, где оно было прервано.

Откройте проект, который мы создали в предыдущем уроке, или загрузите его здесь . Мы начнем с добавления в пользовательский интерфейс двух кнопок: одна для отмены загрузки и одна для возобновления загрузки. В заголовочном файле контроллера представления создайте розетку и действие для каждой кнопки, как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
#import <UIKit/UIKit.h>
 
@interface MTViewController : UIViewController
 
@property (weak, nonatomic) IBOutlet UIButton *cancelButton;
@property (weak, nonatomic) IBOutlet UIButton *resumeButton;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
 
— (IBAction)cancel:(id)sender;
— (IBAction)resume:(id)sender;
 
@end

Откройте основную раскадровку проекта и добавьте две кнопки в представление контроллера представления. Расположите кнопки, как показано на скриншоте ниже, и соедините каждую кнопку с соответствующей розеткой и действием.

Обновите пользовательский интерфейс.

Нам нужно сделать рефакторинг, чтобы все работало правильно. Откройте MTViewController.m и объявите одну переменную экземпляра и два свойства. Переменная экземпляра session будет содержать ссылку на сеанс, который мы будем использовать для загрузки изображения.

01
02
03
04
05
06
07
08
09
10
#import «MTViewController.h»
 
@interface MTViewController () <NSURLSessionDelegate, NSURLSessionDownloadDelegate> {
    NSURLSession *_session;
}
 
@property (strong, nonatomic) NSURLSessionDownloadTask *downloadTask;
@property (strong, nonatomic) NSData *resumeData;
 
@end

Нам также нужно реорганизовать метод viewDidLoad , но сначала я бы хотел реализовать метод getter для сеанса. Его реализация довольно проста, как вы можете видеть ниже. Мы создаем объект конфигурации сеанса с помощью defaultSessionConfiguration фабрики defaultSessionConfiguration и defaultSessionConfiguration с его помощью объект сеанса. Контроллер представления служит делегатом сеанса.

01
02
03
04
05
06
07
08
09
10
11
— (NSURLSession *)session {
    if (!_session) {
        // Create Session Configuration
        NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
 
        // Create Session
        _session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
    }
 
    return _session;
}

После реализации средства viewDidLoad к viewDidLoad метод viewDidLoad становится намного проще. Мы создаем задачу загрузки, как мы это делали в предыдущем руководстве, и сохраняем ссылку на задачу в downloadTask . Затем мы просим задачу загрузки resume .

1
2
3
4
5
6
7
8
9
— (void)viewDidLoad {
    [super viewDidLoad];
 
    // Create Download Task
    self.downloadTask = [self.session downloadTaskWithURL:[NSURL URLWithString:@»http://cdn.tutsplus.com/mobile/uploads/2014/01/5a3f1-sample.jpg»]];
 
    // Resume Download Task
    [self.downloadTask resume];
}

Действие cancel: содержит логику отмены задачи загрузки, которую мы только что создали. Если downloadTask не равно nil , мы вызываем cancelByProducingResumeData: для задачи. Этот метод принимает один параметр — блок завершения. Блок завершения также принимает один параметр, экземпляр NSData . Если resumeData не nil , мы сохраняем ссылку на объект данных в свойстве resumeData контроллера представления.

Если загрузка не возобновляется, параметр resumeData блока завершения имеет resumeData nil . Не каждая загрузка возобновляема, поэтому важно проверить, является ли resumeData допустимым объектом NSData .

01
02
03
04
05
06
07
08
09
10
11
12
— (IBAction)cancel:(id)sender {
    if (!self.downloadTask) return;
 
    // Hide Cancel Button
    [self.cancelButton setHidden:YES];
 
    [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
        if (!resumeData) return;
        [self setResumeData:resumeData];
        [self setDownloadTask:nil];
    }];
}

Возобновить загрузку после отмены очень просто. В действии resume: мы проверяем, установлено ли свойство resumeData контроллера представления. Если resumeData является допустимым объектом NSData , мы сообщаем объекту session создать новую задачу загрузки и передаем ей объект NSData . Это все, что нужно объекту session для воссоздания задачи загрузки, которую мы отменили в действии cancel: Затем мы resumeData задаче загрузки resume и устанавливаем значение resumeData nil .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
— (IBAction)resume:(id)sender {
    if (!self.resumeData) return;
 
    // Hide Resume Button
    [self.resumeButton setHidden:YES];
 
    // Create Download Task
    self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
 
    // Resume Download Task
    [self.downloadTask resume];
 
    // Cleanup
    [self setResumeData:nil];
}

Создайте проект и запустите приложение в iOS Simulator или на физическом устройстве. Загрузка должна начаться автоматически. Нажмите кнопку отмены, чтобы отменить загрузку, и нажмите кнопку возобновления, чтобы возобновить загрузку.

Есть ряд деталей, о которых нам нужно позаботиться. Прежде всего, кнопки не всегда должны быть видны. Мы будем использовать наблюдение значения ключа, чтобы при необходимости показывать и скрывать кнопки. В viewDidLoad кнопки и добавьте контроллер представления в качестве наблюдателя для resumeData ключа resumeData и downloadTask .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
— (void)viewDidLoad {
    [super viewDidLoad];
 
    // Add Observer
    [self addObserver:self forKeyPath:@»resumeData» options:NSKeyValueObservingOptionNew context:NULL];
    [self addObserver:self forKeyPath:@»downloadTask» options:NSKeyValueObservingOptionNew context:NULL];
 
    // Setup User Interface
    [self.cancelButton setHidden:YES];
    [self.resumeButton setHidden:YES];
 
    // Create Download Task
    self.downloadTask = [self.session downloadTaskWithURL:[NSURL URLWithString:@»http://cdn.tutsplus.com/mobile/uploads/2014/01/5a3f1-sample.jpg»]];
 
    // Resume Download Task
    [self.downloadTask resume];
}

В observeValueForKeyPath:ofObject:change:context: мы скрываем кнопку отмены, если resumeData равен nil и скрываем кнопку возобновления, если downloadTask равен nil . Создайте проект и запустите приложение еще раз, чтобы увидеть результат. Это лучше. Правильно?

01
02
03
04
05
06
07
08
09
10
11
12
— (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@»resumeData»]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.resumeButton setHidden:(self.resumeData == nil)];
        });
         
    } else if ([keyPath isEqualToString:@»downloadTask»]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.cancelButton setHidden:(self.downloadTask == nil)];
        });
    }
}
Как указывает в комментариях Джордж Ян, мы не знаем, observeValueForKeyPath:ofObject:change:context: ли в observeValueForKeyPath:ofObject:change:context: Поэтому важно обновить пользовательский интерфейс в блоке GCD (Grand Central Dispatch), который вызывается в главной очереди.

Есть один ключевой аспект NSURLSession о котором я еще не говорил, недействительность сессии. Сеанс сохраняет строгую ссылку на своего делегата, что означает, что делегат не освобождается, пока сеанс активен. Чтобы разорвать этот ссылочный цикл, сеанс должен быть признан недействительным. Когда сеанс признан недействительным, активные задачи отменяются или завершаются, и делегату отправляется сообщение URLSession:didBecomeInvalidWithError: и сеанс освобождает своего делегата.

Есть несколько мест, которые мы можем сделать недействительными. Поскольку контроллер представления загружает только одно изображение, сеанс может быть признан недействительным по окончании загрузки. Взгляните на обновленную реализацию URLSession:downloadTask:didFinishDownloadingToURL: Кнопка отмены также скрыта после завершения загрузки.

01
02
03
04
05
06
07
08
09
10
11
12
— (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    NSData *data = [NSData dataWithContentsOfURL:location];
 
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.cancelButton setHidden:YES];
        [self.progressView setHidden:YES];
        [self.imageView setImage:[UIImage imageWithData:data]];
    });
 
    // Invalidate Session
    [session finishTasksAndInvalidate];
}

Пример проекта, который мы создали в этом руководстве, представляет собой упрощенную реализацию отмены и возобновления загрузки. В ваших приложениях может потребоваться записать объект resumeData на диск для последующего использования, и может быть возможно, что несколько задач загрузки выполняются одновременно. Хотя это добавляет сложности, основные принципы остаются прежними. Обязательно предотвращайте утечки памяти, всегда отменяя сеанс, который вам больше не нужен.