WKWatchConnectivityRefreshBackgroundTask 与 WCSessionDelegate 竞争

Posted

技术标签:

【中文标题】WKWatchConnectivityRefreshBackgroundTask 与 WCSessionDelegate 竞争【英文标题】:WKWatchConnectivityRefreshBackgroundTask competing with WCSessionDelegate 【发布时间】:2016-09-27 23:14:36 【问题描述】:

我正在尝试调整我的代码,从仅在前台使用 WCSessionDelegate 回调到在后台通过 handleBackgroundTasks: 接受 WKWatchConnectivityRefreshBackgroundTask。该文档指出后台任务可能会异步进入,并且在WCSessionhasContentPendingNO 之前不应调用setTaskCompleted

如果我将手表应用程序置于后台并从 iPhone 应用程序中输入transferUserInfo:,我将能够成功接收到我的第一个 WKWatchConnectivityRefreshBackgroundTask。然而,hasContentPending 始终是YES,所以我保存了任务并简单地从我的WCSessionDelegate 方法返回。如果我再次transferUserInfo:hasContentPendingNO,但没有与此消息关联的WKWatchConnectivityRefreshBackgroundTask。也就是说,后续的transferUserInfo: 不会触发对handleBackgroundTask: 的调用——它们只是由WCSessionDelegate 处理。即使我立即setTaskCompleted 而不检查hasContentPending,后续的transferUserInfo: 也由session:didReceiveUserInfo: 处理,而我无需再次激活WCSession

我不确定在这里做什么。似乎没有办法强制 WCSession 停用,并且遵循有关延迟 setTaskCompleted 的文档似乎让我遇到了操作系统问题。

我在GitHub 上发布并记录了一个示例项目,说明了我的工作流程,并在下面粘贴了我的WKExtensionDelegate 代码。我是否在某个地方做出了错误的选择或错误地解释了文档?

我查看了QuickSwitch 2.0 源代码(在修复了 Swift 3 错误之后,rdar://28503030),他们的方法似乎根本不起作用(another SO thread 关于这个)。我已经尝试对WCSessionhasContentPendingactivationState 使用KVO,但仍然没有任何WKWatchConnectivityRefreshBackgroundTask 可以完成,这对于我目前对该问题的解释是有意义的。

#import "ExtensionDelegate.h"

@interface ExtensionDelegate()

@property (nonatomic, strong) WCSession *session;
@property (nonatomic, strong) NSMutableArray<WKWatchConnectivityRefreshBackgroundTask *> *watchConnectivityTasks;

@end

@implementation ExtensionDelegate

#pragma mark - Actions

- (void)handleBackgroundTasks:(NSSet<WKRefreshBackgroundTask *> *)backgroundTasks

    NSLog(@"Watch app woke up for background task");

    for (WKRefreshBackgroundTask *task in backgroundTasks) 
        if ([task isKindOfClass:[WKWatchConnectivityRefreshBackgroundTask class]]) 
            [self handleBackgroundWatchConnectivityTask:(WKWatchConnectivityRefreshBackgroundTask *)task];
         else 
            NSLog(@"Handling an unsupported type of background task");
            [task setTaskCompleted];
        
    


- (void)handleBackgroundWatchConnectivityTask:(WKWatchConnectivityRefreshBackgroundTask *)task

    NSLog(@"Handling WatchConnectivity background task");

    if (self.watchConnectivityTasks == nil)
        self.watchConnectivityTasks = [NSMutableArray new];
    [self.watchConnectivityTasks addObject:task];

    if (self.session.activationState != WCSessionActivationStateActivated)
        [self.session activateSession];


#pragma mark - Properties

- (WCSession *)session

    NSAssert([WCSession isSupported], @"WatchConnectivity is not supported");

    if (_session != nil)
        return (_session);

    _session = [WCSession defaultSession];
    _session.delegate = self;

    return (_session);


#pragma mark - WCSessionDelegate

- (void)session:(WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(NSError *)error

    switch(activationState) 
        case WCSessionActivationStateActivated:
            NSLog(@"WatchConnectivity session activation changed to \"activated\"");
            break;
        case WCSessionActivationStateInactive:
            NSLog(@"WatchConnectivity session activation changed to \"inactive\"");
            break;
        case WCSessionActivationStateNotActivated:
            NSLog(@"WatchConnectivity session activation changed to \"NOT activated\"");
            break;
    


- (void)sessionWatchStateDidChange:(WCSession *)session

    switch(session.activationState) 
        case WCSessionActivationStateActivated:
            NSLog(@"WatchConnectivity session activation changed to \"activated\"");
            break;
        case WCSessionActivationStateInactive:
            NSLog(@"WatchConnectivity session activation changed to \"inactive\"");
            break;
        case WCSessionActivationStateNotActivated:
            NSLog(@"WatchConnectivity session activation changed to \"NOT activated\"");
            break;
    


- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary<NSString *, id> *)userInfo

    /*
     * NOTE:
     * Even if this method only sets the task to be completed, the default
     * WatchConnectivity session delegate still picks up the message
     * without another call to handleBackgroundTasks:
     */

    NSLog(@"Received message with counter value = %@", userInfo[@"counter"]);

    if (session.hasContentPending) 
        NSLog(@"Task not completed. More content pending...");
     else 
        NSLog(@"No pending content. Marking all tasks (%ld tasks) as complete.", (unsigned long)self.watchConnectivityTasks.count);
        for (WKWatchConnectivityRefreshBackgroundTask *task in self.watchConnectivityTasks)
            [task setTaskCompleted];
        [self.watchConnectivityTasks removeAllObjects];
    


@end

【问题讨论】:

【参考方案1】:

根据您的描述和我的理解,这听起来像是正常工作。

向我解释的方式是,watchOS 上的新 handleBackgroundTasks: 旨在成为一种方式:

系统与 WatchKit 扩展通信,为什么它在后台启动/恢复,以及 WatchKit 扩展程序让系统知道它何时完成了它想要做的工作,因此可以再次终止/暂停。

这意味着,每当 Watch 接收到传入的 WatchConnectivity 负载并且您的 WatchKit 扩展程序被终止或暂停时,您应该期待一个 handleBackgroundTasks: 回调,让您知道您为什么在后台运行。这意味着您可能会收到 1 个WKWatchConnectivityRefreshBackgroundTask,但会收到几个 WatchConnectivity 回调(文件、userInfos、applicationContext)。 hasContentPending 让您知道您的 WCSession 何时交付了所有初始的、待处理的内容(文件、用户信息、应用程序上下文)。此时,您应该在 WKWatchConnectivityRefreshBackgroundTask 对象上调用 setTaskCompleted。

然后您可以预期您的 WatchKit 扩展将很快暂停或终止,除非您收到其他 handleBackgroundTasks: 回调并因此有其他 WK 后台任务对象要完成。

我发现,当使用调试器附加到进程时,操作系统可能不会像往常那样暂停它们,因此如果您想确保避免任何此类情况,建议使用日志记录检查此处的行为问题。

【讨论】:

很有趣,谢谢。诚然,我只在调试器附带的模拟器中进行了测试,所以我可以相信这一点。如果这是真的,那么 KVO 听起来是正确的举动(保存任何 BackgroundTasks,并在 hasContentPending 观察者被触发到 NO 时清除它们)。在我返回标记为已回答之前,让我再做一些测试。 是的,我发现模拟器对预期行为的准确度甚至低于连接调试器的设备。我在模拟器中发现,一旦进程运行,它就永远不会再次挂起,因此与您所描述的相符。

以上是关于WKWatchConnectivityRefreshBackgroundTask 与 WCSessionDelegate 竞争的主要内容,如果未能解决你的问题,请参考以下文章