iOS保活方案研究

iOS的后台都是假后台,但是这个假后台也不是纯粹的假后台,这是iOS的系统特征。iOS的保活也由于系统的限制是一个十分棘手的问题。并且也没有完美的实现方式,并且随着iOS系统版本的提升,越来越难做了。

保活方式可分为如下两种:

  1. 短时间保活的方式有beginBackgroundTaskWithName;

  2. App长时间保活的方式有:播放无声音乐、后台持续定位、后台下载资源、VOIP等;

短时间App后台保活

1
beginBackgroundTaskWithName` 和 `endBackgroundTask

经过测试不同系统版本差异很大。

对于系统版本低于iOS13(iOS 12.3)的设备(iPhone6 Plus)后台运行时间约3分钟(175秒);

对于系统版本不低于iOS13(iOS 13.0)的设备(iPhone6 Plus)后台运行时间约31秒;

短时间App后台保活

系统版本低于iOS13.0的设备

系统版本低于iOS13.0的设备,在应用进入后台的时候,开始后台任务([[UIApplication sharedApplication] beginBackgroundTaskWithName:)。在应用进入前台时或后台任务快过期的回调中,终止后台任务([[UIApplication sharedApplication] endBackgroundTask:)。

示例代码如下:

- (void)applicationDidEnterBackground:(UIApplication *)application {
    self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:kBgTaskName expirationHandler:^{
       if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
           [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
           self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
       }
    }];
}
- (void)applicationWillEnterForeground:(UIApplication *)application { 
    [[UIApplication sharedApplication] endBackgroundTask: self.backgroundTaskIdentifier];
}

若果没有使用后台任务,那么app进入后台之后定时器就立马本挂起了, 按照上面的方式添加后台任务之后,定时器可以执行30秒左右之后也会被挂起(测试设备为iphone7,系统版本为iOS12.3和iOS14.2)

app长时间保活–Background Modes AVAudio,AirPlay,and Picture in Picture

对于音视频类App,如果需要后台保活App,在App 进入后台后,可以考虑先使用短时间保活App的方式,如果后台保活App方式快结束后,还没处理事情,那么可以考虑使用后台播放无声音乐。相关示例代码如下。

1
2
3
4
5
6
7
8
9
10
11
- (AVAudioPlayer *)player {
    
    if (!_player) {
        NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"SomethingJustLikeThis" withExtension:@"mp3"];
        AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil];
        audioPlayer.numberOfLoops = NSUIntegerMax;
        _player = audioPlayer;
    }
    return _player;
}
[self.player prepareToPlay];

系统版本低于iOS13.0的设备

- (void)applicationDidEnterBackground:(UIApplication *)application {

    NSLog(@"%s:应用进入后台DidEnterBackground", __FUNCTION__);
    self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:kBgTaskName expirationHandler:^{

       if ([QiAudioPlayer sharedInstance].needRunInBackground) {
           [[QiAudioPlayer sharedInstance].player play];
       }
       if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
           [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
           self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
       }
    }];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {

    NSLog(@"%s:应用将进入前台WillEnterForeground", __FUNCTION__);
    if ([QiAudioPlayer sharedInstance].needRunInBackground) {
        [[QiAudioPlayer sharedInstance].player pause];
    }
    [[UIApplication sharedApplication] endBackgroundTask: self.backgroundTaskIdentifier];
}

使用后台播放无声音乐的缺点

此方案原理上可行,但是在实际使用中会有很多问题,基本上无法满足要求, 理由如下:

  1. 正常情况下能够做到保活, 但是在在后台播放被打断之后,无法自动回复(很简单的例子,有是个支持后台播放的app,都退到后台了, 那么到底让谁恢复播放呢,无法确定),app依旧会被杀掉
  2. 后台播放音频会和一些同样有后台音乐播放功能的app(例如,QQ音乐, 酷狗音乐等)冲突, 导致我们的app无法实现后台播放,然后被杀掉
  3. 电量消耗增加

app长时间保活–使用后台定位的方式

前提条件:

  1. 用户允许app始终使用定位权限(需要在app启动的时候弹窗获取权限,如果用户没有同意使用使用定位权限,那么需要提示用户去设置界面手动修改)
  2. App开启后台定位功能
  3. 代码申请始终使用定位权限

实现方案:

注意ios13以后如果申请使用允许定位,还是用 [self.locationManager requestAlwaysAuthorization];,但是权限提示的弹窗不会像iOS13以前的一样直接有始终允许定位这个选项,但是用户只要选择了使用app时允许,那么此时的定位权限就是kCLAuthorizationStatusAuthorizedAlways, 但是当app尝试在后台使用定位的时候,还有有系统级别的弹窗提示用户, 有app在为使用app是获取位置信息, 此时用户又得到一次权限选择的权利,保持仅使用期间更改为始终允许,如果此时用户选择保持仅使用期间,那么定位权限会改为kCLAuthorizationStatusAuthorizedWhenInUse, 并且app在后台使用定位的时候如果是非刘海屏状态栏会有蓝色的状态提示条,在刘海屏表现为左上角有一个蓝色的色块,表示有app在后台使用定位功能,点击蓝色块能直接打开使用后台定位的app(此时app可以保活); 如果用户选择了更改为始终允许,那么app便获取了后台持续定位的功能,并且以后不会再弹出这个权限弹窗.

如果用户开启了定位的始终允许权限,如果app开始了跟踪用户位置信息, 那么在app能常驻后台(不论是否锁频),此时在状态栏会有一个定位的开启的小图标(小箭头),表示有app一追踪用户的位置信息.

具体的实现方式很简单,代码如下:

.h文件

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface HLLocationManager : NSObject
+ (instancetype)singleton;
- (void)start;
- (void)stop;
- (void)requestAuthority;
@end
NS_ASSUME_NONNULL_END

.m文件

#import "HLLocationManager.h"
#import <CoreLocation/CoreLocation.h>

@interface HLLocationManager()<CLLocationManagerDelegate>
@property(nonatomic,strong)CLLocationManager *locationManager;
@end

@implementation HLLocationManager
+ (instancetype)singleton {
  static HLLocationManager *singleton;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    singleton = [[HLLocationManager alloc] init];
  });
  return singleton;
}

- (instancetype)init {
  self = [super init];
  if (self) {
   self.locationManager = [[CLLocationManager alloc] init];
    self.locationManager.delegate = self;
   self.locationManager.allowsBackgroundLocationUpdates = true;
  }
  return self;
}



- (void)requestAuthority {
  [self.locationManager requestAlwaysAuthorization];
}

- (void)start {
  if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedAlways) {
//此处可以做一些容错的逻辑
    NSLog(@"请开启一直定位权限");
//    return;
  }

  [self.locationManager requestAlwaysAuthorization];
  [self.locationManager startUpdatingLocation];
}

- (void)stop {
  [self.locationManager stopUpdatingLocation];
}

#pragma mark - cllocationdelegate

- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
  NSLog(@"定位类型:%d",status);
}

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
  NSLog(@"位置更新");
}

@end

使用后台定位方案实现app保活缺点:

  1. 开启始终允许定位权限之后,如果应用在后台使用定位,在iOS13以及以上版本,会有系统级别的弹窗提示用户:”是否允许xxxapp在您未使用位置信息时使用您的定位权限”(并且在此弹框之前都无法获取定位),并且此时用户还可以修改原来的定位权限为:仅在使用期间允许使用定位权限, 所以一旦用户修改了定位权限为使用期间允许, 那么就无法使用持续定位的方式进行后台保活.

  2. 申请定位权限的时候如果用户选择的是在引用使用时允许, 那么退到后台之后,状态栏会变成蓝色, 提示用户有引用在后台使用定位功能.此时app也能实现常驻后台,并且锁屏也不会被杀掉.

  3. 如果用户开启了定位的始终允许权限,如果app开始了跟踪用户位置信息, 那么在app能常驻后台(不论是否锁频),此时在状态栏会有一个定位的开启的小图标(小箭头),会引起用户的警觉.

  4. Energy Impact 电量消耗会增加, 能耗一直标识为high或者very high