[iOS]关于iOS后台长时间挂起的方法


补充一下:首先表述一下我个人的观点,对于后台开启audio开关的问题,不是苹果允许,尽可能避免使用一些取巧的方式,最好能按照官方规定。如果很有必要,首先你得说服苹果的审核人员。个人血淋淋的教训,有一个项目就是需要在后台长时间播放语音,不是音乐类或导航类那种,在每次收到推送,在后台播放推送内容TTS合成的语音,内容基本上不会相同,所以放弃使用固定语音文件方式。出了一两个版本都神不知鬼不觉的过审,以为这样会高枕无忧,没想到到最后还是被发现,然后苹果打电话,很显然,我没有说过他,可能我的理由没准备好吧。又试了几次其他方式,虽然也有过了的情况,但考虑到应用的正规性和后续维护成本,只好换成了其他计划。好吧,就这样,下面讲一些后台长时间运行的方法,仅供参考。


苹果限制App在后台运行,是为了减少系统对当前硬件的开销,可以更高效的使用App,驻足后台,肯定会占用系统资源。为了让设备尽量省电,减少不必要的开销,保持系统流畅,因而对后台机制采用墓碑式的“假后台”。除了系统官方极少数程序可以真后台,一般开发者开发出来的应用程序后台受到以下限制:

  • 1.用户按Home之后,App转入后台进行运行,此时拥有180s后台时间(iOS7)或者600s(iOS6)运行时间可以处理后台操作

  • 2.当180S或者600S时间过去后,从 iOS 4 开始,我们可以告知系统有未完成任务,需要申请继续完成,系统批准申请之后,可以继续运行,但总时间还是不会超过10分钟,下面是申请后台的方法。

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

    __block UIBackgroundTaskIdentifier background_task;
    //注册一个后台任务,告诉系统我们需要向系统借一些事件
    _bgTaskId = [application beginBackgroundTaskWithExpirationHandler:^ {
        //不管有没有完成,结束background_task任务
        [application endBackgroundTask: background_task];
        background_task = UIBackgroundTaskInvalid;
    }];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"Running in the background\n");
        while(true){
             sleep(1000);
              //NSLog(@"Background time Remaining: %f",[[UIApplication sharedApplication] backgroundTimeRemaining]);
             if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
                 NSLog(@"---------  App处于前台  ---------");
                 //我们自己完成任务后,结束background_task任务
                 [application endBackgroundTask: background_task];
                 background_task = UIBackgroundTaskInvalid;
                 return;
            }
        }

    });
}

  • 3.当10分钟时间到之后,无论怎么向系统申请继续后台,系统都会强制挂起你的App,挂起所有后台操作、线程,直到用户再次点击App之后才会继续运行。

当然iOS为了特殊应用也保留了一些可以实现“真后台”的方法,摘取比较常用的如:

  • 1.VoIP . 后台语音服务,类似Skype通话应用需要调用,可进行后台的语音通话
  • 2.定位服务 . 下面会讲到
  • 3.后台下载 . 如Newsstand,iOS5后多了一种类型,报刊杂志后台自动下载更新,其能够自动实时更新,iOS 7 则可以下载各种玩意和定时抓取
  • 4.在后台一直播放无声音乐 . 这是后台的音频,这个很早之前便有,也是iOS设备中用得最多的后台应用,调用这个接口可以实现后台的音乐播放,审核也不容易被发现。但是容易受到电话或者其他程序影响, 仅凭自己取舍。

其中VoIP需要绑定一个Socket链接并申明给系统,系统将会在后台接管这个连接,
一旦远端数据过来,你的App将会被唤醒10s(或者更少)的时间来处理数据,超过时间或者处理完毕,程序继续休眠。VOIP对服务端改动太大,选择需符合项目需求,下面介绍两种方式供参考

<h3>一、后台处理多媒体时间</h3>


  • 首先需要在Capabilities -Background Modes打开选择Audio或在plist中配置Required background modes -App plays audio or streams audio/video using AirPlay
  • 开启后台处理多媒体session
  • 设置后台任务
  • [ 重要] 有很多例子采用播放无声音乐的方式,但愿没有被发现。 要给水果粑粑说明你需要后台播放的原因,比如说你有后台播放的实体等,如若被拒,电话讨论时,你要能够说服他
-(void)applicationWillResignActive:(UIApplication* )application{
    
    //开启后台处理多媒体事件
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    AVAudioSession *session=[AVAudioSession sharedInstance];
    [session setActive:YES error:nil];
    //后台播放
    [session setCategory:AVAudioSessionCategoryPlayback error:nil];
    //这样做,可以在按home键进入后台后 ,播放一段时间,600s吧。但是不能持续播放网络歌曲,若需要持续播放网络歌曲,还需要申请后台任务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;
}


<h3>二、后台定位服务</h3>

  • 首先在info.plist (Privacy - Location Always Usage Description)中加上描述申明其必要性,如:GPS在后台持续运行,会降低电池的寿命
  • [重要] 备注里跟苹果解释清楚用后台定位的地方和原因,最好说明App里有有关需求,如地图轨迹定位等

要启动定位服务:

  • 1.需要引入头文件:#import <CoreLocation/CoreLocation.h>
  • 2.在AppDelegate.m中定义CLLocationManager * locationManager;作为全局变量方便控制
  • 3.在程序启动初期对定位服务进行初始化:
locationManager  = [[CLLocationManager alloc] init];
locationManager.delegate = self;
  • 4.在程序转入后台的时候,启动定位服务
    [locationManager startUpdatingLocation];(第一次运行这个方法的时候,如果之前用户没有使用过App,则会弹出是否允许位置服务,关于用户是否允许,后面代码中有判断)
    这样在定位服务可用的时候,程序会不断刷新后台时间,实际测试,发现后台180s时间不断被刷新,达到长久后台的目的。

但是这样使用也有一些问题,在部分机器上面,定位服务即使打开也可能不能刷新后台时间,需要完全结束程序再运行。稳定性不知道是因为代码原因还是系统某些机制原因。
代码:
判断用户是否打开了定位服务,是否禁用了该程序的定位权限:

if(![CLLocationManager
 locationServicesEnabled] || ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied))//判断定位服务是否打开
    {
        [InterfaceFuncation
 ShowAlertWithMessage:@"错误" AlertMessage:@"定位服务未打开\n保持在线需要后台定位服务\n请到
 设置-隐私 中打开定位服务" ButtonTitle:@"我错了"];
        return;
    }

AppDelegate.m源码:

@property (assign,
nonatomic)
 UIBackgroundTaskIdentifier bgTask;
 
@property (strong,
nonatomic)
 dispatch_block_t expirationHandler;
@property (assign,
nonatomic)
BOOL jobExpired;
@property (assign,
nonatomic)
BOOL background;
- (BOOL)application:(UIApplication
 *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
 
 UIApplication*
 app = [UIApplication sharedApplication];
 
 __weak
NSUAAAIOSAppDelegate*
 selfRef = self;
 
 self.expirationHandler
 = ^{  //创建后台自唤醒,当180s时间结束的时候系统会调用这里面的方法
 [app
 endBackgroundTask:selfRef.bgTask];
 selfRef.bgTask
 = UIBackgroundTaskInvalid;
 selfRef.bgTask
 = [app beginBackgroundTaskWithExpirationHandler:selfRef.expirationHandler];
 NSLog(@"Expired");
 selfRef.jobExpired
 = YES;
 while(selfRef.jobExpired)
 {
 //
 spin while we wait for the task to actually end.
 NSLog(@"等待180s循环进程的结束");
 [NSThread sleepForTimeInterval:1];
 }
 //
 Restart the background task so we can run forever.
 [selfRef
 startBackgroundTask];
 };
 
 //
 Assume that we're in background at first since we get no notification from device that we're in background when
 //
 app launches immediately into background (i.e. when powering on the device or when the app is killed and restarted)
 [self monitorBatteryStateInBackground];
 locationManager
 = [[CLLocationManager alloc] init];
 locationManager.delegate
 = self;
 //[locationManager
 startUpdatingLocation];
 return YES;
}
 
- (void)monitorBatteryStateInBackground
{
 self.background
 = YES;
 [self startBackgroundTask];
}
 
- (void)applicationDidBecomeActive:(UIApplication
 *)application
{
 //
 Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
 NSLog(@"App
 is active");
 [UIApplication
 sharedApplication].applicationIconBadgeNumber=0;//取消应用程序通知脚标
 [locationManager
 stopUpdatingLocation];
 self.background
 = NO;
}
 
- (void)applicationDidEnterBackground:(UIApplication
 *)application
{
 //
 Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
 //
 If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
 //if([self
 bgTask])
 if(isLogined)//当登陆状态才启动后台操作
 {
 self.bgTask
 = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:self.expirationHandler];
 NSLog(@"Entered
 background");
 [self monitorBatteryStateInBackground];
 }
}
 
-  (void)locationManager:(CLLocationManager
 *)manager didFailWithError:(NSError *)error//当定位服务不可用出错时,系统会自动调用该函数
{
 NSLog(@"定位服务出错");
 if([error
 code]==kCLErrorDenied)//通过error的code来判断错误类型
 {
 //Access
 denied by user
 NSLog(@"定位服务未打开");
 [InterfaceFuncation
 ShowAlertWithMessage:@"错误" AlertMessage:@"未开启定位服务\n客户端保持后台功能需要调用系统的位置服务\n请到设置中打开位置服务" ButtonTitle:@"好"];
 }
}
 
-  (void)locationManager:(CLLocationManager
 *)manager didUpdateLocations:(NSArray *)locations//当用户位置改变时,系统会自动调用,这里必须写一点儿代码,否则后台时间刷新不管用
{
 NSLog(@"位置改变,必须做点儿事情才能刷新后台时间");
 CLLocation
 *loc = [locations lastObject];
 //NSTimeInterval
 backgroundTimeRemaining = [[UIApplication sharedApplication] backgroundTimeRemaining];
 //NSLog(@"Background
 Time Remaining = %.02f Seconds",backgroundTimeRemaining);
 //
 Lat/Lon
 float latitudeMe
 = loc.coordinate.latitude;
 float longitudeMe
 = loc.coordinate.longitude;
}
 
- (void)startBackgroundTask
{
 NSLog(@"Restarting
 task");
 if(isLogined)//当登陆状态才进入后台循环
 {
 //
 Start the long-running task.
    NSLog(@"登录状态后台进程开启");
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
 0), ^{
 //
 When the job expires it still keeps running since we never exited it. Thus have the expiration handler
 //
 set a flag that the job expired and use that to exit the while loop and end the task.
    NSInteger count=0;
    BOOL NoticeNoBackground=false;//只通知一次标志位
    BOOL FlushBackgroundTime=false;//只通知一次标志位
    locationManager.distanceFilter
 = kCLDistanceFilterNone;//任何运动均接受,任何运动将会触发定位更新
    locationManager.desiredAccuracy
 = kCLLocationAccuracyHundredMeters;//定位精度
    while(self.background
 && !self.jobExpired)
    {
       NSLog(@"进入后台进程循环");
       [NSThread sleepForTimeInterval:1];
       count++;
       if(count>60)//每60s进行一次开启定位,刷新后台时间
       {
          count=0;
          [locationManager
 startUpdatingLocation];
          NSLog(@"开始位置服务");
          [NSThread sleepForTimeInterval:1];
          [locationManager
 stopUpdatingLocation];
          NSLog(@"停止位置服务");
          FlushBackgroundTime=false;
       }
       if(!isLogined)//未登录或者掉线状态下关闭后台
       {
          NSLog(@"保持在线进程失效,退出后台进程");
          [InterfaceFuncation
 ShowLocalNotification:@"保持在线失效,登录已被注销,请重新登录"];
          [[UIApplication
 sharedApplication] endBackgroundTask:self.bgTask];
          return;//退出循环
       }
       NSTimeInterval backgroundTimeRemaining
 = [[UIApplication sharedApplication] backgroundTimeRemaining];
       NSLog(@"Background
 Time Remaining = %.02f Seconds",backgroundTimeRemaining);
       if(backgroundTimeRemaining<30&&NoticeNoBackground==false)
       {
          [InterfaceFuncation
 ShowLocalNotification:@"向系统申请长时间保持后台失败,请结束客户端重新登录"];
          NoticeNoBackground=true;
    }
    //测试后台时间刷新
       if(backgroundTimeRemaining>200&&FlushBackgroundTime==false)
       {
          [[NSNotificationCenter defaultCenter]
 postNotificationName:@"MessageUpdate" object:@"刷新后台时间成功\n"];
          FlushBackgroundTime=true;
          //[InterfaceFuncation
 ShowLocalNotification:@"刷新后台时间成功"];
       }
    }
    self.jobExpired
 = NO;
    });
 }
}

最后,忠诚的祝愿大家都能过审~


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容