iOS开发:通过定位服务保持后台长时间在线(iOS8+ Swift版)

发布于 2017-05-23  12762 次阅读


在之前写AAA客户端的时候,整理了iOS后台在线的方法,链接:iOS开发:保持程序在后台长时间运行

目前iOS也到iOS 10时代,开发语言也逐步过渡到Swift了。很多相关的一些方法已经过期。近期在写学校新核心的CC客户端iOS版的时候重写了通过定位服务长时间保持在线的方法。

项目背景:学校使用自研CC协议,学生通过登录CC客户端进行证书认证,认证通过后,服务端对该学生所在IP开启外网访问权限。在接下来的时间内,客户端需要每隔一定的时间主动向服务端发送保持在线的报告,如果服务端在一定的时间内接收不到客户端发来的保持在线请求,则认为客户端掉线。

最初参考了通用的播放空白音频文件、VOIP等通用方法。首先空白音频文件担心被其他应用程序打断会对用户造成不好的体验,其次VOIP和后台下载需要满足的条件比较多,实现难度较大。最后尝试使用最简单的定位服务来实现。

需要注意的是,要满足后台长时间在线,前提条件缺一不可:

1.XCode项目设置中打开后台中的Location服务。

2.用户安装后需要保持隐私设置-定位服务-该软件定位状态为始终。

如果还需要上架,则还需要满足:

1.在App描述中要加上苹果要求 长时间定位服务可能会减少电池寿命的相关描述,建议在弹出授权请求的时候也加上。

2.在审核描述中尽量描述清楚使用的场景和为什么使用。

题外话:最开始认为这类应用程序无论如何苹果都不会审核通过,准备用企业签名,无奈学校要走流程各种麻烦,就抱着试一试的心态提交审核。当然不出意外被拒绝,理由就是需要标明始终保持在线会耗费电池寿命,以及审查人员找不到为啥需要定位。然后我就列举了这个程序的功能,背景等(中文+英文),在审核失败的通知中回复。审查委员会回复说会进行重审,然后几分钟后就通过了。所以事实证明,只要合理的用途和标明相关规则,苹果还是允许这类应用程序使用的。

-------------------------------------------我是分割线----------------------------------------------------

下面是代码部分,所有代码均在AppDelegate.swift中:

引用CoreLocation,同时继承CLLocationManagerDelegate

我们需要定义几个变量在后面将会使用到
var locationManager : CLLocationManager?//定位服务
var isBackground:Bool = false;//标志是否在后台运行
var backgroundTask:UIBackgroundTaskIdentifier! = nil//后台任务标志

在didFinishLaunchingWithOptions中进行定位服务初始化。(强烈建议,后面后台服务需要使用)

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
		locationManager=CLLocationManager();
		locationManager?.delegate=self;
		locationManager?.allowsBackgroundLocationUpdates = true;
		locationManager?.requestAlwaysAuthorization();
		let setting = UIUserNotificationSettings.init(forTypes: UIUserNotificationType.Badge, categories: nil);
		UIApplication.sharedApplication().registerUserNotificationSettings(setting);
		
		NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(SetLocationService), name: "SetLocationService", object: nil);//这里我注册了一个本地通知服务,方便程序内其他代码能随时开关定位
		return true;
	}

SetLocationService的实现:

func SetLocationService(notification: NSNotification)
	{
		if(notification.object == nil)
		{
			return;
		}
		let setSwitch = notification.object as! Bool;
		if(setSwitch)
		{
			locationManager?.startUpdatingLocation();
		}
		else
		{
			locationManager?.stopUpdatingLocation();
		}
	}

这里先实现定位相关的方法。
locationManager中方法didChangeAuthorizationStatus用来接收用户选择的定位权限状态。这里我们可以通过检测用户是否选择“始终”。

func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
		switch (status) {
			case CLAuthorizationStatus.AuthorizedAlways:
				break;
			case CLAuthorizationStatus.AuthorizedWhenInUse:
				InterfaceFuncation.ShowAlertWithMessageSystemDefault(self.window?.rootViewController,alertTitle:"错误", alertMessage: "定位服务未正确设置\n客户端保持后台功能需要调用系统的位置服务\n请设置NSUCC定位服务权限为始终");
				break;
			case CLAuthorizationStatus.Denied:
				InterfaceFuncation.ShowAlertWithMessageSystemDefault(self.window?.rootViewController,alertTitle:"错误", alertMessage: "定位服务被禁止\n客户端保持后台功能需要调用系统的位置服务\n请到设置中打开位置服务");
				break;
			case CLAuthorizationStatus.NotDetermined:
				break;
			case CLAuthorizationStatus.Restricted:
				break;
			default:
				break;
	  }
	}

//定位失败或无权限将会回调这个方法
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
		if(Int32(error.code) == CLAuthorizationStatus.Denied.rawValue)
		{
			InterfaceFuncation.ShowAlertWithMessageSystemDefault(self.window?.rootViewController,alertTitle:"错误", alertMessage: "未开启定位服务\n客户端保持后台功能需要调用系统的位置服务\n请到设置中打开位置服务");
		}
	}
	

还需要实现定位didUpdateLocations(位置改变) didUpdateHeading(罗盘方向改变),如果不实现,将无法刷新后台在线时间。需要注意的是,不要留空方法,需要写几行代码,否则无法刷新后台时间(测试如此,感觉有点儿迷)

func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
		debugPrint("位置改变,做点儿事情来更新后台时间");
		let loc = locations.last;
		let latitudeMe = loc?.coordinate.latitude;
		let longitudeMe = loc?.coordinate.longitude;
		debugPrint("\(latitudeMe)");
	}
	
	func locationManager(manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
		NSLog("进入方位测定");
		//[NSThread sleepForTimeInterval:1];
		let oldRad =  -manager.heading!.trueHeading * M_PI / 180.0;
		let newRad =  -newHeading.trueHeading * M_PI / 180.0;
	}

至此定位相关的方法实现完毕
接下来开始实现进入后台的方法。实现:applicationDidEnterBackground

func applicationDidEnterBackground(application: UIApplication) {
		// 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(ISLogined)//这里为全局变量用于标志是否当前是否已经登录(是否在进入后台时启动保持在线)
		{
			if self.backgroundTask != nil {
				application.endBackgroundTask(self.backgroundTask)
				self.backgroundTask = UIBackgroundTaskInvalid
			}
			self.backgroundTask = application.beginBackgroundTaskWithExpirationHandler({
				() -> Void in
				//如果没有调用endBackgroundTask,时间耗尽时应用程序将被终止
				application.endBackgroundTask(self.backgroundTask)
				self.backgroundTask = UIBackgroundTaskInvalid
			})//这里是官方Demo的实现,用于当后台过期后的处理,实际中不会用到,但仍写出
			self.isBackground = true;
			BackgroundKeepTimeTask();
		}
	}

同时也别忘了,用户从后台回到前台要关掉定位服务和后台循环

func applicationDidBecomeActive(application: UIApplication) {
		// 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.
		UIApplication.sharedApplication().applicationIconBadgeNumber=0;//取消应用程序通知脚标
		if(ISLogined)
		{
			locationManager?.startUpdatingLocation();
		}
		else
		{
			locationManager?.stopUpdatingLocation();
		}
		self.isBackground = false;
	}

最后是核心的后台在线任务BackgroundKeepTimeTask的实现:

func BackgroundKeepTimeTask()
	{
		if(ISLogined)
		{
			debugPrint("进入后台进程");
			dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
				self.locationManager?.distanceFilter = kCLDistanceFilterNone;//任何运动均接受
				self.locationManager?.desiredAccuracy = kCLLocationAccuracyHundredMeters;//定位精度设置为最差(减少耗电)
				var theadCount = 0;//循环计数器,这里用作时间计数
				var isShowNotice = false;//是否显示掉线通知
				while(self.isBackground)
				{
					NSThread.sleepForTimeInterval(1);//休眠
					theadCount+=1;
					if(theadCount > 60)//每60秒启动一次定位刷新后台在线时间
					{
						debugPrint("开始位置服务");
						self.locationManager?.startUpdatingLocation();
						NSThread.sleepForTimeInterval(1);//定位休眠1秒
						debugPrint("停止位置服务");
						self.locationManager?.stopUpdatingLocation();
						theadCount=0;
					}
					let timeRemaining = UIApplication.sharedApplication().backgroundTimeRemaining;
					NSLog("Background Time Remaining = %.02f Seconds",timeRemaining);//显示系统允许程序后台在线时间,如果保持后台成功,这里后台时间会被刷新为180s
					if(!ISLogined)//未登录或者掉线状态下关闭后台
					{
						debugPrint("保持在线进程失效,退出后台进程");
						dispatch_async(dispatch_get_main_queue(), {
							var userInfo:[NSObject : AnyObject] = [NSObject : AnyObject]()
							userInfo["messageType"] = "showMessage"
							userInfo["messageBody"] = "登录已被注销,请重新登录"
							userInfo["messageTitle"] = "系统提示"
							InterfaceFuncation.ShowLocalNotification("登录已被注销,请重新登录",userInfo: userInfo);
						});
						return;//退出循环
					}
					if(timeRemaining < 60 && !isShowNotice)
					{
						var userInfo:[NSObject : AnyObject] = [NSObject : AnyObject]()
						userInfo["messageType"] = "showMessage"
						userInfo["messageBody"] = "后台保持在线失败,请检查定位设置并重新运行客户端"
						userInfo["messageTitle"] = "后台在线错误"
						InterfaceFuncation.ShowLocalNotification("后台保持在线失败,请检查定位设置并重新运行客户端",userInfo:userInfo);
						isShowNotice=true;
					}
				}
			});
		}

完整代码如下:

//
//  AppDelegate.swift
//  NSUCCIOS
//
//  Created by NivalXer on 2016/10/24.
//  Copyright © 2016年 NivalXer. All rights reserved.
//

import UIKit
import CoreLocation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate,CLLocationManagerDelegate {
	
	var locationManager : CLLocationManager?
	var isBackground:Bool = false;//是否在后台
	var backgroundTask:UIBackgroundTaskIdentifier! = nil
	
	func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {        
		locationManager=CLLocationManager();
		locationManager?.delegate=self;
		locationManager?.allowsBackgroundLocationUpdates = true;
		locationManager?.requestAlwaysAuthorization();
		let setting = UIUserNotificationSettings.init(forTypes: UIUserNotificationType.Badge, categories: nil);
		UIApplication.sharedApplication().registerUserNotificationSettings(setting);
		
		NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(SetLocationService), name: "SetLocationService", object: nil);
		return true;
	}
    	
	func SetLocationService(notification: NSNotification)
	{
		if(notification.object == nil)
		{
			return;
		}
		let setSwitch = notification.object as! Bool;
		if(setSwitch)
		{
			locationManager?.startUpdatingLocation();
		}
		else
		{
			locationManager?.stopUpdatingLocation();
		}
	}
	
	func applicationWillResignActive(application: UIApplication) {
		// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
		// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
	}
	
	func applicationDidEnterBackground(application: UIApplication) {
		// 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(ISLogined)
		{
			//self.backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler(self.expirationHandler);
			if self.backgroundTask != nil {
				application.endBackgroundTask(self.backgroundTask)
				self.backgroundTask = UIBackgroundTaskInvalid
			}
			self.backgroundTask = application.beginBackgroundTaskWithExpirationHandler({
				() -> Void in
				//如果没有调用endBackgroundTask,时间耗尽时应用程序将被终止
				application.endBackgroundTask(self.backgroundTask)
				self.backgroundTask = UIBackgroundTaskInvalid
			})
			self.isBackground = true;
			BackgroundKeepTimeTask();
		}
	}
		
	func applicationWillEnterForeground(application: UIApplication) {
		// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
	}
	
	func applicationDidBecomeActive(application: UIApplication) {
		// 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.
		UIApplication.sharedApplication().applicationIconBadgeNumber=0;//取消应用程序通知脚标
		if(ISLogined)
		{
			locationManager?.startUpdatingLocation();
		}
		else
		{
			locationManager?.stopUpdatingLocation();
		}
		self.isBackground = false;
	}
	
	func applicationWillTerminate(application: UIApplication) {
		// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
	}
	
	func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
		switch (status) {
			case CLAuthorizationStatus.AuthorizedAlways:
				break;
			case CLAuthorizationStatus.AuthorizedWhenInUse:
				InterfaceFuncation.ShowAlertWithMessageSystemDefault(self.window?.rootViewController,alertTitle:"错误", alertMessage: "定位服务未正确设置\n客户端保持后台功能需要调用系统的位置服务\n请设置NSUCC定位服务权限为始终");
				break;
			case CLAuthorizationStatus.Denied:
				InterfaceFuncation.ShowAlertWithMessageSystemDefault(self.window?.rootViewController,alertTitle:"错误", alertMessage: "定位服务被禁止\n客户端保持后台功能需要调用系统的位置服务\n请到设置中打开位置服务");
				break;
			case CLAuthorizationStatus.NotDetermined:
				break;
			case CLAuthorizationStatus.Restricted:
				break;
			default:
				break;
	  }
	}
	
	func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
		debugPrint("位置改变,做点儿事情来更新后台时间");
		let loc = locations.last;
		let latitudeMe = loc?.coordinate.latitude;
		let longitudeMe = loc?.coordinate.longitude;
		debugPrint("\(latitudeMe)");
	}
	
	func locationManager(manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
		NSLog("进入方位测定");
		//[NSThread sleepForTimeInterval:1];
		let oldRad =  -manager.heading!.trueHeading * M_PI / 180.0;
		let newRad =  -newHeading.trueHeading * M_PI / 180.0;
	}
	
	func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
		if(Int32(error.code) == CLAuthorizationStatus.Denied.rawValue)
		{
			InterfaceFuncation.ShowAlertWithMessageSystemDefault(self.window?.rootViewController,alertTitle:"错误", alertMessage: "未开启定位服务\n客户端保持后台功能需要调用系统的位置服务\n请到设置中打开位置服务");
		}
	}
	
	func BackgroundKeepTimeTask()
	{
		if(ISLogined)
		{
			debugPrint("进入后台进程");
			dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
				self.locationManager?.distanceFilter = kCLDistanceFilterNone;//任何运动均接受
				self.locationManager?.desiredAccuracy = kCLLocationAccuracyHundredMeters;//定位精度
				var theadCount = 0;//计数器
				var isShowNotice = false;//是否显示掉线通知
				while(self.isBackground)
				{
					NSThread.sleepForTimeInterval(1);//休眠
					theadCount+=1;
					if(theadCount > 60)
					{
						debugPrint("开始位置服务");
						self.locationManager?.startUpdatingLocation();
						NSThread.sleepForTimeInterval(1);//定位休眠1秒
						debugPrint("停止位置服务");
						self.locationManager?.stopUpdatingLocation();
						theadCount=0;
					}
					let timeRemaining = UIApplication.sharedApplication().backgroundTimeRemaining;
					NSLog("Background Time Remaining = %.02f Seconds",timeRemaining);
					if(!ISLogined)//未登录或者掉线状态下关闭后台
					{
						debugPrint("保持在线进程失效,退出后台进程");
						dispatch_async(dispatch_get_main_queue(), {
							var userInfo:[NSObject : AnyObject] = [NSObject : AnyObject]()
							userInfo["messageType"] = "showMessage"
							userInfo["messageBody"] = "登录已被注销,请重新登录"
							userInfo["messageTitle"] = "系统提示"
							InterfaceFuncation.ShowLocalNotification("登录已被注销,请重新登录",userInfo: userInfo);
						});
						return;//退出循环
					}
					if(timeRemaining < 60 && !isShowNotice)
					{
						var userInfo:[NSObject : AnyObject] = [NSObject : AnyObject]()
						userInfo["messageType"] = "showMessage"
						userInfo["messageBody"] = "后台保持在线失败,请检查定位设置并重新运行客户端"
						userInfo["messageTitle"] = "后台在线错误"
						InterfaceFuncation.ShowLocalNotification("后台保持在线失败,请检查定位设置并重新运行客户端",userInfo:userInfo);
						isShowNotice=true;
					}
				}
			});
		}
	}
}

届ける言葉を今は育ててる
最后更新于 2017-05-23