Как реализовать фоновую выборку с помощью задачи данных?

Я пытаюсь реализовать фоновую выборку в приложении iOS. В официальной документации указано:

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

Более того, в этом видео WWDC 2014 на 51:50 между лучшими практиками говорится:

"Одна из распространенных ошибок, которую, как нам кажется, совершали некоторые люди в прошлом, заключается в том, что они предполагают, что при работе в фоновом режиме для обработки таких вещей, как обновление фоновой выборки... ... они должны были использовать фоновый сеанс. Теперь, использование фоновой загрузки или загрузки... ...работает очень хорошо, но особенно для больших загрузок. приложение приостанавливается] «Если у вас есть такая же небольшая сетевая задача, которая может быть завершена в течение этого времени, вполне нормально сделать это в процессе или по умолчанию NSURLSession ... ..."

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

- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    NSLog(@"AppDelegate application:performFetchWithCompletionHandler:");
    BOOL thereIsALoggedInUser = [self thereIsALoggedInUser];
    if (!(thereIsALoggedInUser && [application isProtectedDataAvailable])){
        completionHandler(UIBackgroundFetchResultNoData);
        NSLog(@"completionHandler(UIBackgroundFetchResultNoData): thereIsALoggedInUser: %@, isProtectedDataAvailable: %@",
          thereIsALoggedInUser ? @"YES" : @"NO",
          [application isProtectedDataAvailable] ? @"YES" : @"NO");
        return;
    }

    NSArray<NSString*> *objectsIWantToSend = [self getObjectsIWantToSend];    
    if (!objectsIWantToSend) {
        [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
        completionHandler(UIBackgroundFetchResultNoData);
        NSLog(@"No post data");
    } else {
        [self sendToServer:objectsIWantToSend usingCompletionHandler:completionHandler];
    }
}  

-(void) sendToServer:(NSArray<NSString*> *)objectsArray usingCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    NSLog(@"AppDelegate - sendToServer:usingCompletionHandler:");
    __block BOOL thereWasAnError = NO;
    __block BOOL thereWasAnUnsuccessfullHttpStatusCode = NO;    

    dispatch_group_t sendTransactionsGroup = dispatch_group_create();

    NSURLSession *postSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];

    for (NSString *objectToPost in objectsArray) {

        dispatch_group_enter(sendTransactionsGroup);

        NSMutableURLRequest *request = [NSMutableURLRequest new];
        [request setURL:[NSURL URLWithString:@"restApiUrl"]];
        [request setHTTPMethod:@"POST"];
        request = [Util setStandardContenttypeAuthorizationAndAcceptlanguageOnRequest:request];
        [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:@{ @"appData": objectToPost } options:kNilOptions error:nil]];

        NSURLSessionDataTask *dataTask = [postSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){

            NSInteger statusCode = -1;
            if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
                statusCode = [(NSHTTPURLResponse *)response statusCode];
            }

            if (error) {
                NSLog(@"Error");
                thereWasAnError = YES;
            } else if (statusCode != 201) {
                NSLog(@"Error: http status code: %d", httpResponse.statusCode);
                thereWasAnUnsuccessfullHttpStatusCode = YES;
            } else {
                [self parseAndStoreData:data];
            }

            dispatch_group_leave(sendTransactionsGroup);
        }];

        [dataTask resume];
    }

    dispatch_group_notify(sendTransactionsGroup, dispatch_get_main_queue(),^{
        if (thereWasAnError) {
            completionHandler(UIBackgroundFetchResultFailed);
        } else if (thereWasAnUnsuccessfullHttpStatusCode) {
            [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
            completionHandler(UIBackgroundFetchResultNoData);
        } else {
            completionHandler(UIBackgroundFetchResultNewData);
        }
    });
}

В моем приложении: метод didFinishLaunchingWithOptions: я устанавливаю для BackgroundFetchInterval значение MinimumInterval, используя

[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

Я не закрываю приложение на устройстве принудительно, потому что знаю, что в этом случае система не будет вызывать метод AppDelegate application:performFetchWithCompletionHandler: до тех пор, пока пользователь не перезапустит приложение.

Я использую несколько тестовых случаев с несколькими комбинациями устройств, заблокированных/разблокированных, заряжающихся/не заряжающихся, все в сети Wi-Fi, потому что у них нет симов.

Когда я запускаю вышеупомянутый тип кода, используя Xcode -> Debug -> Simulate background fetch или используя схему сборки с выбранной опцией «запуск из-за события фоновой выборки», все идет хорошо: сервер получает мой почтовый запрос, приложение правильно получает ответ сервера, и выполняется блок dispatch_group_notify. Тестируя код через реальное устройство, я не получаю никакого результата, даже вызова на сервер.


person Daniele Cux    schedule 23.05.2019    source источник
comment
Итак, вы везде ведете логи в своем делегате приложения и теперь смотрите это в консоли? И вы видите, что он вызывает applicationWillResignActive, но никогда не performFetchWithCompletionHandler? (Если это так, то sendToServer не имеет значения.) Тогда я бы посоветовал две вещи: 1. Полностью удалить и переустановить приложение (если вы еще этого не сделали), чтобы сбросить фоновую загрузку. 2. Создайте новый MCVE, который выполняет фоновую выборку (с некоторой NSURLSession задачей) и убедитесь, что в вашем приложении нет других помех. с процессом.   -  person Rob    schedule 24.05.2019
comment
Спасибо, подход MCVE был правильным: я протестировал свою реализацию в специальном приложении, используя конечную точку, предоставленную веб-сайтом Postman Echo, и все прошло хорошо: был вызван метод application:performFetch, а использование dispatch_group не дало мне никаких результатов. вообще беда. Мне пока не удалось диагностировать проблему, но я могу подтвердить, что задача данных выполняет свою работу. Я обнаружил, что в исходном коде приложения есть дублированный файл Info.plist, и что в одной из его копий не было пары ключ-значение UIBackgroundModes-fetch, сейчас я изучаю эту проблему.   -  person Daniele Cux    schedule 27.05.2019


Ответы (2)


Либо фоновая выборка не инициирована, либо во время фоновой выборки что-то идет не так. Нам нужно сделать небольшую отладку, чтобы определить, в чем проблема.

Итак, пара наблюдений:

  • Правильно ли я предполагаю, что вы вызываете это из своего метода performFetchWithCompletionHandler вашего делегата приложения, передавая параметр обработчика завершения? Возможно, вы можете поделиться своей реализацией этого метода.

  • Чрезвычайно полезно иметь возможность отслеживать ваше приложение, пока оно работает на физическом устройстве, но не подключено к отладчику Xcode (поскольку тот факт, что оно запускается из отладчика, меняет жизненный цикл приложения).

    С этой целью я предлагаю использовать унифицированное ведение журнала вместо NSLog , таким образом вы можете легко отслеживать операторы os_log вашего устройства из консоли macOS, даже если вы не подключены к Xcode (в отличие от запуска из Xcode, унифицированное ведение журнала не влияет на жизненный цикл приложения). Затем, наблюдая в консоли macOS, вы можете подтвердить инициацию фоновой выборки и посмотреть, произошла ли ошибка и где. См. видео WWDC Единое ведение журнала и отслеживание действий.

    Итак, импортируем os.log:

    @import os.log;
    

    Определите переменную для вашего log:

    os_log_t log;
    

    Установить его:

    NSString *subsystem = [[NSBundle mainBundle] bundleIdentifier];
    log = os_log_create([subsystem cStringUsingEncoding:NSUTF8StringEncoding], "background.fetch");
    

    Теперь вы можете регистрировать сообщения, например.

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        os_log(log, "%{public}s", __FUNCTION__);
        return YES;
    }
    

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

    введите здесь описание изображения

    См. это видео для получения дополнительной информации.

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

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

  • Как долго вы ждали начала фоновой выборки? Находится ли устройство в состоянии, позволяющем своевременно вызывать фоновую выборку?

    Суть в том, что вы не имеете никакого контроля, когда это происходит. Например, убедитесь, что вы подключены к источнику питания и Wi-Fi, чтобы дать ему больше шансов быстро выстрелить. (ОС учитывает эти факторы при принятии решения о том, когда выполнять выборку.) В прошлый раз, когда я проверял этот шаблон, для первой фоновой выборки в этой оптимальной ситуации потребовалось около 10 минут.


Кроме того, независимо от вашего вопроса, вам действительно следует проверить, чтобы убедиться, что response был NSHTTPURLResponse. В маловероятном случае, если это не так, вы хотите, чтобы ваше приложение вылетало при попытке получить statusCode? Таким образом:

os_log(log, "%{public}s", __FUNCTION__);

__block BOOL thereWasAnError = NO;
__block BOOL thereWasAnUnsuccessfullHttpStatusCode = NO;

dispatch_group_t sendTransactionsGroup = dispatch_group_create();

NSURLSession *postSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];

for (NSString *objectToPost in objectArray) {
    dispatch_group_enter(sendTransactionsGroup);

    NSMutableURLRequest *request = [NSMutableURLRequest new];
    [request setURL:[NSURL URLWithString:@"restApiUrl"]];
    [request setHTTPMethod:@"POST"];
    request = [Util setStandardContenttypeAuthorizationAndAcceptlanguageOnRequest:request];
    [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:@{ @"appData": objectToPost } options:kNilOptions error:nil]];

    NSURLSessionDataTask *dataTask = [postSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){

        NSInteger statusCode = -1;
        if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
            statusCode = [(NSHTTPURLResponse *)response statusCode];
        }

        if (error) {
            os_log(log, "%{public}s: Error: %{public}@", __FUNCTION__, error);
            thereWasAnError = YES;
        } else if (statusCode < 200 || statusCode > 299) {
            os_log(log, "%{public}s: Status code: %ld", __FUNCTION__, statusCode);
            thereWasAnUnsuccessfullHttpStatusCode = YES;
        } else {
            [self parseAndStoreData:data];
        }

        dispatch_group_leave(sendTransactionsGroup);
    }];

    [dataTask resume];
}

dispatch_group_notify(sendTransactionsGroup, dispatch_get_main_queue(),^{
    if (thereWasAnError) {
        completionHandler(UIBackgroundFetchResultFailed);
    } else if (thereWasAnUnsuccessfullHttpStatusCode) {
        // [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
        completionHandler(UIBackgroundFetchResultNoData);
        return;
    } else {
        completionHandler(UIBackgroundFetchResultNewData);
    }
});

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

person Rob    schedule 23.05.2019
comment
Спасибо, что указали мне на этот отличный API, теперь я регистрирую iPhone через консоль Mac, как вы предложили (при реализации его использования я использую тот факт, что nslog перенаправляется на его использование). Ваши предположения верны, я добавлю их в описание того, что я делаю по своему вопросу. О подробностях, которые вы меня спрашивали: я несколько часов ждал вызова PerformFetchWithCompletionHandler на моем AppDelegate, но ничего не произошло: мобильные телефоны заряжены и подключены к Wi-Fi. Хотя у них нет сима. Один заблокирован, другой нет - person Daniele Cux; 24.05.2019

Поскольку NSURLSession не изменяется в коде здесь, вы должны использовать

NSURLSession *defaultSession = [NSURLSession sharedSession];

вместо

NSURLSession *postSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];

Проверьте принятый ответ здесь:

NSURLSessionDataTask dataTaskWithURL обработчик завершения не вызывается

Надеюсь это поможет.

person BhargavR    schedule 23.05.2019
comment
Спасибо, сначала я использовал общий сеанс, но он не работал. Я решил придерживаться сеанса по умолчанию, потому что в официальном видео, которое я разместил, прямо сказано, что оно должно работать. - person Daniele Cux; 23.05.2019