開始
在項(xiàng)目中遇上了一個(gè)需要常駐后臺(tái)并且輪詢Http的需求(不上App Store),所以整理下后臺(tái)常駐的方式.
iOS是偽多任務(wù)系統(tǒng),當(dāng)按下home鍵,app就會(huì)處于掛起狀態(tài),不執(zhí)行任何操作.不過(guò)很多情況下,這不是我們希望的,iOS提供了兩類后臺(tái)工作的方式:
- 有限長(zhǎng)時(shí)間
- 不限時(shí)間
通過(guò)這兩類方式,均可以實(shí)現(xiàn)常駐后臺(tái)的需求.
有限長(zhǎng)時(shí)間
有限長(zhǎng)時(shí)間,那么是多長(zhǎng)的時(shí)間呢:答案是180s(iOS9)
通過(guò)簡(jiǎn)單的代碼可以查看到
NSLog(@"%.1f",[UIApplication sharedApplication].backgroundTimeRemaining);
//=> 179.9s
也就是說(shuō),可以在向系統(tǒng)申請(qǐng)大約3分鐘的時(shí)間執(zhí)行自己的任務(wù).大約的意思就是不精確.事實(shí)上通過(guò)timer進(jìn)行計(jì)時(shí),在還剩下4s左右的時(shí)候,任務(wù)就已經(jīng)停止,開始執(zhí)行超時(shí)的收尾工作,app隨后被掛起.
做法很簡(jiǎn)單,不用做任何的設(shè)置.在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);
}
然后你會(huì)發(fā)現(xiàn),timer只會(huì)執(zhí)行一次...原因是,并沒(méi)有向系統(tǒng)申請(qǐng).加上申請(qǐng)權(quán)限即可:
- (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];
}
這樣,就會(huì)獲得大約180s的執(zhí)行時(shí)間.在時(shí)間完畢后,系統(tǒng)會(huì)執(zhí)行過(guò)期handler,進(jìn)行一些收尾工作,也就是beginBackgroundTaskWithExpirationHandler
方法的參數(shù).
能否通過(guò)這種方式獲取更多的時(shí)間呢?能!
有兩種方式:
- 在過(guò)期handler里面再次begin,形成一個(gè)循環(huán),這樣的確能保證app不會(huì)被掛起.但在測(cè)試中,有線程混亂的現(xiàn)象(sleep(1)這個(gè)方法無(wú)法正常執(zhí)行).沒(méi)有深究,并沒(méi)有采用.
- 小技巧,往下看!
不限時(shí)間
提供了3種方式:
- GPS
- Audio
- VOIP
當(dāng)然,如果是提交App Store的話,采用某種方式就必須有相關(guān)的業(yè)務(wù)需求,不然會(huì)被拒.不過(guò)不上的話,那就沒(méi)關(guān)系~
使用GPS,和普通的位置服務(wù)一模一樣,沒(méi)有任何區(qū)別.只是在申請(qǐng)權(quán)限為永久而非應(yīng)用內(nèi).當(dāng)位置發(fā)生改變,iOS會(huì)喚醒a(bǔ)pp,進(jìn)入代理方法didUpdateLocations
.
不過(guò)這似乎不符合需求,位置不變的情況下,app仍然處于掛起狀態(tài).
VOIP是最好的方式.不過(guò)需要server端的支持.
需要做3步操作:
- 打開VOIP服務(wù):在plist里面直接添加也行,在capabilities中的background modes中勾選更為簡(jiǎn)潔.
- 注冊(cè)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
屬性設(shè)置為NSURLNetworkServiceTypeVoIP
.如果是使用web socket的話,這個(gè)庫(kù)是一個(gè)很好的選擇.
3.在applicationDidEnterBackground
中調(diào)用setKeepAliveTimeout:handler:
方法.該方法可以用來(lái)執(zhí)行ping/pong等操作.
使用socket的方式對(duì)于輪詢http的需求來(lái)講是最好的方案,通過(guò)配置VOIP來(lái)保持后臺(tái)常駐也是很好的方案.可惜需要server端的支持.為了趕需求只能后續(xù)采用:(.
最后來(lái)使用Audio吧
推薦使用AVQueuePlayer,它自帶了一個(gè)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];
}
都是一些基礎(chǔ)的使用方式:
- volume設(shè)為0,表示無(wú)聲;
- count是一個(gè)static的值,起計(jì)數(shù)作用;
-
addPeriodicTimeObserverForInterval
方法設(shè)置一個(gè)1秒左右的周期性執(zhí)行的block; -
infinityPlaying
這個(gè)方法會(huì)讓一首歌曲無(wú)限循環(huán).
如何處理呢?方式很簡(jiǎn)單.當(dāng)計(jì)數(shù)(count)達(dá)到一定的值的時(shí)候,player可以seekTime
到0,從頭開始播放即可:
//static NSInteger MCInfinityCount = 50;
- (void)infinityPlaying {
if (count == MCInfinityCount) {
[_player seekToTime:kCMTimeZero];
count = 0;
}
}
這個(gè)值取多少呢?可以調(diào)整,取得小則seek的次數(shù)較多;取得大則意味著這首歌曲本身較大,占用的內(nèi)存多;最終根據(jù)實(shí)際情況取舍.
最后通過(guò)AVAudioSessionInterruptionNotification
通知來(lái)處理打斷事件:notification的參數(shù)會(huì)表示打斷事件的begin和end.
注意,當(dāng)AVAudioSession
的option如果不是AVAudioSessionCategoryOptionMixWithOthers
的時(shí)候,處理打斷事件end時(shí),調(diào)用[_player play]
無(wú)效,不會(huì)恢復(fù)播放,自然周期性執(zhí)行的block也不會(huì)恢復(fù).
但是這樣很費(fèi)電啊
這樣就等于一直在聽歌...
有沒(méi)有更好的方式呢?
也就是上面說(shuō)的一個(gè)小技巧.
通過(guò)beginBackgroundTaskWithExpirationHandler
來(lái)注冊(cè)一個(gè)后臺(tái)有限長(zhǎng)時(shí)間任務(wù).
通過(guò)audio服務(wù)來(lái)刷新task的剩余時(shí)間(backgroundTimeRemaining),這樣組合則能同樣達(dá)到不限時(shí)間的效果.
首先準(zhǔn)備一個(gè)超短音頻,大約零點(diǎn)幾秒,我這里的音頻文件大小為7k.
然后同有限長(zhǎng)時(shí)間后臺(tái)任務(wù)一樣,沒(méi)有任何區(qū)別:
- (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];
}
因?yàn)槭欠浅6痰囊纛l文件,所以從生成一個(gè)player到加載音頻文件,到播放完畢,會(huì)在瞬間完成.對(duì)于資源的消耗非常少.也不存在費(fèi)電的問(wèn)題.
而當(dāng)audio播放的時(shí)候,后臺(tái)任務(wù)時(shí)間(backgroundTimeRemaining)是"無(wú)限"的.當(dāng)auido播放完畢的時(shí)候,后臺(tái)任務(wù)時(shí)間會(huì)持續(xù)5秒左右仍然"無(wú)限".隨后進(jìn)入倒計(jì)時(shí)狀態(tài).
MCChat[3167:1406329] time remain:179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0
MCChat[3167:1406329] time remain:179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0
在倒計(jì)時(shí)中,可以做任何事情.當(dāng)然也可以再次開啟一個(gè)audio服務(wù).再次開啟后,后臺(tái)任務(wù)時(shí)間會(huì)被刷新.
那么周期性的開啟重復(fù)操作,既能夠達(dá)到常駐后臺(tái)的目的,又能夠基本不費(fèi)電量.