肇鑫的技术博客

业精于勤,荒于嬉

iOS每次升级后,苹果表表盘通知额度归零问题的处理

我是苹果表的老用户了。有多老?初代苹果表可以预购的第一天,我就预定了一只42mm的不锈钢苹果表。除了2018年苹果表电池把屏幕拱开,维修返厂的那几天,我天天都戴着它。

我写的应用,有一个叫Stand Minus的,提交到了苹果商店后,被审核的工作人员认为功能太少给拒绝上架了。我觉得它现有功能“增一分则肥,减一分则瘦”,拒绝修改。所以这个应用我就自己这一个用户。今天我要说的就是和这个Stand Minus相关的内容。

感谢git的存在,我们可以知道,Stand Minus是我在2017年1月31日最初创建的。

stand_minus_initiation

Stand Minus有两种工作方式,根据当前表盘是否有Stand Minus的小部件来判断。

发现问题

根据苹果的文档,当iOSwatchOS发送消息的时候,可以使用WCSession,其中最好用的方法是transferCurrentComplicationUserInfo(_:)。这个方法,同时还能唤醒手表端扩展的后台。因为能唤醒后台,会导致额外的耗电,所以苹果规定每天唤醒次数最多只能有50次,超过就自动变成不能唤醒后台的transferCurrentComplicationUserInfo(_:)调用。

一天有24小时,50次限制约等于每小时2次。我们可以使用remainingComplicationUserInfoTransfers属性来获得剩余的次数。

Stand Minus设计就是每小时发送两次,一天50次肯定是用不光的。但在实际使用中,有时候表盘小部件并没能及时更新。通过手表端查看我发现,提示剩余次数为0。

zero_count_remain

这个问题不常见,但是有时也会出。经过多次观察,我最终发现,这个问题和iOS升级有关,每次iOS升级完成之后,剩余次数都会归零。解决的办法很简单,额外再重启一次手机,然后就好了。

尝试解决问题

时光荏苒,到了2019年,因为升级到iOS 13 beta之后,我一直使用的推送OneSignal失效了。不得不重新折腾起一直用得好好的Stand Minus。同时,我尝试测试究竟是什么原因导致了次数归零。

我将代码

remainCountsLabel.text = String(session.remainingComplicationUserInfoTransfers)

改成了

remainCountsLabel.text = {
    guard session.activationState == .activated else {
        return "Session状态不是.activated。"
    }
    
    guard session.isWatchAppInstalled else {
        return String("对应手表应用正在安装中……")
    }
    
    guard session.isComplicationEnabled else {
        return "错误:手表当前表盘未安装Stand-的小部件。"
    }
    
    return String(session.remainingComplicationUserInfoTransfers)
}()

然后等待iOS的更新再测试。我最终发现了,导致错误的原因是,虽然手表的表盘上有Stand Minus的小部件,但是session.isComplicationEnabled返回的是否。而对于当前表盘没有对应小部件的情况下,session.remainingComplicationUserInfoTransfers返回0。

no_complication_erro

找到了更确切的原因,就可以解决问题了。我找了不用重启iPhone的办法。先切换手表的表盘到另外的表盘,然后再切回来,然后再点击Stand Minus的表盘小部件,打开手表应用。

经过上面的步骤,再重新打开手机上的应用查看,就会显示正确的剩余次数了。

最终解决方案

每次iOS升级之后,都做一遍切表盘,切表盘,开应用的操作。不算麻烦,但是也挺讨厌的。有没有有什么更好的办法呢?

我用的苹果表初代,在2018年的watchOS 5就不能更新了,我曾经猜测苹果应该早就修复了这个问题。我今年卖了苹果表5,手表到手之后,我发现苹果表5还是有这个问题。苹果怎么回事?这都watchOS 6了啊。

我尝试绕过这个问题。观察代码,手机向手表发送消息的核心代码如下:

if session.activationState == .activated && session.isPaired && session.isComplicationEnabled {
    session.transferCurrentComplicationUserInfo(userInfo)
}

即当一切正常,且当前表盘存在对应小部件的时候,发送能够唤醒后台的消息。已知出错的是session.isComplicationEnabled,那么可否移除它,直接发送唤醒后台的消息呢?

阅读文档,苹果是这么说的:

Call this method when you have new data to send to your complication. Your WatchKit extension can use the data to replace or extend its current timeline entries.

This method can only be called while the session is active—that is, the activationState property is set to WCSessionActivationState.activated. Calling this method for an inactive or deactivated session is a programmer error.

原来,调用transferCurrentComplicationUserInfo(_:)方法,只要求WCSession为活跃就可以,其余的都是可以忽略的。

于是我删掉了session.isComplicationEnabled的检测。这相当于主动忽略了系统出错的部分。

no_complication_but_still_send

不过,因为不再检测当前表盘是否存在小部件,相对于Stand Minus的原本架构,在不使用表盘小部件的情况下,手表的额外耗电理论上会多一些。

其它

我这边能做的就是这些。API的错误,最终还是需要苹果来解决。

参考资料

iOS、watchOS应用用户设置相关问题的答案

最近在写咕唧2,对于iOS应用与配套watchOS应用之间的设置的相关问题有了一些心得。记下来,备查。

问题与答案

1. 创建的Settings.bundle文件放在哪里?

无论iOS还是watchOS的,都放在iOS应用下。

2. iOS应用的设置,为什么watchOS应用不能直接读了?

这是一个历史遗留问题。1代的手表系统,当时还不叫watchOS,上的应用只有一种,叫瞥一眼(glance)。生成机制是所有的一切都在手机上生成,然后传到手表上显示。因此,当然手机应用和手表应用实际上都在手机上运行。因此,直接通过分享组(share group),手表应用就能读取手机应用的设置。

watchOS 2开始,手表应用改在手表上运行了。而手表上的共享组和手机上的共享组之间不自动同步,所以手表和手机之间的设置不能直接读了。

3. iOS应用如何获得watchOS应用的设置?

不能直接读。因为苹果规定,手表应用可以唤醒手机应用,反之则不行。

4. watchOS应用如何获得iOS应用的设置?

通过session的sendMessage方法唤醒手机,然后手机发送设置给手表。

5. 如何设置watchOS应用的设置?

通过手机端的手表应用。这个应用虽然在手机上,但是可以直接读手表应用的设置。并且会写回去。

一张图

iOS与watchOS的设置同步

参考资料

为什么说只有远程通知才是最吼的!(续)

这篇文章会根据我的体验和理解进行增补和删减

如果是新增,那么内容会直接添加。
如果是修改或删减。我会将原有的段落标记加上删除线。然后再写上新段落。
如果出现多次修改和删减。我会添加版本的提示。如果没有提示,那么相邻的删除线的段落,就是同一个版本的。

简析远程推送服务的解决方案

苹果不承诺远程通知的可靠性。所以远程通知在实际使用中,经常有延迟,甚至丢失的情况。这时,你有两个选择。PushKit或调教苹果的apns服务器。

苹果不承诺远程通知的可靠性。并且由于我们在使用远程通知时,往往会使用第三方的推送服务。因此,一旦通知出现延迟甚至丢失,那么既有可能是苹果的问题,也可能是第三方服务的问题。就目前我自己的感受来说,苹果的服务要比第三方服务更可靠一些。

如果你的程序对于实时性要求较高。那么可以使用收费的第三方服务,这样可以保证你的推送的质量。或者使用自己的推送服务器。更进一步,你还可以使用PushKit

PushKit

PushKit是苹果伴随iOS 8推出的特殊通知,它针对的是一般远程通知的缺点。最初的设计目标是提供给VoIP的app以实时的通知。iOS 9的时候,苹果又增加了对于手表表盘上小部件的推送。

一般远程通知的缺陷

  1. 苹果不保证远程通知一定能到达。苹果会根据它的apns服务器的情况,动态来处理你的推送请求。也就是说,你的请求可能被执行,也可能会延迟,甚至被忽略(忘记)。
  2. 苹果的apns服务器最多只保留最后一条的推送请求。比如用户设备离线的情况下,你发出了多条通知,那么最终用户至多只能收到最后一条。
  3. 如果用户通过呼出任务管理,并且上滑移除app。那么该app的远程推送,将不能唤醒该app到后台,直到用户手动打开该app,或重启手机并解锁进入。
  4. 远程通知注册的token,隔一段时间会自动变化。这时如果还按照旧的申请,就会收不到消息。

苹果的处理方式

  1. 针对问题1。苹果保证PushKit通知会即时到达。看其他人写的评测,基本上延迟只有1秒。因为VoIP是类似打电话的功能,这个延迟高了,服务就没法用了。
  2. 用户设备可以在收到远程通知后,向自己的服务器,苹果称之为Provider,进行反馈。由于通知是即时的,服务器在“向要求苹果apns的服务器发出通知”的命令发出后,如果在特定时间没有收到用户设备的反馈,就可以认定用户的设备是离线的。这之后如果还需要给用户设备发通知,就可以先缓存起来,而不要发送到苹果的apns服务器。服务器可以在确认用户设备启动之后,再一次性将缓存的通知发出,这样,用户就可以得到在设备离线期间缓存的所有信息。
  3. PushKit的通知仍可以唤醒程序。
  4. PushKit的token是不变的。

PushKit的缺点

  • 实现比远程通知更为复杂。除了远程通知的要实现之外,还要向Provider做出反馈。
  • 国内没有现成的第三方服务,需要使用国外的第三方服务。或者自己编写Provider

调教苹果的apns服务器

如果你不想实现PushKit,那么你可以采用下面的比较脏的方式来调教苹果的apns服务器。

我的程序每小时通知一次设备。但是我发现,这个通知有时候会丢失,每天大概会丢个两回左右。

更进一步研究,我发现,如果通知没有及时到达,那么我可以在一段时间之后,补发一个通知。由于我没有实现PushKitProvider服务器不知道上一个通知到达了没有,因此我必须每小时发送两次通知。iOS端如果收到了第一个通知,那么可以做下标记,第二个通知到来时,就直接忽略,不要向Apple Watch传送就可以了。

这个调教的方式的原理是这样的。当apns服务器繁忙时,我猜测苹果采用了一个典型的美国式公平的策略。即大家都丢一些,而不是规定一个资源上限,占用资源多的丢,占用少的不丢。这导致一个现象,就是爱哭的孩子有糖吃。比如我的程序,原本每小时只需要一次推送。但是由于苹果的apns服务器会偶发的抛弃其中的1-2个通知。那么我为了保证通知的必须到达,就需要每小时发两次。实际上这导致苹果apns服务器的负载增加了一倍。但是我没有办法,因为只有这样,才能保证我的程序每小时至少刷新一次。

这个方法相对于PushKit,优点是实现简单。缺点也很明显,不考虑苹果apns的服务器负载增加的损失。只考虑开发者自己这边,最主要的缺点是通知到来的时间不稳定。既可能在第一次的时间到来,也可能是第二次。如果是对于通知到来时间,有特定要求的程序,那就不能用这种方式,而必须使用PushKit

系列文章

为什么说只有远程通知才是最吼的!
为什么说只有远程通知才是最吼的!(续)
为什么说只有远程通知才是最吼的!(续+1)