Keep Alive in Background

开始

在项目中遇上了一个需要常驻后台并且轮询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方法的参数.

能否通过这种方式获取更多的时间呢?能!

有两种方式:

  1. 在过期handler里面再次begin,形成一个循环,这样的确能保证app不会被挂起.但在测试中,有线程混乱的现象(sleep(1)这个方法无法正常执行).没有深究,并没有采用.
  2. 小技巧,往下看!

不限时间

提供了3种方式:

  1. GPS
  2. Audio
  3. VOIP

当然,如果是提交App Store的话,采用某种方式就必须有相关的业务需求,不然会被拒.不过不上的话,那就没关系~

使用GPS,和普通的位置服务一模一样,没有任何区别.只是在申请权限为永久而非应用内.当位置发生改变,iOS会唤醒app,进入代理方法didUpdateLocations.

不过这似乎不符合需求,位置不变的情况下,app仍然处于挂起状态.

VOIP是最好的方式.不过需要server端的支持.
需要做3步操作:

  1. 打开VOIP服务:在plist里面直接添加也行,在capabilities中的background modes中勾选更为简洁.
  2. 注册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服务.再次开启后,后台任务时间会被刷新.

那么周期性的开启重复操作,既能够达到常驻后台的目的,又能够基本不费电量.

你可能感兴趣的:(Keep Alive in Background)