iOS后台任务的分类及实现

前言

当用户将运行App切换到后台时,系统会更改其运行状态。对大多数的应用
后台状态就是切换为挂起状态(suspended)。大多数的应用可以轻易的切换为挂起状态(suspended),但是有些应用因为自身需求的原因会在后台保持运行。一个关于远足的App也许会一直追踪用户的位置,从而可以在地图上展示运动的轨迹。一个音频App也许需要在锁屏后继续播放音乐。其他的一些应用希望在后台下载内容,这样可以缩减给用户展示的时间。当前你需要让你的App保持后台运行,iOS系统会给你有效的帮助,从而不会过度消耗系统资源和电量。这项技术的实现分为以下三种情况:

  • 在前台开启了短任务的应用在进入后台时可以请求一段后台运行时间去完成任务
  • 在前台开始下载任务的应用可以下载任务的控制移交给系统,从而在后台任务运行期间可以控制应用的挂起或终止。
  • 对于需要在后台长时间运行的特殊类型任务的应用可以声明一种或多种后台运行的模式

当然除非后台任务可以整体提升用户的体验性,否则尽量避免运行后台任务。应用进入后台也许是由于用户切换了另一个App或者锁屏了,所以临时不使用了。以上任一情况都表明了目前用户不需要维持后台任务了。如果依然维持后台任务将会消耗手机的电量,而且用户也会手动关闭应用。因此尽量考虑好是否需要后台任务,尽量避免后台任务运行。
       在iOS中,系统提供了一下多种后台任务的实现:

  • 短任务向系统借点时间 Background Task
  • 后台获取 Background Fetch
  • 后台下载 Background Download
  • 特定的后台任务,如音乐播放、后台持续定位、VoIP等
  • 静默推送,推送唤醒

Background Task

在应用切换到后台之后,很有可能被系统杀掉,因此切换到后台的时候需要保存重要数据。UIApplication的这个方法能让系统借给App一段时间,通过测试这个时间大致为3分钟左右,执行重要任务。

beginBackgroundTaskWithName:expirationHandler

系统到底给了app多少时间呢,可以通过UIApplication的backgroundTimeRemaining属性来查看,经测试大致是在3分钟左右。如果超时,expirationHandler参数的block将会被调用。需要注意的是,在任务结束的时候需要通过endBackgroundTask来告诉系统,同时将taskId设置为UIBackgroundTaskInvalid,时间有借有还哦。否则就会超时而杀死进程。如下所示:

    NSLog(@"可持续后台运行时间:%f",application.backgroundTimeRemaining);
    self.taskId = [application beginBackgroundTaskWithExpirationHandler:^{
        [self.timer invalidate];
        self.timer = nil;
        [[UIApplication sharedApplication] endBackgroundTask:self.taskId];
        self.taskId = UIBackgroundTaskInvalid;
    }];

如果不在Background的状态下调用beginBackgroundTaskWithName函数呢?会正常执行。

Background Fetch

Background Fetch可以让App没有启动或者在后台的时候,周期性的获取数据。一些内容类型的App,比如新闻,小说等,可以通过使用Background Fetch技术让用户更快的获取信息。一个最合适的场景描述是:一个新闻App,通过Background Fetch技术在夜里获取了用户关注的最新内容并保存到本地,早上用户在地铁等信号不好的地方打开App,可以直接查看本地保存的最新新闻。接下来看看如何使用Background Fetch功能。
第一步:使用Background Fetch功能需要申请Capability:

iOS后台任务的分类及实现_第1张图片
设置Capability.png

第二步:在App启动的时候设置请求周期

[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

第三部:实现AppDelegate的application:performFetchWithCompletionHandler:方法

-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler{
    
    // 请求数据
    completionHandler(UIBackgroundFetchResultNewData);
}

这个方法的执行时间不要超过30秒,实际上越短越好,如果这个方法执行时间过长,iOS系统就会降低Background Fetch的调用频率。
调试设置

iOS后台任务的分类及实现_第2张图片
调试设置.png

将launch due to background fetch event勾上,然后点击Run按钮启动应用。

注意,在用户主动杀死App的情况,Background Fetch不会工作。

https://stackoverflow.com/questions/35478726/background-fetch-is-not-working-after-killing-the-app

后台下载 Background Download

iOS后台下载可以通过创建backgroundSession的方式实现。如下:
第一步:创建后台会话


- (NSURLSession *)backgroundSession
{
/*
 Using disptach_once here ensures that multiple background sessions with the same identifier are not  created in this instance of the application. If you want to support multiple background sessions within a single process, you should create each session with its own identifier.
 */
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.example.apple-samplecode.SimpleBackgroundTransfer.BackgroundSession"];
        session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    });
    return session;
}

第二步:创建后台下载任务并启动

NSURL *downloadURL = [NSURL URLWithString:DownloadURLString];
    NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
    self.downloadTask = [self.session downloadTaskWithRequest:request];
    [self.downloadTask resume];

第三步:在回调的代理中处理下载进度、下载完成及下载失败

// 处理下载进度
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    
}

// 下载任务完成回调
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)downloadURL
{
    
}

// 下载失败回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    
}

// 处理下载任务由于网络原因失败后被系统恢复的回调
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
{
    
}

处理后台任务事件

当App不在运行时,因为后台下载任务是重启的一个进程处理,所以当任务完成或者请求证书验证时,系统会重新启动并进入后台处理后台会话时间。在这里,你应该保存completionHandler以便在处理完事件时告诉系统重新进入暂停状态。你还可以使用后台会话标识来重新创建后台会话,这个会话会关联之前的会话进程。


- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier
  completionHandler:(void (^)(void))completionHandler
{
    BLog();
    /*
     Store the completion handler. The completion handler is invoked by the view controller's checkForAllDownloadsHavingCompleted method (if all the download tasks have been completed).
     */
    self.backgroundSessionCompletionHandler = completionHandler;
}

/*
 If an application has received an -application:handleEventsForBackgroundURLSession:completionHandler: message, the session delegate will receive this message to indicate that all messages previously enqueued for this session have been delivered. At this time it is safe to invoke the previously stored completion handler, or to begin any internal updates that will result in invoking the completion handler.
 */
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    dispatch_async(dispatch_get_main_queue(), ^{
        APLAppDelegate *appDelegate = (APLAppDelegate *)[[UIApplication sharedApplication] delegate];
        if (appDelegate.backgroundSessionCompletionHandler) {
            void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
            appDelegate.backgroundSessionCompletionHandler = nil;
            completionHandler();
        }
        
        NSLog(@"All tasks are finished");
    });
}

**注意:completionHandler必须在主线程执行,如果你要更新UI界面,必须在它之前更新。

特定的后台任务

iOS可以在后台进行一些特定的任务,如音乐播放,后台持续定位、VoIP通话、蓝牙通信等。下面我们就以音乐播放来演示下如何实现后台音乐播放。
第一步:选中Targets-->Capabilities-->BackgroundModes-->ON,
并勾选Audio and AirPlay选项,如下图

iOS后台任务的分类及实现_第3张图片
BackgroundModes设置.png

第二步:在Appdelegate.m的applicationWillResignActive:方法中激活后台播放,代码如下:

-(void)applicationWillResignActive:(UIApplication *)application
{
  //开启后台处理多媒体事件
  [[UIApplication sharedApplication]   beginReceivingRemoteControlEvents];
AVAudioSession *session=[AVAudioSession sharedInstance];
  [session setActive:YES error:nil];
  //后台播放
  [session setCategory:AVAudioSessionCategoryPlayback error:nil];
  //这样做,可以在按home键进入后台后 ,播放一段时间,几分钟吧。但是不能持续播放网络歌曲,若需要持续播放网络歌曲,还需要申请后台任务id,具体做法是:
  _bgTaskId=[AppDelegate backgroundPlayerID:_bgTaskId];
  //其中的_bgTaskId是后台任务UIBackgroundTaskIdentifier _bgTaskId;
}

实现一下backgroundPlayerID:这个方法:
+(UIBackgroundTaskIdentifier)backgroundPlayerID:(UIBackgroundTaskIdentifier)backTaskId
{
  //设置并激活音频会话类别
  AVAudioSession *session=[AVAudioSession sharedInstance];
  [session setCategory:AVAudioSessionCategoryPlayback error:nil];
  [session setActive:YES error:nil];
  //允许应用程序接收远程控制
  [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
  //设置后台任务ID
  UIBackgroundTaskIdentifier   newTaskId=UIBackgroundTaskInvalid;
newTaskId=[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
  if(newTaskId!=UIBackgroundTaskInvalid&&backTaskId!=UIBackgroundTaskInvalid)
{
  [[UIApplication sharedApplication] endBackgroundTask:backTaskId];
}
  return newTaskId;
}

第三步:处理中断事件,如电话,微信语音等。
原理是,在音乐播放被中断时,暂停播放,在中断完成后,开始播放。具体做法是:

在通知中心注册一个事件中断的通知:
//处理中断事件的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterreption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];

实现接收到中断通知时的方法
//处理中断事件
-(void)handleInterreption:(NSNotification *)sender
{
  if(_played)
  {
     [self.playView.player pause];
     _played=NO;
  }
  else
  {
     [self.playView.player play];
     _played=YES;
  }
}

静默推送,Remote Notification

在iOS7以后推送消息的时候可以唤醒App,执行一段代码,也叫静默推送。静默推送和普通推送的流程有些差别:

iOS6推送示意图.png

iOS7推送示意图.png

普通推送,系统收到推送消息之后,应用程序并不做任何事情,等待用户操作。 静默推送,在系统收到推送之后,唤起App,App的didReceiveRemoteNotification方法被调用,在这个方法中可以发起一个网络请求,下载数据。
静默推送只需要在普通推送的payload中增加一个content-available的key:

{
"aps":
    { 
    "content-available":1,
    "alert":"This is some fancy message2.",
    "badge":6,
    "sound": "default"
    }
}

需要注意的是:

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    NSLog(@"iOS7及以上系统,收到通知:%@", userInfo);
    completionHandler(UIBackgroundFetchResultNewData);
}

静默推送唤起的任务最多执行30s的时间,completionHandler必须被调用,告诉系统任务执行完毕。
另外,在用户点击了推动消息启动了应用之后,didReceiveNotificationResponse这个方法会被调用,因此要注意数据的处理是否会和didReceiveRemoteNotification重复。

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{
    completionHandler(); // 系统要求执行这个方法
}

同样,如果用户主动杀死了App,静默推送也不会起作用。

其他

如果你一点也不想让你的应用在后台执行,你可以显示地在info.plist中添加key UIApplicationExitsOnSuspend(并且设置成YES)。When an app opts out, it cycles between the not-running, inactive, and active states and never enters the background or suspended states. 当用户按下home键退出应用时,应用的代理方法applicationWillTerminate:被调用,并且在应用终止前有大约5s的时间去清理工作。
       强烈不推荐选择退出后台执行,但是某些情况下也许偏向于使用这种选择。尤其是,如果编写后台执行代码会显著增加复杂性,结束应用也许是一种简便的方式。同样,如果你的app消耗了大量的内存并且很难释放,系统也许无论如何都会杀死你的应用以保证其他的应用正常运行。选择结束程序替代切换到后台也许会得到相同的结果,同时还能节省你的开发时间和精力。
       应用程序后台任务完成后如何通知用户?
可以使用LocalNotification技术。对于静默推送,本身已经通知了用户,不需要再次使用LocalNotification技术。对于IM类型的应用程序,使用静默推送和LocalNotification组合可以优化设计。具体可以参考这篇文章iOS静默推送介绍及使用场景
参考文章:
1、iOS应用后台任务的三两事
2、iOS App后台任务
3、iOS12下APP进入后台后再返回前台连接断开
4、IOS 后台传输
5、iOS后台持续播放音乐

你可能感兴趣的:(iOS后台任务的分类及实现)