使用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秒钟内执行操作,但只要我们没有任何限制,就可以创建以前准备发送的文件)。
将按以下优先级选择新操作的时间块:
- 如果连续尝试的次数少于
numberOfRetriesIfFailedToSend
,则先前使用的时间戳和先前的文件传输失败。 - 如果队列的阶段允许,则使用以前使用的时间戳,但不准备发送文件(这意味着该应用在准备文件时被终止)。
- 如果队列的阶段允许,则将新时间戳计算为最新的先前时间戳+块持续时间 ,如果该时间早于块持续时间 ,则在其他情况下,将不会创建操作并且自我填充将停止,最后执行
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:)
背后的算法可以大致描述为:
- 在高优先级
dispatchQueue
上调用requestSensorData(from:to:completion:)
,这将使我们获得CMSensorDataList
或错误进入completion
关闭。 - 如果发生错误,则将其推至初始的
completionHandler
。 如果是数据列表,请调用writeToFile(marker:sensorData:)
。 - 在
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应用知道手表运动传感器在该时间间隔内没有任何数据。 这是通过WatchConnectivitytransferUserInfo(_:)
方法完成的。 - 通过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
调用sensorController
的handlers
。
处理程序将:
- 如果文件到达,则将文件重写到文档目录,因为WatchSessionivity提供的副本将在
session(_:didReceive file:)
返回时被iOS删除。 如果是字典,请继续以下步骤。 - 时间戳记和文件URL(如果是字典,
pendingFiles
空白URL)将被放置到pendingFiles
数组中。 该数组用作队列,存储我们从观看某些数据时收到的所有未处理块。 当处理程序应中断处理接受文件的周期时,它的自定义getter足够聪明,可以返回nil
:万一应用程序用完了执行时间,块之间存在间隙或队列为空。 - 通过
UIApplication.shared.beginBackgroundTask(withName:)
启动iOS有限后台任务 - 调用
processPending(for:)
,它将尝试从pendingFiles
获取以下数据文件,如果pendingFiles
,则将其传递到iOS应用程序的深处。 它还将更新lastChunkBeforeInterrupt
时间戳-已处理块的事件范围。 所有时间戳早于lastChunkBeforeInterrupt
块都将以某种方式处理,并将在不久后从pendingFiles
数组中删除。 - 重复步骤4,直到
pendingFiles
将返回nil
。
内部机制通过updateLatestDataArrived(with:)
回调接受数据后,将删除数据文件。 可以在委托初始化期间完成回滚,以防止iOS应用在后台处理数据样本时发生崩溃。