iOS后台任务beginBackgroundTask和endBackgroundTask成对出现

接到线上有人反馈我们的app经常在后台被杀死, 一开始以为是系统机制问题, 后来发现很大程度上是beginBackgroundTask && endBackgroundTask 没有使用正确, 导致后台任务超时, 然后被系统kill. 

1.标准写法

@property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTask;


// APP进入后台的通知
- (void)applicationDidEnterBackgroundWithNote:(NSNotification *)note {
    [self _beginBackgroundTask];
}
 
// APP回到前台的通知
- (void)applicationWillEnterForegroundWithNote:(NSNotification *)note {
    [self _endBackgroundTaskIfNeeded];
}
 
 
- (void)_beginBackgroundTask {
    [self _endBackgroundTaskIfNeeded];
     
    _backgroundTask = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^{
        [self _endBackgroundTaskIfNeeded];
    }];
    
}
 
- (void)_endBackgroundTaskIfNeeded {
    if ( _backgroundTask != UIBackgroundTaskInvalid ) {
        [UIApplication.sharedApplication endBackgroundTask:_backgroundTask];
        _backgroundTask = UIBackgroundTaskInvalid;
    }
}

可以通过打印UIApplication.sharedApplication.backgroundTimeRemaining来看最大的后台运行时长,在不同的iOS系统上不太一样, 大部分是180秒。

注意:测试此功能不能用Xcode直接debug运行,因为在调试器链接到app的进行的情况下,app是不会在后台被挂起的。

2.是否能递归调用此方法来持续获得执行时间

在beginBackgroundTaskWithExpirationHandler里最后再递归调用[self startTask];

经尝试此方法无效,180秒超时后再次申请,会立刻回调超时的block,并且backgroundTimeRemaining时间一直都是0。

并且由于一直不停的在递归创建和终止后台任务,当Expiration真正到来的时候,一个还有一个创建的任务没有关闭。从而导致违背begin和end成对调用的原则,app被系统强制kill。所以此方法不但不能延长执行时间,还会导致app在180秒后台执行时间到达后,被系统kill的情况。

3.beginBackgroundTaskWithExpirationHandler多次被调用的情况

didEnterBackground每次调用都会触发beginBackgroundTaskWithExpirationHandler来创建新的后台任务,并用backgroundUpdateTask保存任务id,但如果第一次的任务还没有endBackgroundTask之前,应用回到前台,然后再次进入后台,就会重新创建一个新的后台任务,并且backgroundUpdateTask之前保存的id会被覆盖,这就违背了beginBackgroundTaskWithExpirationHandler与endBackgroundTask成对调用的原因。

因为前一个后台任务超时的block回调的时候,其实是end了后一个taskId对应的后台任务,并且把taskId赋值为UIBackgroundTaskInvalid。而后一个后台任务超时的block回调的时候,taskId已经变成了null,对其进行end调用已经无效了,所以相当于没有成对调用begin和end,导致的结果就是:后一个后台任务超时的时候,app被系统强制kill。

所以每一次创建的后台任务都要有一个独立的变量来维护其taskId,如果只有一个后台任务,但是有重入的可能,那么应该在willEnterForeground回调中,把前一个后台任务进行endBackgroundTask操作,这样就不存在taskId被覆盖的问题了。或者是每次didEnterBackground的时候,检查taskId == UIBackgroundTaskInvalid,若不满足该条件,说明taskId已经引用了一个正在进行的后台任务,还没有完成,由于这个后台任务重进前台又切换回后台的情况下,backgroundTimeRemaining会被重置为180秒,所以在这种情况下,也可以直接return

if(backgroundUpdateTask != UIBackgroundTaskInvalid){

    return;

}

4、检查其他三方SDK是否存在问题

此时可以保证我们自己的写法是没有问题的了, 但是很多三方SDK也做了保活措施, SDK大多是通过系统通知来监听的, 需要检查下SDK的调用是否成对出现, 那么就需要使用runtime来hook系统的方法, 一共有3个方法需要hook, 2个开启, 一个结束

  • - beginBackgroundTaskWithExpirationHandler:
  • - beginBackgroundTaskWithName:expirationHandler:
  • -endBackgroundTask:

在hook之后, 可以拿一个字典来记录, 使用任务ID作为Key, 开启任务保存此任务ID到数组中, 结束任务从字典中移除此ID, 在app的各种场景下进前台,退后台, 查看开启和结束是否成对, 找出没有成对的地方, 打印堆栈信息, 找出没有成对的SDK, 升级SDK或者替换掉.

- (UIBackgroundTaskIdentifier)gcs_beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler {
    UIBackgroundTaskIdentifier identifier = [self gcs_beginBackgroundTaskWithExpirationHandler:handler];
    NSLog(@"%s, 开启任务ID: %@", __func__, @(identifier));
    NSString *key = [self taskKeyWithTaskIdentifier:identifier];
    leftBackgroundTaskDic[key] = [self taskInfo];
    return identifier;
}

- (UIBackgroundTaskIdentifier)gcs_beginBackgroundTaskWithName:(nullable NSString *)taskName expirationHandler:(void(^ __nullable)(void))handler {
    UIBackgroundTaskIdentifier identifier = [self gcs_beginBackgroundTaskWithName:taskName expirationHandler:handler];
    NSLog(@"%s, 开启任务名称:%@, ID: %@", __func__,taskName, @(identifier));
    NSString *key = [self taskKeyWithTaskIdentifier:identifier];
    leftBackgroundTaskDic[key] = [self taskInfo];
    return identifier;
}

- (void)gcs_endBackgroundTask:(UIBackgroundTaskIdentifier)identifier {
    NSLog(@"%s, 结束任务ID: %@", __func__, @(identifier));
    [self gcs_endBackgroundTask:identifier];
    NSString *key = [self taskKeyWithTaskIdentifier:identifier];
    leftBackgroundTaskDic[key] = nil;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
iOS上的AVAudioPlayer提供了一种方便的方式来进行音频播放,包括后台播放。在后台播放音频时,我们需要遵循特定的设置和步骤。 首先,我们需要在应用程序的Capabilities选项卡中启用后台模式。在Xcode中找到应用程序的Targets,然后点击Capabilities选项卡,将“Background Modes”开关打开,并勾选“Audio, AirPlay, and Picture in Picture”。 然后,在代码中设置AVAudioSession的类别为AVAudioSessionCategoryPlayback。这可以通过以下代码实现: ``` import AVFoundation let audioSession = AVAudioSession.sharedInstance() do { try audioSession.setCategory(.playback, options: .defaultToSpeaker) try audioSession.setActive(true) } catch { print("设置音频会话类别失败: \(error)") } ``` 接下来,我们需要在应用程序的AppDelegate类中创建一个后台任务。当我们按下Home键离开应用程序时,这个后台任务将会被激活。 ``` func applicationDidEnterBackground(_ application: UIApplication) { backgroundTask = application.beginBackgroundTask(withName: "PlayAudioInBackground", expirationHandler: { application.endBackgroundTask(self.backgroundTask) self.backgroundTask = UIBackgroundTaskIdentifier.invalid }) } ``` 然后,创建一个AVAudioPlayer实例来播放音频文件。我们可以使用它的`play`方法来开始播放音频。 ``` let audioURL = Bundle.main.url(forResource: "audio", withExtension: "mp3") do { audioPlayer = try AVAudioPlayer(contentsOf: audioURL!) audioPlayer.prepareToPlay() audioPlayer.play() } catch { print("无法播放音频文件: \(error)") } ``` 在音频播放完成或者我们不再需要后台任务时,需要结束后台任务。 ``` func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { if flag { // 当音频播放完成时调用 audioPlayer.stop() UIApplication.shared.endBackgroundTask(backgroundTask) backgroundTask = UIBackgroundTaskIdentifier.invalid } } ``` 通过以上设置,我们可以确保AVAudioPlayer在应用程序进入后台时继续播放音频。但需要记住,长时间背景播放可能会影响设备的电池寿命,所以请确保仅在必要时使用后台播放功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值