Как реализовать фоновую выборку с помощью задачи с данными? - PullRequest
0 голосов
/ 23 мая 2019

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

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

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

"Одна распространенная ошибка, которую, как мы думаем, некоторые люди совершили в прошлом, предполагает, что при работе в фоновом режиме для обработки таких вещей, как обновление фоновой выборки ... ... они были обязаны использовать фоновый сеанс Теперь, используя фоновую загрузку или загрузку ... ... работает очень хорошо, но особенно для больших загрузок. Когда вы запускаете обновление для выборки в фоновом режиме ... у вас есть от 30 до 60 секунд для запуска в фоновом режиме. «[до того, как приложение будет приостановлено]» Если у вас есть такая же небольшая сетевая задача, которая может быть завершена в течение этого времени, то вполне нормально сделать это в рамках 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: executeFetchWithCompletionHandler: метод, пока пользователь не перезапустит приложение

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

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

Ответы [ 2 ]

0 голосов
/ 23 мая 2019

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

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

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

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

    С этой целью я бы предложил использовать Unified Logging вместо 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, например ::101036

    enter image description here

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

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

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

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

    Итог, у вас нет контроля, когда это происходит. Например, убедитесь, что вы подключены к источнику питания и подключены к 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);
    }
});

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

0 голосов
/ 23 мая 2019

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

NSURLSession *defaultSession = [NSURLSession sharedSession];

вместо

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

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

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

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

...