WKWatchConnectivityRefreshBackgroundTask 与 WCSessionDelegate 竞争

WKWatchConnectivityRefreshBackgroundTask competing with WCSessionDelegate

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

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

我不知道该做什么。似乎没有办法强制 WCSession 停用,并且遵循有关延迟 setTaskCompleted 的文档似乎让我遇到 OS.[=44= 的麻烦]

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

我已经查看了关于此的 QuickSwitch 2.0 source code (after fixing the Swift 3 bugs, rdar://28503030), and their method simply doesn't seem to work (there's )。我已经尝试将 KVO 用于 WCSessionhasContentPendingactivationState,但仍然没有任何 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

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

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

  • 系统与 WatchKit 扩展通信,为什么它在后台 launched/resumed,并且
  • WatchKit 扩展让系统知道它何时完成它想做的工作的一种方式,因此可以再次 terminated/suspended。

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

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

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