)
文档教程知识库【免费下载链接】android-tech-frontier【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目项目地址https://gitcode.com/gh_mirrors/an/android-tech-frontier点击查看免费下载本文整理自 android-tech-frontier 仓库 issue-41/Service十件你不知道的事.md以译文原稿为骨架结合仓库内 Service测试、欢迎来到Android多进程时代、Binder框架解析、一个内存泄漏引发的血案-Square 等文档进行纵深补充。读完你将掌握 Android Service 最容易踩坑的十个知识点它何时在主线程运行、IntentService 的本质、一次只能有一个实例的约束、系统杀进程的真实规则、启动与绑定的组合生命周期、以及如何安全地使用前台服务和跨进程通信。Service 是 Android 四大组件中最常被误解的一个。官方文档解释了它的全部细节却没能阻止开发者把它当成更好用的 AsyncTask来用。本文不做机制的全景复述而是聚焦那些被忽视、被误解、被遗忘的概念——它们是掌握 Service 组件绕不开的关卡。1. Service 不是更好用的 AsyncTask原文要点Service 并不是用于完成通用异步/后台操作的设计 Service 的目的在于即使当前没有 Activity 可见仍可以执行一些逻辑——不妨把它理解为不可见的 Activity。这是对 Service 最常见、也最致命的误解。AsyncTask 解决的是耗时操作不阻塞 UI 线程的问题而 Service 解决的是组件退出前台后逻辑依然继续的问题两者维度完全不同。需要记住每一个 Service 带来的开销不仅会增加 App 的负担还会增加整个 Android 系统的负担。Service 实例、其所在的进程、持有的 Binder 与 Binder 驱动资源参见仓库 Binder框架解析 中关于进程间通信机制的说明都是系统级成本。滥用 Service 等于让系统为你的后台常驻持续付出内存与电量代价这不是免费的。因此动手写Service子类之前先问自己任务真的需要脱离界面存活吗如果只是在界面存活期间做异步工作在Android中使用并发来提高速度和性能 一文所讨论的 AsyncTask、HandlerThread、线程池是更轻量的选择。2. Service 默认运行在主线程中即 App 进程中原文要点Service 默认运行在主线程App 进程的主线程中你可以让 Service 运行在独立进程中但应当避免这样做除非非做不可且清楚其全部开销。这与第 1 点互为因果既然 Service 不是异步工具系统自然没有义务为它开新线程。Service 的onCreate()、onStartCommand()、onBind()、onDestroy()全部跑在主线程UI 线程上如果在这里面做耗时操作一样会卡住 UI、触发 ANR。把 Service 放到独立进程看似能规避主线程阻塞实则是打开了潘多拉魔盒。仓库 欢迎来到Android多进程时代 详细剖析了多进程的利弊通过android:process属性可以让 Service 跑在独立进程中manifest ... application android:icondrawable/ic_launcher android:labelstring/app_name android:themestyle/Theme.Main activity android:name.MusicActivity / service android:name.MusicService android:process:music / /application /manifest该文档指出独立进程有独立的内存预算且系统在低内存时会优先杀死 UI 主进程而保留播放音乐的 Service 进程——这是音乐播放器等场景的真实收益。但代价同样沉重每个进程都有自己的 Dalvik/ART 虚拟机实例静态字段不再全局唯一App 状态无法传统意义上共享跨进程通信必须依赖 Intent、Messenger、AIDL 与 Binder 等手段。是否值得需要你基于自己的业务权衡而非为了隔离而隔离。3. IntentService 并没有什么黑科技原文要点IntentService 通过创建 HandlerThread 将任务置于队列中等待依次完成是在 Service 之外处理逻辑的一种技巧它是一个简单的类只有 164 行代码其中约 90 行还是注释。IntentService 常被当作Service 自带线程的黑科技其实它只是巧妙地复用了HandlerThreadHandlerLooper这套标准消息队列机制每次onStartCommand()收到的 Intent 被放入队列onHandleIntent()在后台线程中串行处理队列消费完后 Service 自动停止。这套机制的底层正是HandlerThread的for (;;) { Message msg queue.next(); ... }消息循环。有意思的是仓库 一个内存泄漏引发的血案-Square 记录了一个与 HandlerThread 消息循环直接相关的经典 BugDalvik VM 中消息循环内的本地变量Message msg在循环体结束后仍持有引用导致最后一个 Message 及其obj内容如 Dialog 的OnClickListener无法被回收进而泄漏整个 Activity。该文给出的临时修复是在 Handler 空闲时主动冲刷消息flushStackLocalLeaks其源码细节值得每个 IntentService 使用者阅读——理解 HandlerThread 的消息池与回收语义是真正掌握 IntentService 的前提。如果你需要并发处理多个任务IntentService串行队列的语义可能不够用此时应回到仓库 在Android中使用并发来提高速度和性能 讨论的多线程/HandlerThread 方案而不是强行扩展 IntentService。4. 一次只能有一个 Service 实例原文要点不论你创建多少 Service一次只能处理一个 Service 事务即使有其他应用/进程与其交互。同一个service声明在进程内只有一个实例多次startService()不会创建多个对象只会反复调用同一个实例的onStartCommand()多个客户端bindService()也只会触发一次onCreate()/onBind()大家共享同一个 Binder 连接见下文第 6 点与 Binder框架解析 中Binder Service/客户端-服务端映射的说明。这也意味着Service 内部天然是单线程单实例的心智模型若叠加主线程限制则更是如此。所有业务状态都应围绕一个实例设计不要幻想为每次请求 new 一个 Service。仓库 Service测试 也印证了这一点多次调用Context.startService()只有第一次触发onCreate()但每次都会触发onStartCommand()。5. 杀死 Service 很容易原文要点不要觉得内存压力是例外条件让 Service 优雅地处理系统引起的重启这是其生命周期很正常的一部分。Android 的进程回收是常态而非意外。系统在低内存时按进程优先级回收进程运行中的 Service 进程属于较高优先级但依然可能被回收。两个关键认知要能优雅应对进程被杀后系统重启 Service把onStartCommand()写成可重入、状态可恢复的实现是 Service 开发的基本功具体策略见第 7 点的START_REDELIVER_INTENT。前台 Service 是更难被杀而非永不被杀startForeground()可以把 Service 标记为前台组件、抬高进程优先级但只在不得不这样做时才设置该标记——前台通知、电量、用户观感都是有代价的。原文还特别强调一个容易被忽略的细节当代码运行在onCreate()、onStartCommand()或onDestroy()中时无论 Service 是不是前台组件它都会被视作前台组件——系统在生命周期回调执行期间会按前台标准对待该进程避免正在初始化就被杀的尴尬。反过来这也意味着这些回调不能拖太久否则就是在白白占用前台优先级。6. Service 能被启动、绑定或同时启动和绑定原文要点只要存在绑定关系显式地停止 Service 不会使其终止unbind 所有组件也不会终止它直到它被显式地停止如果它曾被启动过。此外不论调用多少次startService()一次stopService()或stopSelf()都会将其停止。Service 有两条独立的生命周期轨道启动态started由Context.startService()进入由Context.stopService()或Service.stopSelf()终止与调用方生命周期无关可长期后台运行。绑定态bound由Context.bindService()进入当最后一个绑定者unbindService()后终止前提是它从未被启动过适合跟随客户端存亡的 IPC 场景底层经由 Binder 建立连接参见 Binder框架解析。两者可以叠加一个 Service 可以既被startService()启动、又被多个客户端bindService()绑定。此时停止规则是AND逻辑——只有启动态被显式停止stopService()/stopSelf()且没有任何绑定关系时Service 才会真正进入onDestroy()。原文给出的生命周期图启动 → onStartCommand、绑定 → onBind/unbind、以及 onCreate/onDestroy 两个端点就是这两种模式的状态机先onCreate()随后按启动/绑定走向两条分支最终在停止且未绑定或全部解绑且未启动时汇合到onDestroy()。仓库 Service测试 给出了该模型的测试要点验证onCreate()对startService()/bindService()的响应验证onDestroy()对stopService()、unbindService()、stopSelf()、stopSelfResult()的响应并特别提醒startService()不可嵌套停止——即stopService()或stopSelf()非stopSelf(int)任一即可停止不需要配对计数。7. START_FLAG_REDELIVERY避免丢失输入数据原文要点如果在启动 Service 时传入数据在onStartCommand()中返回START_FLAG_REDELIVERY有助于避免输入数据丢失——如果 Service 在处理它的时候被杀死。onStartCommand(intent, flags, startId)的返回值直接决定了进程被杀后的重投策略这是最容易混淆的一组常量顺带澄清原文表述中容易与START_FLAG_REDELIVERY混淆的两个概念返回值/标志含义START_STICKYService 被系统杀死后系统会重建它并调用onStartCommand()但传入的intent为 null除非有新的 pending intentSTART_NOT_STICKY被系统杀死后不重建除非有新的显式启动请求START_REDELIVER_INTENT被系统杀死后系统会重新投递最后一次收到的 Intent确保输入数据不丢失START_FLAG_REDELIVERY这是flags参数中的标志位当onStartCommand()的调用正是由于系统重新投递上一个 Intent而发生即配合START_REDELIVER_INTENT的重投场景时flags中会带有该标志实战要点当启动时携带的数据如文件路径、URL、业务 ID是任务成败的关键且任务中途可能被系统回收时返回START_REDELIVER_INTENT并在onStartCommand()中通过flags检测START_FLAG_REDELIVERY以感知这是一次重投从而正确处理幂等与断点续做。这正是第 5 点让 Service 优雅处理系统重启的具体落点。8. 前台 Service 通知可能有一部分被隐藏原文要点前台 Service 必须显示持久的通知但可以给它PRIORITY_MIN优先级从而在状态栏隐藏它它在通知的阴影处仍可见。startForeground(id, notification)强制要求一条常驻通知——这是系统告知用户后台仍在运行的手段不能省。但通知的展示层级是可以调整的将通知的优先级设为PRIORITY_MIN可以让它不出现在状态栏顶部避免抢占用户注意力只在通知抽屉shade中可见。需要补充说明的是这条技巧的适用范围因系统版本而异。通知优先级与 Android 8.0 引入的通知渠道NotificationChannel体系叠加后PRIORITY_MIN的具体表现以目标设备与渠道配置为准但前台通知必须存在、可以弱化其存在感这一原则始终成立。只在你确实需要长时间后台运行如播放、下载、导航时才使用前台 Service并诚实地向用户展示那条通知。9. Service 能启动 Activity原文要点和所有非 Activity 的 Context 子类一样在 Service 中使用 Intent 启动 Activity 时必须为其添加FLAG_ACTIVITY_NEW_TASK标志。原因在于任务栈Task模型Service 不在任何 Activity 任务栈的上下文中直接startActivity()没有合法的归属任务系统会拒绝启动。加上FLAG_ACTIVITY_NEW_TASK即为该 Intent 提供一个新任务的上下文Intent intent new Intent(this, TargetActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent);这条规则同样适用于 Application、BroadcastReceiver 等所有非 Activity Context。实践提醒从后台 Service 跳转 UI 属于拉起前台的高打扰操作应谨慎使用并遵循系统的后台启动限制。10. 你可以而且应该使用单一职责原则原文要点不应将业务逻辑放在 Service 类内处理而应放在一个独立的类中这样只要需要你可以在任意类内处理业务逻辑好处多多。Service 的正确姿势是薄壳Service子类只负责生命周期与系统回调的适配真正的业务逻辑放在独立的类Plain 类、Manager、UseCase中。这样做的收益可测试性仓库 Service测试 展示了ServiceTestCase的用法——它提供 mock 的Context与ApplicationsetContext()/setApplication()在启动 Service 前注入模拟对象从而隔离系统依赖、对 Service 生命周期做精确断言。业务逻辑独立成类后普通 JUnit 即可覆盖不必依赖ServiceTestCase。可复用性同一套业务逻辑可同时被 Activity、BroadcastReceiver、其他 Service、甚至 JobScheduler 任务复用参考仓库 在Android 5.0中使用JobScheduler 对后台任务调度的替代讨论。可维护性Service 类保持薄生命周期回调里的代码量降到最低内存泄漏、进程残留等问题的排查面也随之缩小。总结把十件事串起来看Service 的正确心智模型是这样的它是一个主线程上的、单实例的、可与界面解耦的组件——不是异步工具不是线程池更不是后台常驻神器。它默认活在主线程第 1、2 点IntentService 只是借 HandlerThread 实现串行队列的薄封装第 3 点一次仅一个实例第 4 点它随时可能被系统杀死要按START_REDELIVER_INTENT/START_FLAG_REDELIVERY的策略优雅重生第 5、7 点启动与绑定两条生命周期可叠加停止需满足组合条件第 6 点前台通知是代价、也可弱化存在感第 8 点跨上下文的 UI 跳转要补FLAG_ACTIVITY_NEW_TASK第 9 点而业务逻辑应当独立成类、可测可复用第 10 点。想要继续深入仓库内还有三篇与本文直接互补的资料Service测试 讲透 Service 生命周期与ServiceTestCase的测试写法欢迎来到Android多进程时代 展开第 2 点的android:process多进程利弊Binder框架解析 揭示第 6 点绑定模式下跨进程通信的底层机制。把这几篇合在一起读你对 Service 的理解才算真正闭环。赞分享文档教程知识库【免费下载链接】android-tech-frontier【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目项目地址https://gitcode.com/gh_mirrors/an/android-tech-frontier点击查看免费下载相关推荐Android Service 组件详解前台服务、后台服务与绑定服务的完整实践指南Android Service 组件详解前台服务、后台服务与绑定服务的完整实践指南 Android 中的 Service 是四大应用组件之一用于在没有用户界文档教程知识库android-tech-frontier 译文精读Android Service 测试完整指南——ServiceTestCase、模拟对象与生命周期验证android tech frontier 译文精读Android Service 测试完整指南——ServiceTestCase、模拟对象与生命周期验证 本文档教程知识库开发第一个 Android 应用之前必须知道的六件事来自 android-tech-frontier 的内存泄漏与工程实践指南开发第一个 Android 应用之前必须知道的六件事来自 android tech frontier 的内存泄漏与工程实践指南 本文基于 android te文档教程知识库上一篇终极指南如何完全掌控Windows Defender - defender-control开源项目深度解析下一篇告别 TypeScript any 类型用 unknown、泛型与类型守卫重构类型安全Front-End-Checklist no-explicit-any 规则实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考