开始
在项目中遇上了一个需要常驻后台并且轮询Http的需求(不上App Store),所以整理下后台常驻的方式.
iOS是伪多任务系统,当按下home键,app就会处于挂起状态,不执行任何操作.不过很多情况下,这不是我们希望的,iOS提供了两类后台工作的方式:
- 有限长时间
- 不限时间
通过这两类方式,均可以实现常驻后台的需求.
有限长时间
有限长时间,那么是多长的时间呢:答案是180s(iOS9)
通过简单的代码可以查看到
NSLog(@"%.1f",[UIApplication sharedApplication].backgroundTimeRemaining);
//=> 179.9s
也就是说,可以在向系统申请大约3分钟的时间执行自己的任务.大约的意思就是不精确.事实上通过timer进行计时,在还剩下4s左右的时候,任务就已经停止,开始执行超时的收尾工作,app随后被挂起.
做法很简单,不用做任何的设置.在applicationDidEnterBackground:
中直接书写代码即可:
- (void)applicationDidEnterBackground:(UIApplication *)application {
_counter = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(counter1) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_counter forMode:NSDefaultRunLoopMode];
[_counter fire];
}
- (void)counter1 {
NSLog(@"%.1f",[UIApplication sharedApplication].backgroundTimeRemaining);
}
然后你会发现,timer只会执行一次...原因是,并没有向系统申请.加上申请权限即可:
- (void)applicationDidEnterBackground:(UIApplication *)application {
__block UIBackgroundTaskIdentifier taskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:taskIdentifier];
taskIdentifier = UIBackgroundTaskInvalid;
}];
_counter = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(counter1) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_counter forMode:NSDefaultRunLoopMode];
[_counter fire];
}
这样,就会获得大约180s的执行时间.在时间完毕后,系统会执行过期handler,进行一些收尾工作,也就是beginBackgroundTaskWithExpirationHandler
方法的参数.
能否通过这种方式获取更多的时间呢?能!
有两种方式:
- 在过期handler里面再次begin,形成一个循环,这样的确能保证app不会被挂起.但在测试中,有线程混乱的现象(sleep(1)这个方法无法正常执行).没有深究,并没有采用.
- 小技巧,往下看!
不限时间
提供了3种方式:
- GPS
- Audio
- VOIP
当然,如果是提交App Store的话,采用某种方式就必须有相关的业务需求,不然会被拒.不过不上的话,那就没关系~
使用GPS,和普通的位置服务一模一样,没有任何区别.只是在申请权限为永久而非应用内.当位置发生改变,iOS会唤醒app,进入代理方法didUpdateLocations
.
不过这似乎不符合需求,位置不变的情况下,app仍然处于挂起状态.
VOIP是最好的方式.不过需要server端的支持.
需要做3步操作:
- 打开VOIP服务:在plist里面直接添加也行,在capabilities中的background modes中勾选更为简洁.
- 注册VOIP(code from SRWebSocket).
CFReadStreamRef readStream = NULL;
CFWriteStreamRef writeStream = NULL;
CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream);
_outputStream = CFBridgingRelease(writeStream);
_inputStream = CFBridgingRelease(readStream);
[_inputStream setProperty:networkServiceType forKey:NSStreamNetworkServiceTypeVoIP];
[_outputStream setProperty:networkServiceType forKey:NSStreamNetworkServiceTypeVoIP];
顺便提下,square的SocketRocket本身支持VOIP,只需给urlRequest的networkServiceType
属性设置为NSURLNetworkServiceTypeVoIP
.如果是使用web socket的话,这个库是一个很好的选择.
3.在applicationDidEnterBackground
中调用setKeepAliveTimeout:handler:
方法.该方法可以用来执行ping/pong等操作.
使用socket的方式对于轮询http的需求来讲是最好的方案,通过配置VOIP来保持后台常驻也是很好的方案.可惜需要server端的支持.为了赶需求只能后续采用:(.
最后来使用Audio吧
推荐使用AVQueuePlayer,它自带了一个Timer类似的方法:addPeriodicTimeObserverForInterval
.
- (void)setupPlayer {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
NSURL *url = [[NSBundle mainBundle] URLForResource:@"song" withExtension:@"mp3"];
AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:url];
_player = [[AVQueuePlayer alloc] initWithPlayerItem:item];
_player.volume = 0;
__weak typeof(self) weakSelf = self;
[_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
++count;
[weakSelf infinityPlaying];
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
//do what you want to do
}
}];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dealWithInterrpution:) name:AVAudioSessionInterruptionNotification object:nil];
}
都是一些基础的使用方式:
- volume设为0,表示无声;
- count是一个static的值,起计数作用;
-
addPeriodicTimeObserverForInterval
方法设置一个1秒左右的周期性执行的block; -
infinityPlaying
这个方法会让一首歌曲无限循环.
如何处理呢?方式很简单.当计数(count)达到一定的值的时候,player可以seekTime
到0,从头开始播放即可:
//static NSInteger MCInfinityCount = 50;
- (void)infinityPlaying {
if (count == MCInfinityCount) {
[_player seekToTime:kCMTimeZero];
count = 0;
}
}
这个值取多少呢?可以调整,取得小则seek的次数较多;取得大则意味着这首歌曲本身较大,占用的内存多;最终根据实际情况取舍.
最后通过AVAudioSessionInterruptionNotification
通知来处理打断事件:notification的参数会表示打断事件的begin和end.
注意,当AVAudioSession
的option如果不是AVAudioSessionCategoryOptionMixWithOthers
的时候,处理打断事件end时,调用[_player play]
无效,不会恢复播放,自然周期性执行的block也不会恢复.
但是这样很费电啊
这样就等于一直在听歌...
有没有更好的方式呢?
也就是上面说的一个小技巧.
通过beginBackgroundTaskWithExpirationHandler
来注册一个后台有限长时间任务.
通过audio服务来刷新task的剩余时间(backgroundTimeRemaining),这样组合则能同样达到不限时间的效果.
首先准备一个超短音频,大约零点几秒,我这里的音频文件大小为7k.
然后同有限长时间后台任务一样,没有任何区别:
- (void)applicationDidEnterBackground:(UIApplication *)application {
__block UIBackgroundTaskIdentifier taskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:taskIdentifier];
taskIdentifier = UIBackgroundTaskInvalid;
}];
_timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(playeAudio) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
[_timer fire];
//do what you want to do
}
- (void)playAudio {
NSLog(@"time remain:%.1f",[UIApplication sharedApplication].backgroundTimeRemaining);
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
NSURL *url = [[NSBundle mainBundle] URLForResource:@"mute" withExtension:@"wav"];
[_player stop];
_player = nil;
_player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
[_player play];
}
因为是非常短的音频文件,所以从生成一个player到加载音频文件,到播放完毕,会在瞬间完成.对于资源的消耗非常少.也不存在费电的问题.
而当audio播放的时候,后台任务时间(backgroundTimeRemaining)是"无限"的.当auido播放完毕的时候,后台任务时间会持续5秒左右仍然"无限".随后进入倒计时状态.
MCChat[3167:1406329] time remain:179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0
MCChat[3167:1406329] time remain:179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0
在倒计时中,可以做任何事情.当然也可以再次开启一个audio服务.再次开启后,后台任务时间会被刷新.
那么周期性的开启重复操作,既能够达到常驻后台的目的,又能够基本不费电量.