原文发布于我的博客:http://blog.zyliu.com/ios-proximity-state-bug-and-solution/
实习的时候写公司产品,有个用到接近感应器的功能。就比如打电话,电话接通时开启接近感应器,侦测到接近状态改变(接近/离开)时执行相应的操作——当开启接近感应器时,系统会在接近时熄灭屏幕,离开时再点亮屏幕,等等。但是在这个过程中有bug存在,导致系统接口给的结果不一定是准确的。
先说理论上的实现
在 UIKit/UIDevice.h
中的 UIDevice
类(iOS 8.4 SDK),有如下属性
@property(nonatomic,getter=isProximityMonitoringEnabled) BOOL proximityMonitoringEnabled NS_AVAILABLE_IOS(3_0); // default is NO
@property(nonatomic,readonly) BOOL proximityState NS_AVAILABLE_IOS(3_0); // always returns NO if no proximity detector
proximityMonitoringEnabled
用来标识是否开启接近感应器,如果为 YES
则开启。proximityState
为当前的接近状态,如果为 YES
则为接近(触发),否则为离开(未触发),需要的时候可以直接拿来用。
以及用于 UINotificationCenter
的键:
UIKIT_EXTERN NSString *const UIDeviceProximityStateDidChangeNotification NS_AVAILABLE_IOS(3_0);
于是如果需要在某个地方使用接近感应器,可以注册通知并且开启接近感应器:
UIDevice *device = [UIDevice currentDevice];
[device setProximityMonitoringEnabled:YES];
if ([device isProximityMonitoringEnabled]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(proximityStateDidChange:)
name:UIDeviceProximityStateDidChangeNotification object:nil];
}else {
NSLog(@"No Proximity Sensor");
}
然后在 -(void)proximityStateDidChange:(BOOL)
方法里面实现需要做的处理。当然记得在不需要的时候取消注册通知、停止接近检测。
这样看起来是没问题的。
突然问题来了
直到有一天发现了一个问题:在已经开启接近检测的情况下,同时触发接近感应器和进入后台(按 Home 键的同时捂住听筒),这样会在熄灭屏幕的情况下进入了后台。手离开听筒,屏幕再次点亮。再进入前台,发现 [UIDevice currentDevice].proximityState
的值为 YES
。也就是说,触发接近感应器的同时进入后台,在后台时离开接近感应器是不会刷新接近状态的(会保持在触发状态)。在这种情况下,系统提供的接口结果不正确。经验证,从 iOS 7
到 iOS 9
都存在这个问题。只有再次触发接近感应器并离开时,才会收到 UIDeviceProximityStateDidChangeNotification
通知,也就是变回正常了。这样看来,UIDevice
的 proximityState
属性也是依赖于上面的通知更新吧。
解决方案
好了,既然 iOS
留下了这个 Bug,下面就是如何想办法解决它。对于接近感应器,我们需要的很简单,就是在任何情况下拿到的数据都是真实有效的。有这样一种思路:每次从后台进入前台时,是用户在屏幕亮着并且可操作的情况下进来的,进来之前的瞬间不可能是接近状态。所以在进入前台后到收到接近状态改变的通知前的这段时间,可以推测是非接近状态。
在这样的思路下,我们就可以做一个简单的接近检测的工具类,添加一个 proximityState
属性,将 get
方法写为:
- (BOOL)proximityState{
if (self.isWaitingProximityStateUpdate) {
return NO;
}else {
return [[UIDevice currentDevice] proximityState];
}
}
其中,isWaitingProximityStateUpdate
表示是否为进入前台后到收到接近状态改变通知前的这段时间。每次进入前台就把这个属性置为 YES
,收到接近状态改变通知就置为 NO
。
这样,通过这个工具类的 proximityState
属性,在任何情况下拿到的结果都是真实有效的。