使用Apple Watch进行24/7加速度计跟踪

  • watchOS的人机界面指南
  • WatchKit编程指南
  • WWDC 2016 —设计出色的Apple Watch体验
  • WWDC 2016 — watchOS的新增功能

1.苹果框架:构建基块

1.1后台任务

  • WWDC 2016 —使您的手表应用程序保持最新
  • WWDC 2016 — watchOS上的性能架构
  • watchOS上的应用生命周期

watchOS上应用程序的默认状态not running 。 正在前台运行且屏幕打开的应用程序正在running 。 Dock中的应用程序和活动表面上具有复杂功能的应用程序处于suspended状态,已加载到内存中,可以快速恢复。 运行时未在监视屏幕上显示的应用程序(无论屏幕关闭还是其他应用程序或表面在前景中)都处于background状态,这是严格预算的, 实际上是我们的目标。

Apple提供了四个后台任务选项,watchOS可以将它们传递给扩展委托的handle(_ backgroundTasks: Set)方法:

  • WKApplicationRefreshBackgroundTask是一种常规的后台任务类型,不适用于特定的刷新类型。 这种类型最常见的用例是安排其他任务。 在我们的应用程序中,它用于触发几乎所有工作:从CoreMotion请求加速度计数据,将其写入文件,然后开始将文件传输到手机端。 为了从watchOS接收此类任务,您应隐式请求它。
  • WKSnapshotRefreshBackgroundTask是一种任务类型,专门用于您的应用程序需要更新其快照(用作启动映像,如Dock中所示)时。 WatchOS会不时将任务分配给Dock中的每个应用程序,但也可以提出要求。
  • WKWatchConnectivityRefreshBackgroundTask是一种任务类型,用于当您通过WatchConnectivity框架将数据从iPhone传输到手表时。 我们目前不使用它。
  • WKURLSessionRefreshBackgroundTask是后台联网任务完成后分配给扩展委托的任务类型。 我们目前还没有使用它。

为了请求WKApplicationRefreshBackgroundTask

2.1.3 SendSamplesOperationQueue

SendSamplesOperationQueue责任是SendSamplesOperationQueue的创建和排队。 它跟踪成功和失败的操作,为将要选择的时间块重新填充新操作,具有自己的状态以防止使用外部的精确操作种类进行重新填充(例如,在ShortTasks中,我们希望允许从传感器请求数据只能在前4秒​​钟内执行操作,但只要我们没有任何限制,就可以创建以前准备发送的文件)。

将按以下优先级选择新操作的时间块:

  1. 如果连续尝试的次数少于numberOfRetriesIfFailedToSend ,则先前使用的时间戳和先前的文件传输失败。
  2. 如果队列的阶段允许,则使用以前使用的时间戳,但不准备发送文件(这意味着该应用在准备文件时被终止)。
  3. 如果队列的阶段允许,则将新时间戳计算为最新的先前时间戳+块持续时间 ,如果该时间早于块持续时间 ,则在其他情况下,将不会创建操作并且自我填充将停止,最后执行performWhenEverythingTransferred关闭。

2.1.4数据文件

DataFile对象封装了可以从手表发送到手机的加速度计/陀螺仪数据文件选项。 它是带有三个选项的enum

  • notRequestedYet是默认情况,在从运动传感器管理器请求运动数据之前, Operations用于流程阶段。
  • noData代表请求运动数据但运动传感器未记录任何数据的情况。
  • url(URL)是具有关联的URL值的情况,表示请求,接收,处理运动数据并将其写入放置在url的文件中的情况。

为了保存为UserDefaults, DataFile在Swift 3.2的艰难时期符合我们的自制StringCodable协议。

2.3运动传感器管理器

MotionSensorManager提供CoreMotion框架的加速度计,陀螺仪和其他服务,并将其原始数据写入文件。

它具有许多公共方法:

  • requestAuthorization()在电话上触发隐私授权请求(请参阅1.4 CoreMotion )。
  • askSensorToContinueRecording()只是告诉CMSensorRecorder在接下来的12小时内继续写入加速度计数据( CoreMotion API允许的最大值
  • 应该调用getAccelerometerData(startTimestemp:endTimestamp:completionHandler:)以将加速度计数据写入文件。 这是此类中最重要的方法,它涵盖了CoreMotion和FileManager之间的所有异步剑舞。

getAccelerometerData(startTimestemp:endTimestamp:completionHandler:)背后的算法可以大致描述为:

  1. 在高优先级dispatchQueue上调用requestSensorData(from:to:completion:) ,这将使我们获得CMSensorDataList或错误进入completion关闭。
  2. 如果发生错误,则将其推至初始的completionHandler 。 如果是数据列表,请调用writeToFile(marker:sensorData:)
  3. writeToFile(marker:sensorData:)迭代writeToFile(marker:sensorData:)类型的CMRecordedAccelerometerData并将其写入文件。 迭代应该在autoreleasepool { }内部进行,以防止过多的内存使用(watchOS会杀死使用超过30mb RAM的应用程序)。 如果数据已成功写入文件,请使用URL调用completionHandler

我们将CMRecordedAccelerometerData样本序列化为字符串,然后以serialize(vector:at:)方法serialize(vector:at:)数据。 ThreeAxisVector协议可帮助我们针对陀螺仪的CMRotationRate和加速度计的CMAcceleration对象推广该方法的CMAcceleration

这可以进行优化:我们可以打印十六进制数字,而不是将十进制数字打印为字符串,十六进制数字将更短->字符串中的字符更少->传输到手机中的文件数据更少->电池使用量更少,锻炼时间更少,LongTask更快,工作更快。

尽管加速度计数据具有一定的历史意义,但由于CMMotionManager ,陀螺仪数据几乎实时地传给我们,因此对于每个记录的样本,它都会将串行gyroscopeQueue CMMotionManager关闭。 这种方式的不便之处在于CMDeviceMotion没有提供任何正常的时期时间戳,而是提供了自设备启动以来的时间(以秒为单位) 。 为了将其转换为UTC,我们在MotionSensorManager的构造函数中保存deviceBootTime时间戳。

2.4数据传输

SessionManager是WatchConnectivity的WCSessionDelegate负责与手机的所有通信。

sendFile(_:via:)方法旨在将大多数加速度计数据文件传输到手机。 由于文件仅在唤醒时才传递到iOS应用,因此此方法首先发送即时消息(从手表发送时,它会启动或唤醒对方应用),并在传递确认后通过fastsendFile(_:via:)发送文件。 这样做是为了在两个应用程序都在后台运行时传输文件。

方法fastsendFile(_:via:)也用于发送监视日志文件或陀螺仪数据文件-两者都在iOS应用程序为前台且不需要唤醒消息时发生。 根据DataFile大小写, fastsendFile(_:via:)可以:

  • 传输带有块时间戳的UserInfo词典,以使iOS应用知道手表运动传感器在该时间间隔内没有任何数据。 这是通过WatchConnectivity transferUserInfo(_:)方法完成的。
  • 通过WatchConnectivity transferFile(_:metadata:)方法传输文件。

无论传递成功与否, WCSessionDelegate都会收到以下回调之一:

  • 在字典传输的情况下, session(_ session: WCSession, didFinish userInfoTransfer: WCSessionUserInfoTransfer, error: Error?)
  • session(_ session: WCSession, didFinish fileTransfer: WCSessionFileTransfer, error: Error?)

他们都将文件/字典映射回其内容类型(加速度计块时间戳,陀螺仪或日志),并向OperationsCoordinator报告成功或失败,因此它可以管理以下操作-锻炼状态更改,队列重新填充等。请注意OperationsCoordinator将不知道文件或字典是否已实际发送,而仅知道传递是否成功。

2.5运营协调员策略

2.5.1短期任务

就电池使用而言,短期任务是有效的,但就终止应用程序而言,这是冒险的。 它的想法是运行4个并发的SendSamplesOperation ,这样,它们的CPU昂贵部分( MotionSensorManager准备带有数据的文件)将在后台任务的前4-9秒内完成,然后将打开锻炼模式以保持应用程序活动而SessionManager将等待文件传送确认。 在队列状态(通过倒计时切换)允许的情况下,充值将继续。

效率:仅在等待确认4个并发分娩时才运行耗电量大的锻炼。

风险:如果在开始锻炼之前无法完成CPU昂贵的MotionSensorManager工作,则存在超过CPU使用限制和0xc51bad0X终止的风险。 另外,如果看门狗出于某种原因决定WKApplicationRefreshBackgroundTask的4-9秒的硬编码(启发式发现)超时太大,则该应用程序将终止。

MotionSensorManager没有任何数据并且处理不花费任何敏感时间的情况下,此流程也可以正常工作,因此在canSendFor结束时不应开始锻炼秒数,因为字典已经传递到电话了。

2.5.2长任务

就电池使用效率而言,“长任务”几乎永远不会被系统终止。 它的想法是尽快开始锻炼, SendSamplesOperation运行SendSamplesOperation并等待SendSamplesOperation操作之间的sleep()来等待平均CPU使用率上限不超过15%。 充值将一直持续到最新数据传输到手机上为止。

效率:在整个任务期间(包括空闲的sleep()间隔sleep()正在运行耗电量大的锻炼。 这是很多时间:大约需要10分钟的锻炼时间,仅传输30分钟的原始数据记录。

风险:终止的唯一可能原因是CPU使用率上限超过15%。 我们已经在Instruments中运行了该应用程序,以找出正确的时间安排和块大小,因此这永远不会发生。

2.5.3入口点

  • 当系统给我们WKApplicationRefreshBackgroundTask时, WatchExtensionDelegate的方法handle(_ backgroundTasks: Set)
  • 应用由于某种原因而成为前台(请参阅WatchExtensionDelegate applicationWillEnterForeground()方法)
  • 通过发送锻炼配置从iOS触发(请参阅1.2锻炼并从phone开始 )。 如果iOS应用会注意到它在很长时间内没有收到手表的任何东西,则可能会发生这种情况

3.我们的代码库:iPhone端

3.1接受加速度计文件

WCSessionDelegate接受文件或字典(有关发送端决策的更多详细信息,请参见2.4数据传输 )并调用session(_:didReceive file:)session(_:didReceiveUserInfo:)回调。 两者都绑定到received(file:with:) ,它将检查到达的数据类型,如果是加速度计数据(如果传感器没有数据,则带有时间戳的字典)将sensorController调用sensorControllerhandlers

处理程序将:

  1. 如果文件到达,则将文件重写到文档目录,因为WatchSessionivity提供的副本将在session(_:didReceive file:)返回时被iOS删除。 如果是字典,请继续以下步骤。
  2. 时间戳记和文件URL(如果是字典, pendingFiles空白URL)将被放置到pendingFiles数组中。 该数组用作队列,存储我们从观看某些数据时收到的所有未处理块。 当处理程序应中断处理接受文件的周期时,它的自定义getter足够聪明,可以返回nil :万一应用程序用完了执行时间,块之间存在间隙或队列为空。
  3. 通过UIApplication.shared.beginBackgroundTask(withName:)启动iOS有限后台任务
  4. 调用processPending(for:) ,它将尝试从pendingFiles获取以下数据文件,如果pendingFiles ,则将其传递到iOS应用程序的深处。 它还将更新lastChunkBeforeInterrupt时间戳-已处理块的事件范围。 所有时间戳早于lastChunkBeforeInterrupt块都将以某种方式处理,并将在不久后从pendingFiles数组中删除。
  5. 重复步骤4,直到pendingFiles将返回nil

内部机制通过updateLatestDataArrived(with:)回调接受数据后,将删除数据文件。 可以在委托初始化期间完成回滚,以防止iOS应用在后台处理数据样本时发生崩溃。