肇鑫的技术博客

业精于勤,荒于嬉

WKWatchConnectivityRefreshBackgroundTask is never triggered in background, but WKSnapshotRefreshBackgroundTask

Conclusion first

Currently WKWatchConnectivityRefreshBackgroundTask is only called for sure on a watchOS Simulator when the watchOS extension's WCSession is in notActivated state and the extension is not running in foreground (in background or terminated).

In real devices, it won't be called in my tests. But Apple docs says it may. So you shouldn't rely on it won't be called until Apple changes its docs.

WCSession Cores

WCSession Core

For WCSession, when it is activated, you can transfer userInfo, and when the counterpart is active, it can get the userInfo. The counterpart won't need to be in foreground to be activated, it can be in a high priority background.

Testing Results

Here are my testing results.

In Simulators and in devices

How to make WCSession notActivated?

  1. Using Xcode terminate your watchOS Extension. Xcode will send a kill signal to your WKExtension.
  2. Or don't run WCSession.activate() in your code on watchOS Extension side. As WCSession is notActivated by default.

WatchConnectivity的一些坑

func updateApplicationContext([String : Any])的传输

这个方法的传输会对数据进行优化,连续发送相同的数据,后发的不会被传输。

正如这个函数的名称那样,这个函数用于发送程序状态的信息,如果有多个状态,那么连续发送相同的,只有第一个会被发送。

与WKWatchConnectivityRefreshBackgroundTask的关系


  1. When transferring data between iOS and watchOS, there must be an activated WCSession in the sending app. That is the first condition to check before sending data.
  2. For sendMessage(_:replyHandler:errorHandler:) and sendMessageData(_:replyHandler:errorHandler:), it is needed to check isReachable to be true as well.
  3. For a watchOS app sending data to iOS, you need to check iOSDeviceNeedsUnlockAfterRebootForReachability for if the iOS device needs to unlock after rebooting.
  4. For an iOS app sending data to watchOS, you may also check isPaired and isWatchAppInstalled before sending any data.
// iOS app
class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        if WCSession.isSupported() {
            let session = WCSession.default()
            session.delegate = self
            session.activate()
        }
        
        return true
    }
    
    extension AppDelegate:WCSessionDelegate {
    public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        guard error == nil else { fatalError(error!.localizedDescription) } 
    }
}
// view controller
class ViewController: UIViewController {
    @IBAction func sendButtonClicked(_ sender: Any) {
        if WCSession.isSupported() {
            let session = WCSession.default()
            if session.activationState == .activated {
                if session.isWatchAppInstalled && session.isPaired {
                    session.transferUserInfo(["send test":nil])
                }
            }
        }
    }
}

// watchOS app
class ExtensionDelegate: NSObject, WKExtensionDelegate {

    func applicationDidFinishLaunching() {
        // Perform any final initialization of your application.
        let session = WCSession.default()
        session.delegate = self
        session.activate()
    }

    func applicationDidBecomeActive() {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillResignActive() {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, etc.
    }
    
    var total = 0

    func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
        // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
        for task in backgroundTasks {
            // Use a switch statement to check the task type
            switch task {
            case let backgroundTask as WKApplicationRefreshBackgroundTask:
                // Be sure to complete the background task once you’re done.
                backgroundTask.setTaskCompleted()
            case let snapshotTask as WKSnapshotRefreshBackgroundTask:
                // Snapshot tasks have a unique completion call, make sure to set your expiration date
                snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
            case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
                // Be sure to complete the connectivity task once you’re done.
                total += 1
                DispatchQueue.main.async {
                    if let viewController = WKExtension.shared().rootInterfaceController as? InterfaceController {
                        viewController.activeBurnedEnergyLabel.setText(String(self.total))
                    }
                }
                
                connectivityTask.setTaskCompleted()
            case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
                // Be sure to complete the URL session task once you’re done.
                urlSessionTask.setTaskCompleted()
            default:
                // make sure to complete unhandled task types
                task.setTaskCompleted()
            }
        }
    }

}

extension ExtensionDelegate: WCSessionDelegate {
    public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        guard error == nil else { fatalError(error!.localizedDescription) }
    }
    
    /** -------------------------- Background Transfers ------------------------- */
    
    public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        total += 4
        DispatchQueue.main.async {
            if let viewController = WKExtension.shared().rootInterfaceController as? InterfaceController {
                viewController.activeBurnedEnergyLabel.setText(String(self.total))
            }
        }
    }
}

Swift程序内部对于锁的处理

我们在同步时,经常会遇到不同来源的数据同时到达的问题,这个时候,我们就需要先锁定资源,然后再对数据依次进行处理。

这里,我们通过一个开关来简单的模拟这个锁定的过程,当开关打开时,点击按钮的信息会被延后;在开关关闭时,延后的消息会执行。

s

方案一:忙等待

忙等待是最简单的处理方式。程序反复检测开关是否关闭,如果不是,则继续检测;如果是,则执行任务。

@IBAction func printHello(_ sender: Any) {
    // 忙等待
    DispatchQueue(label: "default").async {
        while true {
            if self.shouldWaitSwitch.isOn { continue }
            
            print("你好!")
            break
        }
    }
}

@IBOutlet weak var shouldWaitSwitch: UISwitch!

忙等待的优点是实现简单。缺点是忙等待会一直占用CPU。如果预期等待的时间会很长,则应避免使用忙等待。

方案二:延时处理

为避免忙等待一直占用CPU的问题,我们可以使用延迟处理的方式,即创建一个计时器,每间隔一定的时间,就检查一次状态,看可不可以执行。如果可执行,则执行,并终止计时器;如果不能执行,则等待下一次的查看。

@IBAction func printHello(_ sender: Any) {
    // 延迟
    if shouldWaitSwitch.isOn {
        Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [unowned self] (timer) in
            if self.shouldWaitSwitch.isOn { return }
            
            print("你好!")
            timer.invalidate()
        })
    }
    else {
        print("你好!")
    }
}

@IBOutlet weak var shouldWaitSwitch: UISwitch!

延迟处理的好处是大大降低了等待时的CPU的占用。缺点是,当开关变为关闭时,不会立即执行,而是可能会有一段间隔。如果时间间隔设置不当,或者开关切换得很频繁,则可能产生饥饿现象。

方案三:消息绑定

消息绑定在开关状态改变时,立即执行相应操作。

@IBAction func printHello(_ sender: Any) {
    // 消息绑定
    if shouldWaitSwitch.isOn {
        if let _ = shouldWaitSwitch.actions(forTarget: self, forControlEvent: .valueChanged) { // already set
            return
        }
        
        shouldWaitSwitch.addTarget(self, action: #selector(printHello(_:)), for: .valueChanged)
    }
    else {
        print("你好!")
    
        if let _ = shouldWaitSwitch.actions(forTarget: self, forControlEvent: .valueChanged) { // if set
            shouldWaitSwitch.removeTarget(self, action: #selector(printHello(_:)), for: .valueChanged)
        }
    }
}

@IBOutlet weak var shouldWaitSwitch: UISwitch!

消息绑定的优点是CPU占用低、响应迅速。缺点是实现起来比较复杂。特别是,一旦绑定了某个操作,这个操作在完成后,要尽量取消绑定,或者使其在开关状态发生改变时,刚好成为不满足运行条件的状态。


队列

上面解决问题的方案,都是只能执行一次打印功能。如果我们需要记录每一次的按钮点击,上面的方案就不适用了。这时我们就需要额外维护一个队列,来记录每一次的点击。这里通过扩展消息绑定的代码来实现这个队列功能。

当开关开启时,如果有任务来,我们就向队列插入一个任务。当开关关闭时,我们执行这个任务。在执行任务之前,我们对于队列进行锁定,此时不能插入新任务,由于我们确认执行时间会很短,所以插入这个新任务的等待这里采用了忙等待。当任务执行完毕后,队列锁定被打开。

@IBAction func printHello(_ sender: Any) {
    // 队列,以消息绑定为例
    if shouldWaitSwitch.isOn {
        while true {
            let isSuccess = WaitingTaskQueue.append(task: "你好!")
            if isSuccess { break } else { continue }
        }
        
        if let _ = shouldWaitSwitch.actions(forTarget: WaitingTaskQueue.self, forControlEvent: .valueChanged) { // already set
            return
        }

        shouldWaitSwitch.addTarget(WaitingTaskQueue.self, action: #selector(WaitingTaskQueue.dealingTasks), for: .valueChanged)
    }
    else {
        print("你好!")
        
        if let _ = shouldWaitSwitch.actions(forTarget: WaitingTaskQueue.self, forControlEvent: .valueChanged) { // if set
            shouldWaitSwitch.removeTarget(WaitingTaskQueue.self, action: #selector(WaitingTaskQueue.dealingTasks), for: .valueChanged)
        }
    }
}

@IBOutlet weak var shouldWaitSwitch: UISwitch!

class WaitingTaskQueue {
    private static var firstItemPosition = 0
    private static var queue:[String] = []
    private static var isLocked = false
    
    @objc static func dealingTasks() {
        guard !isLocked else {
            return
        }
        
        isLocked = true
        
        var relativePosition = 0
        while relativePosition < queue.count {
            print("\(queue[relativePosition]), \(firstItemPosition + relativePosition)")
            relativePosition += 1
        }
        queue.removeAll(keepingCapacity: true)
        firstItemPosition += relativePosition
        
        isLocked = false
    }
    
    static func append(task:String) -> Bool { // is success
        guard !isLocked else {
            return false
        }
        
        queue.append(task)
        
        return true
    }
}

结论

采用消息通知,结合忙等待是最好的方式。延迟技术,存在较大的局限性,例如容易造成饥饿或者任务的执行顺序无序等问题,应该仅在特定的条件下使用。