文档管理中心
我的

后台位置上报类型长时任务开发指导

问题现象

员工外出工作,需要每隔一定时间上报一次位置,应用处于后台运行的案例。

背景知识

  • 长时任务:应用退至后台后,在后台需要长时间运行用户可感知的任务,如导航等。为防止应用进程被挂起,导致对应功能异常,可以申请长时任务,使应用在后台长时间运行。
  • Location Kit(位置服务):位置子系统使用多种定位技术提供服务,通过这些定位技术,无论用户设备在室内或是户外,都可以准确地确定设备位置。通过on('locationChange')开启位置变化监听,可实时获取当前位置信息。

解决方案

  1. 申请ohos.permission.KEEP_BACKGROUND_RUNNING、ohos.permission.LOCATION、ohos.permission.APPROXIMATELY_LOCATION权限,配置方式请参见声明权限。
  2. 声明后台模式类型:

    在module.json5配置文件中abilities下的backgroundModes字段里,为需要使用长时任务的UIAbility声明相应的长时任务类型,配置文件中填写长时任务类型的配置项。

    收起
    自动换行
    深色代码主题
    复制
    1. "backgroundModes": [
    2. "location"
    3. ],
  3. 定位能力先申请定位权限,然后在定位功能启动时可同步启动后台定位长时任务,在取消定位时同时取消长时任务,完整代码如下:
    收起
    自动换行
    深色代码主题
    复制
    1. import { geoLocationManager } from '@kit.LocationKit';
    2. import { abilityAccessCtrl, common, PermissionRequestResult, Permissions, WantAgent, wantAgent } from '@kit.AbilityKit';
    3. import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
    4. @Entry
    5. @Component
    6. struct Index {
    7. private context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
    8. @State workingState: boolean = false;
    9. @State locationInfos: Array<LocationInfo> = [];
    10. // Start solution1
    11. aboutToAppear() {
    12. // 申请定位权限
    13. let permissions: Array<Permissions> = ['ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION'];
    14. this.reqPermissionsFromUser(permissions);
    15. }
    16. reqPermissionsFromUser(permissions: Array<Permissions>): void {
    17. let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    18. // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
    19. atManager.requestPermissionsFromUser(this.context, permissions).then((data: PermissionRequestResult) => {
    20. console.info(`requestPermissionsFromUser success, result: ${JSON.stringify(data)}`);
    21. }).catch((err: BusinessError) => {
    22. console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
    23. });
    24. }
    25. // End solution1
    26. build() {
    27. Column({ space: 16 }) {
    28. Row() {
    29. Column({ space: 20 }) {
    30. Row({ space: 15 }) {
    31. Image($r('sys.media.person_shield'))
    32. .width(30)
    33. .height(30)
    34. Text('张三')
    35. }
    36. Text('外出场所:大运广场')
    37. }
    38. .alignItems(HorizontalAlign.Start)
    39. .width('70%')
    40. Blank().width(2).backgroundColor(Color.Gray)
    41. Column() {
    42. Column() {
    43. if (this.workingState) {
    44. Text('结束')
    45. } else {
    46. Text('开始')
    47. }
    48. Text('工作')
    49. }
    50. .padding(5)
    51. .backgroundColor(this.workingState ? '#E84026' : '#64BB5C')
    52. .borderRadius($r('sys.float.corner_radius_level4'))
    53. .onClick(() => {
    54. if (this.workingState) {
    55. this.offLocationListen();
    56. this.stopContinuousTask();
    57. } else {
    58. this.getLocation();
    59. this.startContinuousTask();
    60. }
    61. });
    62. }
    63. .width('29%')
    64. }
    65. .width('100%')
    66. .padding(14)
    67. .borderRadius(8)
    68. .backgroundColor(Color.White)
    69. Column() {
    70. Row() {
    71. Text('位置信息列表')
    72. Text(`Total: ${this.locationInfos.length}`)
    73. }
    74. .width('100%')
    75. .justifyContent(FlexAlign.SpaceBetween)
    76. Scroll() {
    77. List({ space: 5 }) {
    78. ForEach(this.locationInfos, (item: LocationInfo) => {
    79. ListItem() {
    80. Column() {
    81. Text(`时间:${item.time}`)
    82. Text(`坐标:[${item.latitude}, ${item.longitude}]`)
    83. Text(`地址:${item.address}`)
    84. }
    85. .width('100%')
    86. .padding(5)
    87. .borderRadius(5)
    88. .alignItems(HorizontalAlign.Start)
    89. .backgroundColor('#0C0A59F7')
    90. }
    91. })
    92. }
    93. }
    94. .scrollBar(BarState.Off)
    95. .padding({ top: 5, bottom: 10 })
    96. }
    97. .width('100%')
    98. .height('100%')
    99. .padding(14)
    100. .borderRadius(8)
    101. .flexShrink(999)
    102. .backgroundColor(Color.White)
    103. }
    104. .height('100%')
    105. .width('100%')
    106. .padding(12)
    107. .backgroundColor('#f1f3f5')
    108. }
    109. locationChange = async (location: geoLocationManager.Location) => {
    110. if (location) {
    111. this.workingState = true;
    112. if (this.locationInfos.length >= 50) {
    113. this.locationInfos.pop();
    114. }
    115. let info = new LocationInfo();
    116. info.location = location;
    117. info.latitude = location?.latitude.toFixed(5);
    118. info.longitude = location?.longitude.toFixed(5);
    119. info.time = new Date(location?.timeStamp).toLocaleString('zh-CN', { hour12: false });
    120. let address = await this.getAddressFromLocation(location.latitude, location.longitude);
    121. info.address = address;
    122. this.locationInfos.push(info);
    123. }
    124. };
    125. /**
    126. * 每隔2秒上报一次位置
    127. */
    128. getLocation() {
    129. let requestInfo: geoLocationManager.LocationRequest = {
    130. 'priority': geoLocationManager.LocationRequestPriority.FIRST_FIX,
    131. 'scenario': geoLocationManager.LocationRequestScenario.UNSET,
    132. 'timeInterval': 2,
    133. 'distanceInterval': 0,
    134. 'maxAccuracy': 0
    135. };
    136. try {
    137. geoLocationManager.on('locationChange', requestInfo, this.locationChange);
    138. } catch (err) {
    139. console.error(`getLocation errCode: ${err?.code},errMessage: ${err?.message}`);
    140. }
    141. }
    142. /**
    143. * 坐标转化为地址
    144. */
    145. async getAddressFromLocation(lati: number, long: number): Promise<string> {
    146. let address: string | undefined = '';
    147. let reverseGeocodeRequest: geoLocationManager.ReverseGeoCodeRequest = {
    148. 'latitude': lati,
    149. 'longitude': long,
    150. 'maxItems': 1
    151. };
    152. let locationResult = await geoLocationManager.getAddressesFromLocation(reverseGeocodeRequest);
    153. console.log(`locationResult => ${JSON.stringify(locationResult)}`);
    154. if (locationResult && locationResult.length > 0) {
    155. address = locationResult[0].placeName;
    156. }
    157. return address ? address : '';
    158. }
    159. /**
    160. * 关闭位置上报
    161. */
    162. offLocationListen() {
    163. try {
    164. // 需要取消订阅的回调函数。该回调函数需要与on接口传入的回调函数保持一致。若无此参数,则取消当前类型的所有订阅。
    165. geoLocationManager.off('locationChange', this.locationChange);
    166. this.workingState = false;
    167. this.locationInfos = [];
    168. } catch (err) {
    169. console.error(`offLocationListen errCode: ${err?.code},errMessage: ${err?.message}`);
    170. }
    171. }
    172. startContinuousTask() {
    173. let wantAgentInfo: wantAgent.WantAgentInfo = {
    174. // 点击通知后,将要执行的动作列表
    175. // 添加需要被拉起应用的bundleName和abilityName
    176. wants: [
    177. {
    178. bundleName: this.context.abilityInfo.bundleName,
    179. abilityName: 'EntryAbility'
    180. }
    181. ],
    182. // 指定点击通知栏消息后的动作是拉起ability
    183. actionType: wantAgent.OperationType.START_ABILITY,
    184. // 使用者自定义的一个私有值
    185. requestCode: 0,
    186. // 点击通知后,动作执行属性
    187. actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    188. };
    189. // 通过wantAgent模块下getWantAgent方法获取WantAgent对象
    190. wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
    191. backgroundTaskManager.startBackgroundRunning(this.context,
    192. backgroundTaskManager.BackgroundMode.LOCATION, wantAgentObj).then(() => {
    193. console.info(`Succeeded in operating startBackgroundRunning.`);
    194. }).catch((err: BusinessError) => {
    195. console.error(`Failed to operation startBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
    196. });
    197. });
    198. }
    199. stopContinuousTask() {
    200. backgroundTaskManager.stopBackgroundRunning(this.context).then(() => {
    201. console.info(`Succeeded in operating stopBackgroundRunning.`);
    202. }).catch((err: BusinessError) => {
    203. console.error(`Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
    204. });
    205. }
    206. }
    207. class LocationInfo {
    208. public location: geoLocationManager.Location | undefined;
    209. public latitude: string = '';
    210. public longitude: string = '';
    211. public address: string = '';
    212. public time: string = '';
    213. }

常见FAQ

Q:应用置于后台一段时间后会被冻结,报FROZEN_BY_DEVICE_STILL_ENTER。

A:FROZEN_BY_DEVICE_STILL_ENTER表示应用基于定位的长时任务,在应用退至后台进入静止状态超过3分钟或者应用退后台前已进入静止超过30秒,系统会主动进行冻结。

Q:应用在后台长时间进行定位采集时,进程被反复kill并重启,日志显示kill reason=GnssWorkTimeAbnormal。

A:应用长时间进行定位采集但未申请后台持续定位长时任务时,系统会对进程进行管控。需要申请后台持续定位长时任务,具体操作请参见【解决方案】章节。