ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

鸿蒙后台任务:ServiceExtensionAbility短时任务与长时任务抉择

鸿蒙后台任务:ServiceExtensionAbility短时任务与长时任务抉择 ServiceExtensionAbility 的短时任务和长时任务我一开始也没搞清楚还以为只是“短任务”和“长任务”的字面区别。直到有一次我写了个数据上传功能用户切到后台任务直接被系统回收上传失败。后来查文档、翻源码、反复测试才发现这俩的启动方式、生命周期、权限要求、甚至被系统杀掉的概率完全不是一回事。这篇文章我就把这段踩坑经历整理出来把短时任务和长时任务的选择逻辑彻底讲明白。1. 后台任务分类与选型逻辑先搞清楚“后台”在鸿蒙里意味着什么1.1 鸿蒙的后台限制到底卡的是什么很多人刚接触鸿蒙开发时会觉得“后台任务不就是开个线程吗”。其实不是。鸿蒙对后台的限制本质上是系统在帮你管资源、省电、保流畅。应用退到后台以后CPU、网络、定位这些资源都会被限制甚至进程都可能被系统回收。所以当你需要在后台执行一段代码时不能直接开个线程丢在那里不管。系统有专门的托管机制叫 ServiceExtensionAbility。它是鸿蒙 ExtensionAbility 的一种专门用来承载后台任务。但 ServiceExtensionAbility 本身又分两种运行模式一种是短任务模式任务做完自动结束另一种是长任务模式需要显式指定启动类型任务可以一直在后台跑。这里我踩过的第一个坑就是我以为 ServiceExtensionAbility 启动之后就能一直跑。实际上默认情况下它是一个短任务任务执行完必须自己调用 terminateSelf()否则系统会视其为异常并进行回收。1.2 短时任务与长时任务的核心差异短时任务和长时任务的差异不只是“运行时间长短”而是它们的生命周期、启动方式、权限要求、以及系统对待它们的策略完全不同。我整理了一张对比表对比项短时任务长时任务启动方式默认行为直接 startAbility 拉起必须用 startAbilityWithOptions 指定启动类型生命周期与宿主 UIAbility 生命周期相关联独立生命周期不随宿主页面销毁结束机制必须自行调用 terminateSelf()由系统管理任务完成后自行结束权限申请无需额外后台权限需要申请 KEEP_BACKGROUND_RUNNING 等权限通知要求无强制通知必须常驻通知栏让用户感知典型场景短时间数据同步、临时计算播放音乐、后台导航、文件传输被回收风险高宿主进程被杀则任务终止低系统会尽量保障其存活从这个表可以看出一条选型主线如果你的任务能在短时间内完成并且允许被系统打断那么短时任务就够用如果任务需要长时间持续运行、且用户必须能感知到它的存在那就必须走长时任务。2. 短时任务适用场景与实现要点2.1 什么时候选短时任务短时任务的典型特征是“短平快”。比如用户点击了一个导出按钮你需要在后台把数据整理一下、生成一个文件、再弹个通知。整个过程可能也就几秒到几十秒这种场景用短时任务非常合适。再比如登录后拉取用户信息、上传一条日志、同步一次设置项这些都属于短时任务。它的最大优势是简单、不需要额外权限、不需要常驻通知栏。用户根本感知不到后台有一个服务在悄悄工作。但短时任务有个致命短板它依赖宿主 UIAbility 的进程。如果你的页面退到了后台宿主进程还活着任务可以继续跑但如果宿主进程被系统回收短时任务也会跟着消失。所以短时任务适合那种“可被中断”的场景中断了也影响不大重试即可。2.2 短时任务的实现步骤与代码模板短时任务的实现分两步第一步在 module.json5 里注册 ServiceExtensionAbility第二步在代码里继承 ServiceExtensionAbility 并实现生命周期方法。先看 module.json5 注册代码{ extensionAbilities: [ { name: ShortTaskService, srcEntry: ./ets/serviceload/ShortTaskService.ets, type: service, description: 短时任务示例服务, exported: true } ] }srcEntry 指向实际的实现文件type 固定为 service。这里有个容易忽略的点exported 字段。如果这个服务只给应用内部使用exported 不写或设为 false 就可以了没必要对外开放。然后在实现文件中核心是重写 onCommand 方法import { ServiceExtensionAbility, Want } from kit.AbilityKit; export default class ShortTaskService extends ServiceExtensionAbility { onCreate(want: Want) { console.info([ShortTaskService] onCreate); } onCommand(want: Want, startId: number) { console.info([ShortTaskService] onCommand startId: startId); // 执行耗时任务 this.executeTask().then(() { // 任务执行完毕主动终止服务 this.terminateSelf(); }); } executeTask(): Promisevoid { return new Promise((resolve) { // 模拟耗时操作比如网络请求、数据库读写 setTimeout(() { console.info([ShortTaskService] task finished); resolve(); }, 3000); }); } onDestroy() { console.info([ShortTaskService] onDestroy); } }然后在页面里启动这个服务import { common, Want } from kit.AbilityKit; let context getContext(this) as common.UIAbilityContext; let want: Want { bundleName: com.example.myapp, abilityName: ShortTaskService }; context.startAbility(want);注意一个关键细节短时任务启动后服务不会像 UIAbility 那样一直在后台挂机。你的 onCommand 执行完之后必须调用 terminateSelf()。如果忘了调用系统会等一段时间后认定这个服务异常强制回收。2.3 短时任务的两个典型问题第一个问题是“任务还没做完就被杀了”。这种情况通常发生在宿主进程被系统回收时。因为短时任务和宿主 UIAbility 在同一个进程里宿主进程没了服务也跟着没了。我的处理方案是短时任务只做“允许失败、可以重试”的操作上传失败就记录一条状态下次打开 App 再补传。第二个问题是“terminateSelf() 调用时机不对”。一开始我在 executeTask 里面还没执行完就调了 terminateSelf()结果任务被腰斩。后来养成一个习惯所有异步任务都包成 Promise在 then 回调里统一调 terminateSelf()避免过早结束。注意短时任务类似于“小程序里的定时器”它可以在后台短暂运行但系统随时可能把你叫停。设计业务的时候务必假设任务可能被中断做好重试或断点续传逻辑不要假设一次就能做完。3. 长时任务适用场景与实现要点3.1 什么时候选长时任务长时任务的典型场景是音乐播放、后台导航、实时通话、大文件传输、设备互联。这些任务的共同点是持续时间长、且用户对任务的持续运行有明确的感知需求。比如用户在听歌切到后台音乐不应该停用户开着导航锁屏了导航也应该继续播报。这时短时任务就完全不够用了。长时任务要求系统“高优先级对待”不能因为宿主页面退到后台就被回收。要实现这个效果必须做两件事一是调用 startAbilityWithOptions以长时任务模式启动服务二是申请 KEEP_BACKGROUND_RUNNING 权限并在服务运行期间保持一条常驻通知。3.2 长时任务的类型与启动方式鸿蒙的长时任务有多种类型分别对应不同的业务场景。在长期运行的代码中启动服务时不仅要指定启动类型还需要明确任务类型。我整理了一个常用对照表任务类型枚举值业务场景典型用途dataTransfer数据传输上传/下载大文件audioPlayback音频播放音乐、有声书、播客location定位导航实时定位、语音导航bluetooth蓝牙相关蓝牙耳机连接、数据传输multiDeviceConnection多设备互联分布式设备组网voip音视频通话网络电话、会议启动长时任务的代码和短时任务完全不同必须用 startAbilityWithOptionsimport { common, AbilityStartType, Want } from kit.AbilityKit; let context getContext(this) as common.UIAbilityContext; let want: Want { bundleName: com.example.myapp, abilityName: LongTaskService }; let options { abilityStartType: AbilityStartType.ABILITY_START_TYPE_ATTACHED }; context.startAbilityWithOptions(want, options);ABILITY_START_TYPE_ATTACHED 代表跟随模式即服务跟随启动它的应用进程此外还有 RESIDENT常驻模式和 INVISIBLE隐藏模式。对大多数应用而言ATTACHED 就够了RESIDENT 通常留给系统应用或特殊场景。3.3 长时任务生命周期管理与权限配置配置权限这一步很多人会漏。长时任务必须声明 KEEP_BACKGROUND_RUNNING 权限否则启动时会失败。在 module.json5 里添加{ requestPermissions: [ { name: ohos.permission.KEEP_BACKGROUND_RUNNING, reason: $string:keep_background_running_reason, usedScene: { abilities: [LongTaskService] } } ] }reason 字段是向用户解释为什么需要这个权限的文案必须写清楚否则审核阶段会被打回。我一般写“用于在后台持续播放音频”或“用于在后台保持文件传输”严格匹配实际场景。长时任务运行期间必须发布一条常驻通知。我一开始没做通知任务直接就被系统干掉了。后来才发现这个通知其实是长时任务的“存续凭证”系统靠它告知用户“这个应用正在后台运行”。通知代码用 notificationManagerimport { notificationManager } from kit.NotificationKit; let notificationRequest: notificationManager.NotificationRequest { id: 1, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: 文件传输中, text: 正在上传 2/10 个文件 } } }; notificationManager.publish(notificationRequest).catch((err) { console.error(publish notification failed: JSON.stringify(err)); });任务结束后记得取消通知并终止服务notificationManager.cancel(1); this.terminateSelf();长时任务的结束时机也要把握好。比如文件传完了、音乐暂停了就应该立刻调 terminateSelf()。如果任务结束了还不终止服务长时间挂机反而会引发系统资源回收甚至被判为“异常占用后台”。4. 选型决策从需求倒推方案不纠结4.1 决策清单一个需求到底该用哪种任务我总结了一套判断方法每次拿到后台任务需求就按这个顺序过一遍第一任务能否在很短时间通常是几十秒内完成能选短时任务。不能进入下一步。第二任务是否需要用户在后台持续感知比如用户是否在听、在看、在通话、在等文件传完需要选长时任务。不需要再考虑任务能否容忍被打断。第三如果任务是“系统想杀就杀也没关系”那短时任务加个重试机制就够了。但如果是“必须完成”比如付费后的文件下载、录音取证就必须长时任务保证存活。第四看是否需要额外权限。长时任务必须申请权限并且权限审核比较严格如果不是必须尽量用短时任务。有些应用明明只是同步一下购物车却申请了后台运行权限这种属于过度申请审核也会被拒。4.2 一个真实项目的选型过程我做过一个工具类应用其中有个功能是用户把本地录音上传到云端。最初我图省事直接用了短时任务。实际测试发现录一段十分钟的音频上传需要一分多钟用户切到后台过一会儿短时任务就被系统回收了上传中断。后来我改成断点续传加短时任务重试效果也还是不够好因为用户一旦切出去时间稍长进程就被回收。最后改成 dataTransfer 类型的长时任务配上常驻通知任务是稳稳跑完的。用户也能通过通知栏看到上传进度体验反而更好了。这个案例给我的启发是不能仅仅从“任务时长”来判断还要看“任务失败代价”。上传音频失败用户可能以为传成功了这是不可接受的。宁可多申请权限也不能让任务半途而废。4.3 几种容易混淆的场景怎么处理有些场景容易被“任务时长”误导。比如播放本地 MP3 音乐任务时长可能只有三分钟。但音乐播放属于 audioPlayback 场景必须用长时任务。为什么因为用户希望在后台继续听歌而且系统对音乐播放有专门的保活机制。再比如定位打卡理论上定位一次只要几秒。但如果用户打开了“轨迹记录”功能需要持续定位三十分钟那短时任务就扛不住了必须使用 location 类型的长时任务。还有一类场景是短时任务和长时任务配合使用。比如先通过页面启动一个 show 类型的短任务把用户要的数据拉取下来然后再根据业务判断是否有长任务需求。两者不是互斥关系而是根据业务阶段按需切换。5. 常见问题与排查实录5.1 任务被系统回收的排查思路后台任务被杀是开发者最常遇到的问题。我的排查顺序是这样的先看是不是短时任务被宿主进程拖累了。如果宿主 UIAbility 完全退到后台且内存压力大系统会回收整个进程。针对这种情况要么换长时任务要么让任务做成“可重入”的比如记录进度下次启动接着传。再看长时任务有没有正确申请权限。如果你的应用没申请 KEEP_BACKGROUND_RUNNING即使以长时模式启动系统也不会给你保护。排查方法是启动服务后立刻查看日志有没有权限相关报错。还要检查你有没有发布常驻通知。长时任务在运行期间如果没有通知栏提醒系统会认为这个任务“不可信”随时可能回收。我在测试阶段就因为这个翻过车。注意长时任务的常驻通知不是“做一个通知”就完事它需要持续存在。某些系统版本上通知被用户手动划掉任务也会跟着终止。所以代码里要处理好“通知被移除”和“任务重启”的逻辑否则用户一划通知任务就断了。5.2 启动长时任务失败的常见报错启动长时任务时最常见的报错是“Error: errorCode: 401, reason: Parameter error”这种多半是 options 没传对。我遇到过把 abilityStartType 写错字母或漏传的情况排查时先检查枚举值是否正确。另一个报错是“Missing required permission”这个就是权限没配置好。检查 module.json5 里的 requestPermissions 是否包含 KEEP_BACKGROUND_RUNNINGreason 字段是否填写usedScene 里的 abilities 是否准确对应到你的服务。还有一个非常隐蔽的问题配置了长时任务类型和权限但服务代码里没有在 onStop 或任务结束时调用 notificationManager.cancel。此时任务已经结束但通知还在会造成“幽灵通知”。更严重的是如果你的服务代码抛了未捕获异常长时任务可能被自动终止但通知还在用户点开通知发现什么也没有。这种情况一定要在任务收尾时统一清理。5.3 测试阶段最容易踩的坑后台任务的测试一定要用真机模拟器对后台进程的回收策略和真机差异很大。我在模拟器上跑短时任务能跑五分钟到了真机上不到一分钟就被回收了差点以为是代码问题结果是测试环境不准。还有一点测试长时任务时要手动把应用切到后台锁屏等待系统触发资源回收再回到应用看任务是否还活着。如果只是开着应用干等是测不出长时任务真效果的。另外长时任务的适配和系统版本关系很大。同一段代码API 9 上运行正常升级到 API 12 之后有些接口行为发生了变化。我现在的习惯是除了在目标 API 上开发还会拿备份机测试低版本环境。6. 实操建议从项目第一天就想清楚任务策略6.1 任务设计要前置不要等功能写完再补后台任务策略最好在需求评审阶段就定下来而不是等代码写完了才发现短时任务不满足需求再反过来改造。因为长时任务涉及权限申请、通知设计、任务类型报备这些都不是随便能改的。尤其权限申请涉及应用市场审核改起来周期很长。我在做新页面时会先问自己三个问题这个功能会不会退到后台继续运行运行失败用户能不能接受用户是否需要通过通知感知进度三个问题一过方案基本就定下来了。6.2 通知栏文案和状态管理要做细既然长时任务必须常驻通知那这个通知就不是“做出来就行”的无脑操作。我在项目里把这些东西做了统一封装任务类型、任务进度、任务暂停/取消操作、以及异常状态提示。通知的作用不仅仅是让系统保活更是用户和任务交互的唯一窗口。如果通知上能加“暂停”和“取消”按钮体验会好很多。6.3 别忘了任务的终止和重启衔接长时任务在正常情况下的结束不应该依赖 terminateSelf 硬性调用有时候业务主动停止有时候是系统资源回收后由系统侧终止。主动终止和被动终止在代码里要分别处理。被动终止时任务进度要落盘下次启动要能恢复。这个衔接逻辑写好了用户基本感知不到任务曾经断过。我自己写任务时会用一个全局的任务状态对象来维护当前长时任务类型、当前阶段、已处理数量、总量。任何一次重启都能根据状态对象恢复现场。提示如果你的应用由多个页面都会启动长任务建议用一个单例管理器统一管理和向系统申请资源而不是每个页面各自为政。否则多个 LongTaskService 实例并存通知栏会出现多条通知系统也可能因为任务冲突而判定异常。根据我个人这大半年的实操经验ServiceExtensionAbility 短时任务和长时任务的选择本质上是一个权衡题一边是短时任务的轻量、免权限一边是长时任务的稳定性和用户可感知。不要把“时长”作为唯一标准要把“任务失败代价”和“用户感知需求”放在前面。宁可权限申请麻烦一点也别让用户感觉功能莫名其妙就断了。
返回列表