96、Core Motion与持久存储技术解析

Core Motion与持久存储技术解析

1. Core Motion使用要点

1.1 Core Motion使用注意事项

在使用Core Motion时,有以下几点需要注意:
- 应用应仅创建一个 CMMotionManager 实例。
- Core Motion需要开启各种传感器,如磁力计和陀螺仪,这会增加电池消耗。因此,尽量避免使用不必要的传感器,并在不需要更新时及时停止。
- 应用在后台运行时可以合法使用Core Motion,但需要有其他后台运行的理由,因为 Info.plist 中没有Core Motion的 UIBackgroundModes 设置。例如,若使用了Core Location,可借此同时使用Core Motion。
- 若应用不在后台运行,当应用进入后台时,应明确告知运动管理器停止生成更新。

1.2 Core Motion其他数据类型

除了 CMDeviceMotion ,Core Motion框架还能获取以下四种其他类型的数据:
| 数据类型 | 功能 | 使用步骤 |
| ---- | ---- | ---- |
| CMMotionActivityManager | 部分设备有运动协处理器芯片,能检测、分析和记录设备运动,即使设备休眠且耗电量极低。可分析用户携带或佩戴设备时的活动情况,但不涉及位置确定。 | 1. 维护一个 CMMotionActivityManager 实例,通常作为实例属性。
2. 调用 isActivityAvailable 方法检查设备是否有运动协处理器。
3. 有两种查询方式:
- 实时更新:调用 startActivityUpdates(to:withHandler:) ,回调函数会定期调用,不需要更新时调用 stopActivityUpdates
- 历史数据:调用 queryActivityStarting(from:to:to:withHandler:) 查询记录的数据,可在运动活动管理器已提供更新时查询历史数据。 |
| CMPedometer | 计步器,通过设备的前后运动推断步数,还可接收用户活动开始或停止的事件。在某些Core Location不可靠的情况下仍能可靠工作。 | 1. 维护一个 CMPedometer 实例,通常作为实例属性。
2. 使用前调用 isStepCountingAvailable 方法检查是否支持计步。
3. 不同设备有不同功能,如推断步幅大小、计算距离、估算是否爬楼梯、获取瞬时节奏和步速等。
4. 数据查询方式与运动活动数据相同:
- 实时更新:调用 startUpdates(from:withHandler:)
- 历史数据:调用 queryPedometerData(from:to:withHandler:) ,数据以 CMPedometerData 对象形式返回。
5. 调用 startEventUpdates(handler:) 可接收用户运动变化的通知,处理函数接收 CMPedometerEvent 对象,类型为 .pause .resume 。 |
| CMAltimeter | 部分设备有高度计(本质是气压计),主要用于提醒用户活动期间相对高度的变化,而非绝对高度。 | 1. 维护一个 CMAltimeter 实例,通常作为实例属性。
2. 使用前调用 isRelativeAltitudeAvailable 方法检查是否支持相对高度测量。
3. 调用 startRelativeAltitudeUpdates(to:withHandler:) 开始接收 CMAltitudeData 对象,关键指标是 relativeAltitude 属性,以米为单位,初始值为0,后续对象提供相对于初始值的测量值。 |
| CMSensorRecorder | 部分设备可在后台记录加速度计的输出。 | 1. 使用前调用 isAccelerometerRecordingAvailable 方法检查是否支持加速度计记录。
2. 实例化 CMSensorRecorder 并调用 recordAccelerometer(forDuration:) 开始记录,系统每秒记录50次,无论应用是否在前台或运行,达到指定时长后自动停止。
3. 检索数据时,再次实例化 CMSensorRecorder 并调用 accelerometerData(from:to:) ,返回 CMSensorDataList 对象。
4. 为 CMSensorDataList 添加扩展使其符合 Sequence 协议:
swift<br>extension CMSensorDataList: Sequence {<br> public typealias Iterator = NSFastEnumerationIterator<br> public func makeIterator() -> NSFastEnumerationIterator {<br> return NSFastEnumerationIterator(self)<br> }<br>}<br>
5. 遍历 CMSensorDataList 获取 CMRecordedAccelerometerData 实例:
swift<br>let rec = CMSensorRecorder() // and d1 and d2 are Dates<br>if let list = rec.accelerometerData(from: d1, to: d2) {<br> for datum in list {<br> if let accdatum = datum as? CMRecordedAccelerometerData {<br> let accel = accdatum.acceleration<br> let t = accdatum.timestamp<br> // do something with data here<br> }<br> }<br>}<br> |

1.3 用户授权

获取以上四种数据都需要用户授权,且用户后续可在设置应用中撤销授权。 Info.plist 必须包含“Privacy - Motion Usage Description”键( NSMotionUsageDescription )的条目,解释使用目的。奇怪的是,没有 requestAuthorization 方法。以前,很难提前知道是否获得授权,需尝试查询数据并查看是否出错。从iOS 11开始,可直接询问类的 authorizationStatus ,若状态为 .notDetermined ,仍需“触发”类以弹出授权对话框。

以下是检查授权的示例代码:

func checkAuthorization(andThen f:(()->())? = nil) {
    let status = CMMotionActivityManager.authorizationStatus()
    switch status {
    case .notDetermined: // bring up dialog
        let now = Date()
        self.actman.queryActivityStarting(from: now, to:now, to:.main) {
            _,err in
            print("asked for authorization")
            if err == nil {
                f?()
            }
        }
    case .authorized: f?()
    case .restricted: break // do nothing
    case .denied: break // could beg for authorization here
    }
}

1.4 历史运动活动数据查询示例

以下是查询过去24小时历史运动活动管理器数据的示例:

let now = Date()
let yester = now - (60*60*24)
self.actman.queryActivityStarting(
    from: yester, to: now, to: self.queue) { arr, err in
        guard var acts = arr else {return}
        // ...
}

1.5 数据处理

CMMotionActivity 对象有 startDate confidence .low .medium .high )和一系列布尔属性,用于分类活动。为了简化数据,可对 CMMotionActivity 进行扩展,添加一个实用方法将布尔属性总结为字符串:

extension CMMotionActivity {
    private func tf(_ b:Bool) -> String {
        return b ? "t" : "f"
    }
    func overallAct() -> String {
        let s = tf(self.stationary)
        let w = tf(self.walking)
        let r = tf(self.running)
        let a = tf(self.automotive)
        let c = tf(self.cycling)
        let u = tf(self.unknown)
        return "\(s) \(w) \(r) \(a) \(c) \(u)"
    }
}

然后,可去除无明确活动、置信度低或活动与前一个相同的 CMMotionActivity 对象:

let blank = "f f f f f f"
acts = acts.filter {act in act.overallAct() != blank}
acts = acts.filter {act in act.confidence == .high}
for i in (1..<acts.count).reversed() {
    if acts[i].overallAct() == acts[i-1].overallAct() {
        acts.remove(at:i)
    }
}
DispatchQueue.main.async {
    self.data = acts
}

2. 持久存储

2.1 沙盒

设备的整体内容对应用不可见,应用有自己的沙盒,这是设备持久存储的有限区域。每个应用只能看到自己的沙盒,可防止窥探或影响其他应用的文件,同时自身文件也受到保护。若用户删除应用,沙盒及其中的数据将被删除;否则,数据应可靠持久。

2.2 标准目录

推荐使用文件URL( URL 实例)来引用文件或目录,也可使用文件路径(字符串),必要时可在两者间转换。沙盒包含一些标准目录,可通过 FileManager 实例的 url(for:in:appropriateFor:create:) 方法获取标准目录的URL。

以下是获取文档目录URL的示例:

do {
    let fm = FileManager.default
    let docsurl = try fm.url(for:.documentDirectory,
        in: .userDomainMask, appropriateFor: nil, create: false)
    // use docsurl here
} catch {
    // deal with error here
}

对于要保存和读取的文件和文件夹,文档目录是个不错的选择,但如果应用支持文件共享,用户可通过iTunes查看和修改文档目录,因此可能不适合存放用户不应查看和更改的内容。应用支持目录是个很好的替代方案,iOS中每个应用在沙盒中有自己的私有应用支持目录,可直接安全地存放文件。该目录可能最初不存在,但可在获取时创建:

do {
    let fm = FileManager.default
    let suppurl = try fm.url(for:.applicationSupportDirectory,
        in: .userDomainMask, appropriateFor: nil, create: true)
    // use suppurl here
} catch {
    // deal with error here
}

可将愿意接受丢失的临时文件写入缓存目录( .cachesDirectory )或临时目录( FileManager temporaryDirectory )。若将临时文件写入应用支持文件夹,默认可通过iTunes或iCloud备份,可通过设置文件属性排除备份:

var myFileURL = // file URL
var rv = URLResourceValues()
rv.isExcludedFromBackup = true
try myFileURL.setResourceValues(rv)

2.3 检查沙盒

开发应用时,可查看沙盒以确保文件按预期保存。在模拟器中,可通过打印应用文档目录的路径,复制到Finder的“前往文件夹”对话框中查看。在设备上,连接设备后,选择“Window → Devices and Simulators”,切换到“Devices”标签,选择设备和应用,点击齿轮图标,可选择“Show Container”查看沙盒层次结构,或选择“Download Container”将沙盒复制到计算机,以 .xcappdata 包形式存在,可在Finder中打开。

2.4 基本文件操作

2.4.1 创建文件夹

以下是在文档目录中创建 MyFolder 文件夹的示例:

let foldername = "MyFolder"
let fm = FileManager.default
let docsurl = try fm.url(for:.documentDirectory,
    in: .userDomainMask, appropriateFor: nil, create: false)
let myfolder = docsurl.appendingPathComponent(foldername)
try fm.createDirectory(at:myfolder, withIntermediateDirectories: true)
2.4.2 查看目录内容

可通过 contentsOfDirectory 方法获取目录的内容数组,该数组列出目录的直接内容的完整URL,是浅层的。若要深度遍历目录内容,可使用目录枚举器( FileManager.DirectoryEnumerator ),这在内存使用上更高效,因为每次只返回一个文件引用。

以下是深度遍历文档目录查找 .txt 文件的示例:

let fm = FileManager.default
let docsurl = try fm.url(for:.documentDirectory,
    in: .userDomainMask, appropriateFor: nil, create: false)
let dir = fm.enumerator(at:docsurl, includingPropertiesForKeys: nil)!
for case let f as URL in dir where f.pathExtension == "txt" {
    print(f.lastPathComponent) // file1.txt, file2.txt
}

目录枚举器还允许跳过特定子目录,使遍历更高效。可查阅 FileManager 类文档和Apple的文件系统编程指南了解更多文件操作。

2.5 保存和读取文件

2.5.1 避免存储绝对文件URL

沙盒目录的绝对URL在应用的单次运行中会持续存在,但长期来看是不稳定的,不同运行时可能不同。因此,不能将应用的绝对文件URL或路径字符串存储到任何持久存储中,否则下次应用启动时会出错。若必须存储文件和文件夹信息,应存储其名称(或相对URL、部分路径)以及所在沙盒目录的信息,每次需要时重新生成URL。

2.5.2 可直接转换的类

NSString NSData NSArray NSDictionary 这四个Cocoa类提供了从文件URL创建实例和将实例保存到文件URL的方法,可在文件和类实例间有效转换。

以下是生成文本文件和属性列表文件的示例:

// 生成文本文件
try "howdy".write(to: myfolder.appendingPathComponent("file1.txt"),
    atomically: true, encoding:.utf8)

// 生成属性列表文件
let arr = ["Manny", "Moe", "Jack"]
let temp = FileManager.default.temporaryDirectory
let f = temp.appendingPathComponent("pep.plist")
try (arr as NSArray).write(to: f)
2.5.3 序列化对象到文件

若要保存其他类型的对象到文件,可将其序列化为 NSData 对象(Swift的 Data )。序列化有两种方法:旧的Cocoa方式( NSCoding )和新的Swift方式( Codable )。

NSCoding

NSCoding 协议在Cocoa的Foundation框架中定义。若对象的类采用 NSCoding ,可通过 NSKeyedArchiver NSKeyedUnarchiver 将对象转换为 NSData 并转换回来。这意味着类要实现 encode(with:) 方法来归档对象,实现 init(coder:) 方法来解档对象。

以下是一个简单的 Person 类采用 NSCoding 协议的示例:

class Person: NSObject, NSSecureCoding {
    static var supportsSecureCoding: Bool { return true }
    var firstName : String
    var lastName : String
    override var description : String {
        return self.firstName + " " + self.lastName
    }
    init(firstName:String, lastName:String) {
        self.firstName = firstName
        self.lastName = lastName
        super.init()
    }
    func encode(with coder: NSCoder) {
        // do not call super in this case
        coder.encode(self.lastName, forKey: "last")
        coder.encode(self.firstName, forKey: "first")
    }
    required init(coder: NSCoder) {
        self.lastName = coder.decodeObject(
            of: NSString.self, forKey:"last")! as String
        self.firstName = coder.decodeObject(
            of: NSString.self, forKey:"first")! as String
        // do not call super init(coder:) in this case
        super.init()
    }
}

// 保存Person实例
let fm = FileManager.default
let docsurl = try fm.url(for:.documentDirectory,
    in: .userDomainMask, appropriateFor: nil, create: false)
let moi = Person(firstName: "Matt", lastName: "Neuburg")
let moidata = try NSKeyedArchiver.archivedData(
    withRootObject: moi, requiringSecureCoding: true)
let moifile = docsurl.appendingPathComponent("moi.txt")
try moidata.write(to: moifile, options: .atomic)

// 检索保存的Person实例
let persondata = try Data(contentsOf: moifile)
let person = try NSKeyedUnarchiver.unarchivedObject(
    ofClass: Person.self, from: persondata)!
print(person) // "Matt Neuburg"
Codable

Codable 协议在Swift 4中引入,是 Encodable Decodable 协议的组合。对象只要符合 Encodable 就可序列化(归档),符合 Decodable 就可从序列化形式恢复(解档),通常对象会同时符合两者,即采用 Codable 协议。有三种序列化模式:属性列表、JSON和 NSCoder

以下是将 Person 类重写为采用 Codable 协议的示例:

class Person: NSObject, Codable {
    var firstName : String
    var lastName : String
    override var description : String {
        return self.firstName + " " + self.lastName
    }
    init(firstName:String, lastName:String) {
        self.firstName = firstName
        self.lastName = lastName
        super.init()
    }
}

// 保存Person实例
let fm = FileManager.default
let docsurl = try fm.url(for:.documentDirectory,
    in: .userDomainMask, appropriateFor: nil, create: false)
let moi = Person(firstName: "Matt", lastName: "Neuburg")
let moidata = try PropertyListEncoder().encode(moi)
let moifile = docsurl.appendingPathComponent("moi.txt")
try moidata.write(to: moifile, options: .atomic)

// 检索保存的Person实例
let persondata = try Data(contentsOf: moifile)
let person = try PropertyListDecoder().decode(Person.self, from: persondata)
print(person) // "Matt Neuburg"

保存可编码的 Person 对象数组的方法与保存单个对象相同。 Codable 的实现可能更复杂,特别是当编码数据的格式不由自己控制时,如通过服务器规定的JSON API进行通信。

虽然有了 Codable ,但仍可能需要使用 NSCoding ,因为Cocoa是用Objective - C编写的,其可编码对象类型采用 NSCoding 而非 Codable 。不过,可通过 NSCoder encodeEncodable(_:forKey:) decodeDecodable(_:forKey:) 方法结合Swift的 Codable 和Cocoa的 NSCoding

以下是一个视图控制器参与视图控制器状态保存和恢复的示例:

class Pep: UIViewController {
    let boy : String
    // ...
    override func encodeRestorableState(with coder: NSCoder) {
        super.encodeRestorableState(with:coder)
        coder.encode(self.boy, forKey:"boy")
    }
}

// 结合Codable和NSCoding
class Pep: UIViewController {
    let boy : String
    let prop : MyStruct // adopts Codable
    // ...
    override func encodeRestorableState(with coder: NSCoder) {
        super.encodeRestorableState(with:coder)
        coder.encode(self.boy, forKey: "boy")
        let arch = coder as! NSKeyedArchiver
        try! arch.encodeEncodable(self.prop, forKey: "prop")
    }
}

decodeRestorableState(with:) 的实现类似,将编码器转换为 NSKeyedUnarchiver 并调用 decodeDecodable(_:forKey:) 提取编码的结构体。

3. 序列化模式对比

3.1 序列化模式总结

序列化模式 适用场景 编码方法 解码方法
属性列表 数据结构简单,适合存储基本类型数据 PropertyListEncoder().encode(_:) PropertyListDecoder().decode(_:from:)
JSON 用于网络数据传输,与服务器交互 JSONEncoder().encode(_:) JSONDecoder().decode(_:from:)
NSCoder 处理Cocoa对象,如 UIColor NSKeyedArchiver.encodeEncodable(_:forKey:) NSKeyedUnarchiver.decodeDecodable(_:forKey:)

3.2 选择建议

在选择序列化模式时,可参考以下几点:
- 简单数据存储 :若数据为基本类型,如字符串、数组、字典等,且不需要与外部服务器交互,属性列表是不错的选择,其实现简单,与 NSKeyedArchiver 底层原理相似。
- 网络通信 :当需要与服务器进行数据交互时,JSON是首选,因为JSON是一种通用的数据交换格式,大多数服务器API都支持。
- Cocoa对象处理 :对于Cocoa框架中的对象,如 UIColor UIImage 等,由于它们采用 NSCoding 协议,需使用 NSKeyedArchiver NSKeyedUnarchiver 进行编码和解码。

3.3 示例对比

以下是使用不同序列化模式保存和读取 Person 对象的示例:

属性列表模式
// 保存
let moi = Person(firstName: "Matt", lastName: "Neuburg")
let moidata = try PropertyListEncoder().encode(moi)
let moifile = docsurl.appendingPathComponent("moi.plist")
try moidata.write(to: moifile, options: .atomic)

// 读取
let persondata = try Data(contentsOf: moifile)
let person = try PropertyListDecoder().decode(Person.self, from: persondata)
print(person) // "Matt Neuburg"
JSON模式
// 保存
let jsonData = try JSONEncoder().encode(moi)
let jsonFile = docsurl.appendingPathComponent("moi.json")
try jsonData.write(to: jsonFile, options: .atomic)

// 读取
let jsonPersondata = try Data(contentsOf: jsonFile)
let jsonPerson = try JSONDecoder().decode(Person.self, from: jsonPersondata)
print(jsonPerson) // "Matt Neuburg"
NSCoder模式
// 保存
let archiver = NSKeyedArchiver(requiringSecureCoding: true)
archiver.encodeEncodable(moi, forKey: "person")
let archivedData = archiver.encodedData
let archivedFile = docsurl.appendingPathComponent("moi.archived")
try archivedData.write(to: archivedFile, options: .atomic)

// 读取
let archivedPersondata = try Data(contentsOf: archivedFile)
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: archivedPersondata)
let archivedPerson = try unarchiver.decodeDecodable(Person.self, forKey: "person")
print(archivedPerson) // "Matt Neuburg"

4. 数据处理流程总结

4.1 Core Motion数据处理流程

graph LR
    A[初始化CMMotionManager] --> B[检查授权]
    B -->|已授权| C[选择数据类型]
    B -->|未授权| D[请求授权]
    D --> B
    C -->|CMMotionActivityManager| E[实时更新或查询历史数据]
    C -->|CMPedometer| F[检查功能可用性并查询数据]
    C -->|CMAltimeter| G[检查高度测量可用性并获取数据]
    C -->|CMSensorRecorder| H[检查记录可用性并记录/检索数据]
    E --> I[处理数据]
    F --> I
    G --> I
    H --> I

4.2 持久存储数据处理流程

graph LR
    A[确定存储需求] --> B[选择存储目录]
    B -->|Documents目录| C[创建文件或文件夹]
    B -->|Application Support目录| C
    B -->|缓存或临时目录| C
    C --> D[选择数据类型]
    D -->|NSString/NSData/NSArray/NSDictionary| E[直接保存/读取]
    D -->|其他类型对象| F[序列化对象]
    F -->|NSCoding| G[使用NSKeyedArchiver/NSKeyedUnarchiver]
    F -->|Codable| H[选择序列化模式]
    H -->|属性列表| I[使用PropertyListEncoder/PropertyListDecoder]
    H -->|JSON| J[使用JSONEncoder/JSONDecoder]
    H -->|NSCoder| K[使用NSKeyedArchiver/NSKeyedUnarchiver结合Codable]
    E --> L[完成存储/读取]
    G --> L
    I --> L
    J --> L
    K --> L

5. 注意事项总结

5.1 Core Motion注意事项

  • 仅创建一个 CMMotionManager 实例,避免资源浪费。
  • 及时停止不必要的传感器更新,以减少电池消耗。
  • 确保在 Info.plist 中添加“Privacy - Motion Usage Description”键,解释使用目的,获取用户授权。

5.2 持久存储注意事项

  • 避免存储绝对文件URL,防止应用重启后URL失效。
  • 根据数据的重要性和使用场景选择合适的存储目录。
  • 对于不同类型的对象,选择合适的序列化方法进行存储和读取。

通过以上对Core Motion和持久存储技术的详细解析,我们了解了如何使用Core Motion框架获取设备的运动数据,以及如何将数据持久存储在设备上。在实际开发中,可根据具体需求选择合适的技术和方法,确保应用的性能和数据的安全性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值