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

Posted

技术标签:

【中文标题】如何使用数据任务实现后台提取?【英文标题】:How do I implement background fetch using a data task? 【发布时间】:2019-05-23 17:41:23 【问题描述】:

我正在尝试在 ios 应用中实现后台获取。在官方documentation中声明:

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

此外,在这个 WWDC 2014 video 51:50 之间的最佳实践中陈述了:

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

我正处于一个小型网络任务的情况:我想与我们服务器上的一个 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 -> Debug -> Simulate background fetch 或使用构建方案运行上述类型的代码时,选择了“由于后台获取事件而启动”选项,一切顺利:服务器收到我的发布请求,应用程序正确地返回了服务器响应,并且 dispatch_group_notify 块被执行。通过实际设备测试代码我没有得到任何结果,甚至没有对服务器的调用

【问题讨论】:

好的,所以你在你的应用程序委托中到处都在记录,你现在正在控制台中观看这个?你看到它叫applicationWillResignActive,但从来没有performFetchWithCompletionHandler? (如果是这样,那么sendToServer 是无关紧要的。)然后我建议两件事: 1. 完全卸载并重新安装应用程序(如果您还没有)以重置后台获取。 2. 创建新的MCVE,它执行后台获取(带有一些NSURLSession 任务)并确保您的应用程序中没有其他东西干扰进程。 谢谢,MCVE 方法是可行的方法:我使用 Postman Echo 网站提供的端点在专用应用程序中测试了我的实现,一切顺利:调用了 application:performFetch 方法,并且 dispatch_group 的使用完全没有给我带来麻烦。我还没有设法诊断出问题,但我可以确认数据任务完成了它的工作。我发现应用源中有一个重复的 Info.plist 文件,并且其中一个副本没有 UIBackgroundModes-fetch 键值对,我现在正在调查这个问题 【参考方案1】:

后台提取未启动或后台提取期间出现问题。我们需要做一些调试来确定问题所在。

所以,有几点意见:

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

能够在您的应用在物理设备上运行时对其进行监控非常有用,但不附加到 Xcode 调试器(因为从调试器运行它会改变应用生命周期)。

为此,我建议使用Unified Logging 而不是NSLog,这样即使您没有连接到Xcode,您也可以从macOS 控制台轻松监控您的设备os_log 语句(不像从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, "%publics", __FUNCTION__);
    return YES;

然后使用它转到您的控制台,包括信息和调试消息,并观察您的设备日志语句出现在您的 macOS 控制台上,即使没有通过 Xcode 调试器运行应用程序,例如:

更多信息请参见that video。

无论如何,借助此日志记录,您可以确定问题是后台获取未启动还是您的例行程序中出现问题。

另外,我假设您没有强制退出设备上的应用程序? IE。您应该只在设备上运行该应用程序,然后返回主屏幕或转到另一个应用程序。如果您强制退出应用程序,则可以完全阻止其中一些后台操作的发生。

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

最重要的是,您无法控制何时发生这种情况。例如,确保您已连接电源并连接到 wifi,以便为它提供快速启动的最佳机会。 (操作系统在决定何时提取时会考虑这些因素。)我上次检查此模式时,在这种最佳情况下,第一次后台提取大约需要 10 分钟。


顺便说一句,与您手头的问题无关,您确实应该检查以确保responseNSHTTPURLResponse。万一不是,您是否希望您的应用程序在您尝试检索 statusCode 时崩溃?因此:

os_log(log, "%publics", __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, "%publics: Error: %public@", __FUNCTION__, error);
            thereWasAnError = YES;
         else if (statusCode < 200 || statusCode > 299) 
            os_log(log, "%publics: 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);
    
);

我个人也不会在失败时更改获取间隔,但这取决于你。操作系统会在确定下一次启动应用后台提取的时间时自行考虑这一点。

【讨论】:

感谢您向我指出这个很棒的 AP​​I,我现在正在按照您的建议通过 Mac 控制台记录 iPhone(在实现它的使用时,我正在利用 nslog 被重定向以使用它的事实)。您的假设是正确的,我会将它们添加到我的问题的“我正在做的事情的描述”中。关于你问我的细节:我在我的AppDelegate上等了几个小时的“performFetchWithCompletionHandler”调用,但什么也没发生:手机正在充电,并连接到wifi。不过他们没有SIM卡。一个被锁定,另一个没有【参考方案2】:

由于这里的代码中没有修改 NSURLSession,你应该使用

NSURLSession *defaultSession = [NSURLSession sharedSession];

而不是

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

在此处查看接受的答案:

NSURLSessionDataTask dataTaskWithURL completion handler not getting called

希望这会有所帮助。

【讨论】:

谢谢,我一开始使用的是共享会话,但它不起作用。我选择坚持使用默认会话,因为在我发布的官方视频中明确表示应该可以使用

以上是关于如何使用数据任务实现后台提取?的主要内容,如果未能解决你的问题,请参考以下文章

如何实现堆栈数据结构以进行范围提取(codewars 任务)?

如何让iOS设备上App定时执行后台任务(下)

如何让iOS设备上App定时执行后台任务(下)

如何使用 Uvicorn 和 asyncio.create_task() 将任务置于后台?

在 iOS SWIFT 2 中取消获取请求 [关闭]

ios - 如何在 ios swift 中正确实现后台获取