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服务.再次开启后,后台任务时间会被刷新.

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 文档app在后台时会被暂停,暂停的apps会提高电池的使用寿命,并且会让系统将重要的系统资源投入到引起用户注意的前...
    zziazm阅读 4,579评论 0 5
  • IOS开发之----详解在IOS后台执行 文一 我从苹果文档中得知,一般的应用在进入后台的时候可以获取一定时间来...
    dongfang阅读 1,379评论 0 7
  • 原文地址:http://dxjia.cn/2016/05/26/ios-background-executions...
    苹果API搬运工阅读 846评论 0 4
  • 文一 我从苹果文档中得知,一般的应用在进入后台的时候可以获取一定时间来运行相关任务,也就是说可以在后台运行一小段时...
    Kloar阅读 1,471评论 0 1
  • 在iOS后台执行是本文要介绍的内容,大多数应用程序进入后台状态不久后转入暂停状态。在这种状态下,应用程序不执行任何...
    我的马里奥兄弟阅读 827评论 0 0