如何使用数据任务实施后台提取?

时间:2019-05-23 17:41:24

标签: ios nsurlsessiondatatask background-fetch

我正在尝试在iOS应用中实现后台抓取。官方documentation中指出:

”“您不必使用后台会话进行所有后台网络活动……。声明适当后台模式的应用程序可以使用默认的URL会话和数据任务,就像它们在前景。”

此外,在WWDC 2014 video中,最佳做法之间的时间为51:50:

”“我们认为某些人过去犯的一个常见错误是,假设在后台运行时处理诸如后台获取更新之类的事情…………要求他们使用后台会话。现在,使用后台上传或下载... ...确实很好,但是特别适用于大型下载。当您在运行后台获取更新时...在后台运行大约需要30到60秒“ [在应用程序暂停之前]“如果您有相同的小型网络任务可以在这段时间内完成,则完全可以在进程内或默认的NSURLSession中完成此任务……”

我正处于一项小型网络任务的情况下:我想与服务器上的rest 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:方法,直到用户重新启动应用程序为止

我正在使用几个测试用例,这些设备在wifi网络下都具有锁定/解锁,充电/不充电的几种组合,因为它们没有模拟物

当我使用Xcode->调试->模拟后台获取或使用选择“由于后台获取事件而启动”选项的构建方案来运行上述代码时,一切顺利:服务器收到了我的发布请求,该应用正确返回了服务器响应,并且执行了dispatch_group_notify块。通过实际的设备测试代码我没有结果,甚至没有调用服务器

2 个答案:

答案 0 :(得分:0)

由于未在此处的代码中修改NSURLSession,因此应使用

NSURLSession *defaultSession = [NSURLSession sharedSession];

代替

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

在此处查看已接受的答案:

NSURLSessionDataTask dataTaskWithURL completion handler not getting called

希望这会有所帮助。

答案 1 :(得分:0)

后台获取未启动或在后台获取过程中出现了问题。我们需要进行一些调试,以找出问题所在。

因此,有几个观察结果:

  • 我是否正确假设您是通过应用程序委托的performFetchWithCompletionHandler方法调用此函数,并传递了完成处理程序参数?也许您可以分享此方法的实现。

  • 能够在应用程序在物理设备上运行但未附加到Xcode调试器的情况下对其进行监视(这是因为从调试器运行它会更改应用程序生命周期),这非常有用。

    为此,我建议使用Unified Logging而不是NSLog,这样即使您未连接,也可以从macOS控制台轻松监视设备os_log的语句到Xcode(与从Xcode运行不同,统一日志记录不会影响应用程序生命周期)。然后,在macOS控制台中观看,您可以确认后台提取的启动,并查看后台提取是否成功以及在何处失败。参见WWDC视频Unified Logging and Activity Tracing

    因此,导入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调试器运行应用程序,例如:

    enter image description here

    有关更多信息,请参见that video

    无论如何,有了此日志记录,您就可以确定问题是未启动后台获取还是例程中的某些问题。

  • 此外,我认为您不是要在设备上强制退出该应用程序吗?即您应该只在设备上运行该应用,然后返回主屏幕或转到另一个应用。如果您强制退出某个应用程序,则可以完全停止某些后台操作。

  • 您等待启动后台提取多长时间了?设备是否处于允许及时调用后台获取的状态?

    最重要的是,您无法控制何时进行。例如,请确保您已连接电源并处于wifi状态,以使其具有快速启动的最佳机会。 (操作系统在决定何时获取数据时会考虑这些因素。)我上次检查此模式时,在这种最佳情况下发生了大约10分钟的第一次背景获取。


顺便说一句,与手头的问题无关,您确实应该检查以确保responseNSHTTPURLResponse。如果不是这样,您是否想在尝试检索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);
    }
});

我个人也不会因为失败而更改获取间隔,尽管这取决于您。在确定下一次何时启动对应用程序的后台提取时,操作系统会考虑到这一点。