今天下了个软件,可以记录手机解锁的次数和使用时间,当然啦,App 必须在后台运行着。当时比较纳闷的是有什么 API 可以接收设备解锁事件或通知的,Google 了下,还真有哎——我是链接。
设备锁定的状态
由上面的回答可以知道,设备在锁定、解锁的时候,SpringBoard 都会发出通知,iPhoneDevWiki 这里能找到更多有趣的通知(注意:黄色标注的通知是有状态变量与之关联的,后面会用到)。贴订阅通知的代码:
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), // 获取通知中心
NULL, // 设置观察者
deviceLockStatusChanged, // 接收到通知时的回调函数
CFSTR("com.apple.springboard.lockstate"), // 通知名
NULL, // 要观察的对象
CFNotificationSuspensionBehaviorDeliverImmediately // 决定应用在后台如何处理通知的标记
);
这里所用的通知中心并不是我们常用的 [NSNotificationCenter defalutCenter]
,而是 CFNotificationCenterRef
对象。提一下,即便前者的底层确实是由 CFNotificationCenter
实现的,但它们两者不是 Toll-Free Bridged。回到 CFNotificationCenterRef
,有下面三个函数获得不同的通知中心:
- CFNotificationCenterGetLocalCenter(void);
- CFNotificationCenterGetDistributedCenter(void);
- CFNotificationCenterGetDarwinNotifyCenter(void);
第一个是我们熟悉的 Local Center,可以理解为通知的行为完全由本进程维护,作用域也仅在本进程;
第二个是 Distributed Center,如果有看到这个函数声明上的编译条件,你就会发现仅在桌面系统上才有 Distributed Center 可用。它可以实现两个进程之间的通信,感兴趣可以看看 Communicating With the Target Application ,似乎是通过 BundleID 实现对特定应用发送通知;
第三个是本文的重点,Darwin Center 的服务由系统的一个守护进程维护,相比 Local Center,通知的作用域扩大到了所有进程。这就为什么我们的应用能够接收到 SpringBoard.app 发送的通知。本文用的是用 CoreFoundation 的函数实现接收通知,除此之外,文档里还提到了利用 Mach Port, File Descriptors, Signal 等方法。查看 Darwin Notification Concepts 了解更多。
接下来要讲讲回调函数了:
static void deviceLockStatusChanged(CFNotificationCenterRef center,
void *observer,
CFStringRef name,
const void *object,
CFDictionaryRef userInfo){
NSString *nameString = (__bridge NSString*)name;
int token;
uint64_t state;
notify_register_check(nameString.UTF8String, &token);
notify_get_state(token, &state);
NSLog(@"%@: token: %d, state: %llu", nameString, token, state);
if (state == 0) {
counter++;
}
notify_cancel(token);
}
函数的原型是直接抄文档的,notify_register_check()
可以生成一个 token 值用来关联某一个通知,接着用 notify_get_state()
就可以获得响应状态值。最后是 notify_cancel()
,它用来取消跟 token 相关联的通知和释放相应的资源,按 manual pages 的描述好像只针对利用 Mach Port 和 File Descriptors 接收消息时创建的资源。具体到这个回调函数,不太清楚底层做了什么,但我们能看到的是 token 被清零了。
后台运行
获得设备锁定、解锁的方法有了,接着是要让应用保存生命力,不能让它被挂起,否则就统计不了次数了。比较常见的方法就是循环播放一段空白的声音,然后在 Info.plist 里面添加相应的字段(KEY: UIBackgroundModes, VALUE: audio),或者直接在 Capabilities 里面的 Background Modes 中 Audio 的复选框中打个勾。
完
🌝