如何使用数据任务实现后台获取?

How do I implement background fetch using a data task?

我正在尝试在 iOS 应用程序中实现后台获取。在官方documentation中是这样写的:

"You don’t have to do all background network activity with background sessions ... ... . Apps that declare appropriate background modes can use default URL sessions and data tasks, just as if they were in the foreground."

此外,在此 WWDC 2014 video 中,在 51:50 中陈述了最佳实践:

"One common mistake that we think some people have made in the past is assuming that when running in the background to handle things like a background fetch update... ...they have been required to use background session. Now, using background upload or download... ...works really well, but in particular for large downloads. When you're running for a background fetch update ... you have about 30 to 60 seconds to run in the background" [应用程序被暂停之前] "If you have same small networking task that could finish within this time it is perfectly ok to do this in an in-process or default NSURLSession ... ..."

我正处于一个小型网络任务的情况:我想与我们服务器上的休息 api 通信,以 post 相同的数据并取回数据响应。我正在创建一个默认会话并执行一个或两个任务,使用调度组同步它们,如本例所示:

- (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);
        }
    });
}

在我的 application:didFinishLaunchingWithOptions: 方法中,我使用

将 BackgroundFetchInterval 设置为 MinimumInterval
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

我不会强制退出设备上的应用程序,因为我知道在那种情况下系统不会调用 AppDelegate application:performFetchWithCompletionHandler: 方法,直到用户重新启动应用程序

我正在使用多个测试用例和几种设备组合 locked/unlocked,charging/not 充电,所有这些都在 wifi 网络下,因为它们没有 sims

当我 运行 使用 Xcode -> 调试 -> 模拟后台获取或使用选择了 "launch due to a background fetch event" 选项的构建方案时,上述类型的代码一切顺利:服务器收到我的 post 请求,应用程序正确地返回了服务器响应,并且执行了 dispatch_group_notify 块。通过实际设备测试代码我没有得到任何结果,甚至没有调用服务器

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

NSURLSession *defaultSession = [NSURLSession sharedSession];

而不是

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

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

NSURLSessionDataTask dataTaskWithURL completion handler not getting called

希望对您有所帮助。

后台获取未启动或后台获取期间出现问题。我们需要进行一些调试以确定问题所在。

所以,有几点观察:

  • 我假设您是从应用程序委托的 performFetchWithCompletionHandler 方法调用它并传递完成处理程序参数,我是否正确?或许你可以分享一下你对这个方法的实现。

  • 能够在 运行 物理设备上运行但未连接到 Xcode 调试器时监控您的应用程序非常有用(因为它是运行 从调试器更改应用生命周期)。

    为此,我建议使用 Unified Logging instead of NSLog, that way you can easily monitor your device os_log statements from the macOS Console even though you’re not connected to Xcode (unlike running from Xcode, the unified logging does not affect the app lifecycle). Then, watching in macOS Console, you can confirm the initiation of the background fetch, and see if and where it failed. See WWDC video 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 调试器,例如:

    有关详细信息,请参阅 that video

    无论如何,借助此日志记录,您可以确定问题是否出在后台提取未启动或您的例程中。

  • 此外,我假设您不是 force-quitting 设备上的应用程序? IE。您应该 运行 设备上的应用程序,然后 return 到主屏幕或转到另一个应用程序。如果你 force-quit 一个应用程序,它可以完全阻止其中一些后台操作的发生。

  • 您等待后台提取启动多久了?设备是否处于可以及时调用后台fetch的状态?

    最重要的是,您无法控制何时发生这种情况。例如,确保您已连接到电源并连接到 wifi,以便为其快速开火提供最佳机会。 (OS 在决定​​何时获取时会考虑这些因素。)上次我检查此模式时,在这种最佳情况下第一次后台获取大约需要 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);
    }
});

我个人也不会费心更改失败时的提取间隔,尽管这取决于您。 OS 会在确定下一次启动后台获取应用程序时考虑这一点。