Фоновая передача NSURLSession: обратный вызов для каждого видео, загруженного из очереди

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

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

-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier
 completionHandler:(void (^)())completionHandler

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

-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session

Но оба метода вызываются, когда все загрузки заканчиваются. Я поставил 3 видео для загрузки, а затем поставил приложение в фоновом режиме. Оба метода вызываются после того, как все 3 видео были загружены.


Вот что я делаю в этих методах:

AppDelegate

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

ЗагрузитьViewController

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];

    if (appDelegate.backgroundTransferCompletionHandler) 
    {
        void (^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;
        appDelegate.backgroundTransferCompletionHandler = nil;
        completionHandler();
    }

    NSLog(@"All tasks are finished");
}

Можно ли показать пользователю локальное уведомление о загрузке каждого видео? Или мне придется ждать, пока все видео не загрузятся в фоновом режиме?

Если ответ НЕТ, тогда мой вопрос: какова цель этих двух разных обратных вызовов? Что отличает их друг от друга?




Ответы (2)


Можно ли показать пользователю локальное уведомление о загрузке каждого видео? Или мне придется ждать, пока все видео не загрузятся в фоновом режиме?

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

Теоретически вы могли бы создать отдельный фоновый сеанс для каждого из них, но это кажется мне злоупотреблением фоновыми сеансами (целью которых является сокращение времени, затрачиваемого на раскрутку вашего приложения и его запуск в фоновом режиме), и я бы не удивлюсь, если Apple осудит эту практику. Это также потребует более неуклюжей реализации (с несколькими объектами NSURLSession).

Если ответ НЕТ, то мой вопрос в том, какова цель этих двух разных обратных вызовов? Что отличает их друг от друга?

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


Кроме того, LombaX прав, что handleEventsForBackgroundURLSession должен запускать фоновый сеанс. Лично я делаю completionHandler свойством моей оболочки для объекта NSURLSession, поэтому handleEventsForBackgroundURLSession создаст его экземпляр (подготовив его к вызову своих методов делегата) и сохранит completionHandler там. Это логичное место для сохранения обработчика завершения, вам все равно пришлось создавать экземпляр NSURLSession и его делегата, и это избавляет URLSessionDidFinishEventsForBackgroundURLSession от необходимости возвращаться к делегату приложения, чтобы получить сохраненный обработчик завершения.

Правильно это или нет, моя типичная реализация состоит в том, чтобы сделать фоновый объект NSURLSession одноэлементным. Таким образом, я получаю что-то вроде:

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {    
    [BackgroundSession sharedSession].savedCompletionHandler = completionHandler;
}

Это убивает двух зайцев одним выстрелом, запуская фоновую NSURLSession и сохраняя файл completionHandler.

person Rob    schedule 23.05.2016
comment
В моем коде handleEventsForBackgroundURLSession не вызывается при завершении загрузки каждого видео. Это нормально ? Потому что LombaX говорит, что он должен вызываться каждый раз, когда видео заканчивает загрузку. - person NSPratik; 23.05.2016
comment
Роб, можешь ответить? - person NSPratik; 23.05.2016
comment
Ты уверен насчет этого? Приложение будет перезапущено в фоновом режиме только с помощью handleEventsForBackgroundURLSession, когда будут выполнены все загрузки, связанные с этим сеансом, а не по одному. Я помню обратное, однако я использовал NSURLSession на этом уровне несколько лет назад, и я мог бы быть неправильный. Что касается другого предложения (использовать фоновый сеанс как синглтон), это тот же подход, который я использовал в своем демонстрационном проекте (прикреплен к моему ответу), хотя в этом случае я забыл добавить вызов обработчик завершения - person LombaX; 23.05.2016
comment
Да, LombaX, я сделал логи обоими способами. Они оба вызываются после окончания загрузки. - person NSPratik; 23.05.2016
comment
Я подтверждаю поведение, объясненное Робом. Однако кажется, что поведение отличается, когда приложение убито (процесс не запущен) и когда приложение приостановлено (процесс все еще жив). В первом случае iOS снова разбудит приложение, только когда все загрузки, связанные с сеансом, будут завершены (проверены). Во втором случае делегат сеанса вызывается напрямую (еще не тестировался), поясняется здесь. Я обновил свой исходный пост и провел несколько тестов - person LombaX; 23.05.2016
comment
После некоторых тестов кажется, что даже последнее утверждение неверно. Посмотрите на мой оригинальный пост. Документы Apple кажутся неправильными в отношении объясненного поведения ... или я что-то не понимаю :-( - person LombaX; 23.05.2016
comment
@NSPratik - Да, приложение перезапускается только после завершения всех загрузок (или пользователь вручную перезапустил приложение). И я со всем уважением не согласен с утверждением LombaX о том, что документация неверна. - person Rob; 23.05.2016
comment
@Rob Я сказал, что документ неверен ИЛИ что я что-то неправильно понимаю :-) это не первый раз, когда я неправильно понимаю документы Apple ... иногда это оставляет читателю слишком много интерпретаций. - person LombaX; 23.05.2016
comment
Спасибо, ребята, что поделились мыслями. Я ясно теперь. Если я продвигаю локальное уведомление только после завершения всех загрузок, это абсолютно нормально. Но дискуссия привела меня к новому пути. Загрузка не работает в моем случае, когда приложение убито. В чем может быть причина? - person NSPratik; 24.05.2016
comment
@NSPratik - Правильно. Если пользователь убивает приложение вручную (например, с помощью двойного нажатия кнопки «Домой»), это останавливает фоновые запросы. Но если приложение завершается в ходе обычной работы (например, пользователь переходит в другое приложение, а ваше приложение приостанавливается; даже если пользователь использует какое-либо другое приложение, которое занимает так много памяти, что ваше приложение в конечном итоге отбрасывается из-за нехватки памяти), то ваш фоновые запросы продолжаются, и ваше приложение будет перезапущено в фоновом режиме, когда запросы закончатся. - person Rob; 24.05.2016
comment
Короче говоря, загрузка не может продолжаться, если пользователь удалит приложение из окна многозадачности - так ли это? Я читал документацию Apple, и они также использовали слово Killed with Suspended, поэтому я пытался это сделать. - person NSPratik; 24.05.2016
comment
@NSPratik - Кажется, ты просишь меня повторяться каждый раз, когда я что-то говорю. :) Да, если пользователь убьет приложение вручную двойным нажатием кнопки «Домой», фоновые запросы будут отменены и не будут выполняться. - person Rob; 24.05.2016

Проблема здесь в том, что вы используете NSURLSessionDelegate, который дает вам информацию о текущем сеансе загрузки. Однако вы хотите знать информацию об отдельных задачах, а не обо всем сеансе. По этой причине вам следует просмотреть NSURLSessionTaskDelegate или NSURLSessionDownloadDelegate

В частности, используя NSURLSessionDownloadDelegate, вы должны реализовать этот метод делегата:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location

Если приложение находится в фоновом режиме, когда загрузка завершена, этот метод не будет вызываться автоматически. Однако система выполняет вызов application:handleEventsForBackgroundURLSession:completionHandler:, что дает вам возможность восстановить сеанс и отреагировать на события (например, запустить уведомление, как вы просите). Дополнительная информация здесь:

В iOS, когда фоновая передача завершается или требуются учетные данные, если ваше приложение больше не работает, ваше приложение автоматически перезапускается в фоновом режиме, а UIApplicationDelegate приложения отправляется application:handleEventsForBackgroundURLSession:completionHandler: сообщение. Этот вызов содержит идентификатор сеанса, вызвавшего запуск вашего приложения. Затем ваше приложение должно сохранить этот обработчик завершения перед созданием объекта фоновой конфигурации с тем же идентификатором и созданием сеанса с этой конфигурацией. Вновь созданный сеанс автоматически повторно связывается с текущей фоновой активностью.

Наконец, несколько лет назад я сделал проект с открытым исходным кодом, обертку над NSURLSession. Этот проект был создан для iOS 7, поэтому в нем могут использоваться некоторые устаревшие методы, однако часть, охватываемая этим ответом, по-прежнему действительна. Ссылка на FLDownloader

EDIT После ответа Роба я сделал некоторую проверку. Кажется, что поведение приложения в приостановленном состоянии и приложения в убитом состоянии отличается.

  • кажется, что когда приложение закрыто (убито), система разбудит его, вызвав application:handleEventsForBackgroundURLSession:completionHandler:, только когда все загрузки будут завершены. Я попытался подключить XCode к своему iPhone, и это кажется правильным.
  • Однако по этой ссылке в «Вопросы фоновой передачи», кажется, что если приложение находится в «приостановленном» состоянии, оно говорит:

Если какая-либо задача была завершена, пока ваше приложение было приостановлено, затем вызывается метод делегата URLSession:downloadTask:didFinishDownloadingToURL: с задачей и URL-адресом нового загруженного файла, связанного с ней.

ИЗМЕНИТЬ

Последнее утверждение, даже если оно подтверждается документами Apple, кажется неверным. Проверял лично с iPhone 6S, iOS 9.3.2, XCode и Instruments. Я начал две загрузки и закрыл приложение (состояние приостановки, подтвержденное монитором активности Istruments — процесс все еще был активен, но процессорное время не потреблялось), но метод URLSession:downloadTask:didFinishDownloadingToURL: не вызывался. Однако, когда обе загрузки были завершены, был вызван application:handleEventsForBackgroundURLSession:completionHandler:.

person LombaX    schedule 23.05.2016
comment
Спасибо, LombaX, будет ли он вызываться, даже если приложение находится в фоновом режиме? - person NSPratik; 23.05.2016
comment
Я хочу показывать локальное уведомление для каждого загруженного видео, только если приложение работает в фоновом режиме. - person NSPratik; 23.05.2016
comment
AFAIR не автоматически. Если загрузка завершается, когда приложение находится в фоновом режиме, то повторно запускается вызов application:handleEventsForBackgroundURLSession:completionHandler, то вам необходимо перестроить сеанс и зарегистрироваться для событий - person LombaX; 23.05.2016
comment
Как это сделать: перестроить сессию и зарегистрироваться на события? И рекомендуется ли это делать? - person NSPratik; 23.05.2016
comment
Да, не только рекомендуется, но и является официальным методом восстановления сеанса из фона. При вызове handleEvents... передается идентификатор сеанса. Когда вы инициализируете новый сеанс с этим идентификатором, он будет связан с фоновыми задачами. Посмотрите новую информацию и ссылку, указанную в ответе. - person LombaX; 23.05.2016
comment
handleEventsForBackgroundURLSession не будет вызываться, пока не скачает все видео. Как я получу возможность создать новый сеанс с тем же идентификатором? - person NSPratik; 23.05.2016
comment
handleEventsForBackgroundURLSession вызывается, даже если одна загрузка завершена, а другая все еще выполняется. Этот метод вызывается каждый раз, когда вам нужно обработать событие для фонового сеанса, например, перемещение файла с временного пути (файлы загружаются по временному пути) на окончательный путь после завершения загрузки. Если в вашем случае метод не вызывается, то проблему придется искать в другом месте. Вы действительно уверены в этом? - person LombaX; 23.05.2016
comment
Вероятно, это происходит из-за того, что вы неправильно отвечаете на handleEventsForBackgroundURLSession. В этом методе, прежде всего, вы должны воссоздать сеанс с правильным делегатом для ответа на событие. Затем, когда вы закончите управлять своими обратными вызовами, вы должны вызвать обработчик завершения с помощью completionHandler(). В противном случае система может предположить, что вы не готовы отвечать на обратные вызовы, а затем перестанет перезванивать вам (это только предположение). - person LombaX; 23.05.2016
comment
Я проверяю. В связи с этим приложение будет перезапущено в фоновом режиме только с помощью handleEventsForBackgroundURLSession, когда будут выполнены все загрузки, связанные с этим сеансом, а не по одному, возможно, что-то изменилось в последних версиях, я помню, что ранее загрузка была завершена -одним. Я проверяю свои приложения - person LombaX; 23.05.2016
comment
Давайте продолжим это обсуждение в чате. - person NSPratik; 23.05.2016
comment
@LombaX - эта цитата из документации Apple неверна. (Возможно, плохо написано, но не неверно.) Они ничего не говорят о точном времени, когда приложение будет запущено снова, просто о том, что при его перезапуске будут вызываться эти методы делегата завершения загрузки. Это все, что они говорят. - person Rob; 23.05.2016
comment
@ Роб, я могу только согласиться с тобой ... :-) - person LombaX; 23.05.2016