1. iOS后臺運行
iOS后臺運行分為三種
后臺任務
App在進入后臺后還有任務沒執行完,還需要運行一小段時間,那么可以用Background Task相關API向系統申請運行權限,運行完了再通知系統可以掛起App了后臺模式
需要后臺長時間運行任務的App都需要顯式向系統申請權限,如Background Fetch,允許App不定時被喚醒來更新一些數據。后臺下載
專指由配置了backgroundSessionConfiguration的NSURLSession管理的下載過程。由系統進程接管App數據的下載,因此即便App被系統掛起,甚至殺死或崩潰了,也能繼續下載。下載完成后App會被喚醒,處理一些狀態更新和回調。
1.1. iOS后臺模式
- Audio, AirPlay,and Picture in Picture
- Location updates
- Voice over IP VoIP
- External accessory communication
- Uses Bluetooth LE accessories
- Acts as a Bluetooth LE accessory
- Background fetch
- Remote notifications
- iOS13 新增 Background processing
1.2. iOS App 后臺保活方式簡介
1.2.1. 短時間APP后臺保活(有時間限制30s)
該種方式屬于后臺任務,是調用相關的api來實現
// 開啟后臺任務
- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler
- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithName:(nullable NSString *)taskName expirationHandler:(void(^ __nullable)(void))handler
// 結束后臺任務
- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier
實測,在有音樂播放或錄音時,后臺任務申請的時間會無限大
1.2.2. 當app需要支持在后臺下載文件時
可以通過設置urlsession的background模式來讓下載任務傳遞給系統,這樣當系統需要終止APP時會自動接收未下載完成的任務,并在下載完成后調用相應的api進行處理
1.2.3. Audio, AirPlay,and Picture in Picture 模式
應用在后臺時可以播放聲音信息。
可以利用此模式播放無聲音樂,App 進入后臺后,播放無聲音樂,配合beginBackgroundTaskWithName對系統申請后臺使用時間,可以使APP在后臺長時間保活。
1.2.4. Location updates 模式
應用提供位置信息 應用場景:在后臺時需要不斷通知用戶位置更新信息。
通過后臺持續定位App,可以實現App后臺保活
如果持續后臺播放無聲音頻或是使用后臺持續定位的方式實現iOS App后臺保活,會浪費電量,浪費CPU
實測,用戶定位權限為kCLAuthorizationStatusAuthorizedWhenInUse,此時使用定位保活,并不一定能永久保活,在定位權限為kCLAuthorizationStatusAuthorizedAlways時,使用定位保活,可以永久保活
1.2.5 VoIP 模式
VoIP是能真正做到在App掛起和被殺死情況下實時拉起應用的方法。當然它也有一定的局限性,應用必須要是VoIP應用,即應用中有類似視頻呼叫或者語音呼叫等功能。
1.2.5. Background fetch 模式
應用場景:需不斷地頻繁的基于一定規律從網絡上獲取新的數據,大多數APP的后臺刷新都是使用此模式來完成。
1.2.6. Remote notifications 模式
iOS的靜默推送:收到推送(沒有文字沒有聲音),不用點開通知,不用打開APP,就能執行
-application:didReceiveRemoteNotification:fetchCompletionHandler:,用戶完全感知不到
靜默推送的缺點是:
1、如果應用已經被Kill。是無法自動拉起應用的。所以它只能在應用后臺掛起的情況下使用。
2、靜默推送和無法保證應用被實時喚醒。官網說法如下:
靜默推送不是讓您的應用程序在快速刷新操作之后保持醒來的方式,也不是用于高優先級更新的方式。>APN將后臺更新通知視為低優先級,如果總數過多,APN可能會將其傳輸完全限制在一定程度。實際的限>制是動態的,可以根據條件進行更改,但不要每小時發送一次以上的通知。
1.2.7. External accessory communication 模式
有規律的從外部藍牙設備獲取信息, 可以在后臺不斷的與外設進行溝通,開啟后可讓應用不斷的與外設進行溝通。
1.2.8. Uses Bluetooth LE accessories/Acts as a Bluetooth LE accessory 模式
這兩種模式區別是一個是將設備作為外圍設備,一個是將設備作為中心設備。需要在后臺不斷訪問其他藍牙設備獲取數據或不斷更新藍牙狀態。
1.2.9 Background processing
這是iOS13新增的一個模式,基于BackgroundTasks,
有點在于不會檢測cpu的占用率,也會啟動應用的后臺任務。
2.后臺保活
后臺任務+定位保活+無聲音樂,實現永遠保活
先開啟后臺任務,后臺任務大概30s,再在后臺任務過期時,如果定位權限是kCLAuthorizationStatusAuthorizedAlways,每隔10s定位一次,如果定位權限不是kCLAuthorizationStatusAuthorizedAlways,就每隔10s播放下無聲音樂
核心代碼
- (void)startBackRuning {
NSLog(@"%@ startBackRuning",NSStringFromClass([self class]));
self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
[self startDoTask];
}];
}
- (void)stopBackRuning {
NSLog(@"%@ stopBackRuning",NSStringFromClass([self class]));
if (self.backgroundTaskIdentifier) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}
[self stopDoTask];
}
- (void)startDoTask {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(startDoTask) object:nil];
[self doTask];
[self performSelector:@selector(startDoTask) withObject:nil afterDelay:10];
}
- (void)doTask {
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedAlways) {
// 用戶允許持續定位,使用定位保活
[self locationTask];
}else {
[self playTask];
}
}
- (void)stopDoTask {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(startDoTask) object:nil];
}
- (void)locationTask {
[self.locationManager requestLocation];
NSLog(@"%@ locationTask",NSStringFromClass([self class]));
}
- (void)playTask {
[self setAudioPlaySession];
[self playSound];
NSLog(@"%@ playTask",NSStringFromClass([self class]));
}
- (void)setAudioPlaySession {
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
if([NSThread mainThread]){
[audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
[audioSession setActive:YES error:nil];
}else{
dispatch_async(dispatch_get_main_queue(), ^{
[audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
[audioSession setActive:YES error:nil];
});
}
}
- (void)playSound
{
if (!self.audioPlayer) {
// 播放文件
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"RunInBackground" ofType:@"mp3"];
NSURL *fileURL = [[NSURL alloc] initFileURLWithPath:filePath];
if (!fileURL) {
NSLog(@"playEmptyAudio 找不到播放文件");
}
// 0.0~1.0,默認為1.0
NSError *error = nil;
self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error];
self.audioPlayer.volume = 0.0;
// 循環播放 保活在后臺導航時 容易不生效
// self.audioPlayer.numberOfLoops = -1;
// [self.audioPlayer prepareToPlay];
}
[self.audioPlayer play];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.audioPlayer pause];
self.audioPlayer = nil;
});
}