第15周:Service 全功能 + 后台优化

学 Service 之前,我心里对它的定义一直是错的:"Service 就是 Android 给的后台线程,把耗时任务扔进去跑就行。"

直到写本周 Demo,我在 onStartCommand 里打印了当前线程名,屏幕上赫然显示 main------服务的所有回调都跑在主线程上。如果我真的把"耗时任务"直接塞进 Service,结果不是后台执行,而是整个 App 卡死,然后 ANR(Application Not Responding,应用无响应弹窗)。

这是我这周拆掉的第一个、也是最重要的一个误解。除此之外,Service 还有一整层被版本演进反复改写的规则:Android 8 限制后台服务、Android 12 限制后台启动前台服务、Android 14 强制服务类型------每一刀都砍在"老教程教你的写法"上。标题里的"优化"指的是三件事:不泄漏(内存)、不合规就崩(正确性)、被系统和厂商乱杀(可用性) 。


实践来源

来源 说明
Android Developers:Services overview Service 主线程模型的官方原文、三种形态、started/bound 生命周期、START_* 语义、混合销毁规则的核心依据
Android Developers:Restrictions on background starts(FGS) Android 12 后台启动前台服务限制、豁免清单、ForegroundServiceStartNotAllowedException 的依据
Android Developers:Foreground service types Android 14 类型强制三要素、类型清单、各类型运行时权限前提的依据

一、概念地基:Service 不是线程,也不是进程

官方文档对 Service 线程模型有一段加粗的警告(Caution),我把它放在最前面,因为它摧毁的误解最普遍:

"A service runs in the main thread of its hosting process; the service does not create its own thread and does not run in a separate process unless you specify otherwise."

翻译过来三句话:Service 跑在宿主进程的主线程上;它不创建自己的线程;它也不在独立进程里 (除非你在 Manifest 里用 android:process 显式指定)。

Demo 里的验证只需要一行日志:

kotlin 复制代码
// Week15StartedService.kt
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    Log.d(TAG, "回调线程:${Thread.currentThread().name}")   // 输出:main
    return startMode
}

运行后日志区显示"回调线程:main"。这就是证据:你在 onStartCommand 里写一个 Thread.sleep(10000),卡的不是"后台",是整个 App 的 UI。

所以 Service 的正确定位是:一个"没有界面的组件",系统给它的承诺是"即使用户离开了你的页面,我也尽量不立刻杀掉你" ------它买的是存活时间,不是线程。要真正的后台计算,你仍然要在服务里自己开线程(Thread、ExecutorService、协程都行)。

那什么时候才需要 Service?官方文档给了一个很狠的判断标准:如果这个工作只在用户和你的界面交互期间进行,你根本不需要 Service------在 Activity 里开个线程就够了。Service 的价值场景是"用户走了,活还得继续":音乐播放、文件下载、数据同步、定位记录。

这三个概念再收一遍口:

概念 它是什么 和 Service 的关系
进程(Process) 应用代码的运行容器 Service 默认和 Activity 同进程,主线程共享
线程(Thread) 代码执行流 Service 不自带线程,回调全在主线程
Service 无界面的存活型组件 买"存活性",不买"后台执行"

二、三种形态与生命周期:系统什么时候创建,什么时候销毁

Service 有三种用法形态,官方定义分得很清楚:

  • Started(启动型) :startService() 拉起,服务独立存活,直到自己 stopSelf() 或别人 stopService()
  • Bound(绑定型) :bindService() 绑定,提供客户端-服务器接口,所有客户端解绑后销毁
  • Foreground(前台型) :必须挂一条常驻通知的特殊服务,系统几乎不杀它(第五节展开)

三种形态共享四个核心回调,但走的路径完全不同:

kotlin 复制代码
class Week15StartedService : Service() {
​
    override fun onCreate() {
        // 首次创建时走一次;服务已存在时,再次 startService 不会重走
    }
​
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 只有 started 形态会走;每次 startService 都走一次,startId 递增
        return START_STICKY
    }
​
    override fun onBind(intent: Intent?): IBinder? {
        // 只有 bound 形态会走;必须实现,不支撑绑定就返回 null
        return null
    }
​
    override fun onDestroy() {
        // 服务收到的最后一个回调,清理资源的唯一机会
    }
}

Demo 里能直接观察到的关键事实有三个。

事实一:onCreate 只有一次,onStartCommand 每次都有。 连续点三次"startService",日志里 onCreate 出现一次,onStartCommand 出现三次,而且 startId 是 1、2、3 递增的。这个 startId 不是摆设------它是并发场景的保护锁:如果你的服务同时处理多个请求,某个请求处理完时应该用 stopSelf(startId) 停掉"那一单",而不是裸 stopSelf()。少了 startId 匹配,旧任务结束时会把刚到的新请求一起误杀。

事实二:started 服务没有"暂停"概念,也没有 onStop 回调。 它被停止时,系统直接销毁,你只会收到 onDestroy。所以所有资源清理(线程、Handler 回调、监听器、注册的广播)都必须放在 onDestroy 里,没有第二次机会。

事实三:bound 服务的生死完全由客户端数量决定。 多个组件可以同时绑定同一个服务,系统计数,最后一个客户端解绑时 服务才销毁。Demo 区域②的操作链是:bindService(日志出现 onCreate → onBind)→ 调用 increment()(通过 Binder 直接调服务方法)→ unbindService(日志出现 onUnbind → onDestroy)。

绑定的通信通道是 IBinder。同进程场景的标准写法是 LocalBinder:

kotlin 复制代码
// Week15BoundService.kt
inner class LocalBinder : Binder() {
    fun getService(): Week15BoundService = this@Week15BoundService
}
​
override fun onBind(intent: Intent?): IBinder {
    return LocalBinder()   // 客户端拿到后强转,直接拿到服务实例调方法
}

Activity 侧通过 ServiceConnection 接收它:

kotlin 复制代码
private val connection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        boundService = (service as Week15BoundService.LocalBinder).getService()
        // 从这里开始可以像调普通对象一样调服务的方法
    }
​
    override fun onServiceDisconnected(name: ComponentName?) {
        // 注意:主动 unbindService 不会回调这里,只有服务异常死亡才会
    }
}
​
bindService(intent, connection, Context.BIND_AUTO_CREATE)

BIND_AUTO_CREATE 的意思是"服务没在跑就先创建再绑定";少了它,绑定一个不存在的服务会静默失败。onServiceDisconnected 的触发条件是个高频误解点:主动解绑不走它,只有服务所在进程被杀才走------所以指望它做"解绑完成"的收尾逻辑是错的。

实践

  • 参考方向:音乐 App 的播放控制(Activity 绑定播放服务调 play()/pause()/seekTo())、下载管理器(页面绑定下载服务查进度)
  • 解决问题:页面和服务解耦------页面只持有 Binder 接口,服务不用知道页面存在;页面销毁重建(旋转)后重新绑定即可继续控制,业务连续性不受 UI 生命周期影响
  • 本节落点:Demo 区域② 的 LocalBinder 通信与 increment() 调用
  • 取舍判断:绑定模式写着比启动模式重(要维护 ServiceConnection 状态机),只调用一两次的轻任务用 started 更划算;需要双向通信、持续交互、进度查询的场景才是 bound 的主场

相关技术

  • API / 类:startService()/stopService()、bindService()/unbindService()、ServiceConnection、IBinder、LocalBinder、BIND_AUTO_CREATE、stopSelf(startId)
  • 系统机制:onCreate 单例性、startId 递增、绑定计数销毁、onServiceDisconnected 仅异常触发
  • 常见坑:把耗时操作写进主线程回调;用 stopSelf() 误杀并发请求;指望 onServiceDisconnected 做正常解绑收尾

三、onStartCommand 返回值:服务被系统杀死之后怎么办

onStartCommand 的返回值不是形式参数,它是你给系统的一份"遗嘱":如果服务被系统杀掉(内存不足回收),之后要不要重建、怎么重建?

三个取值,语义完全不同:

返回值 被杀后的行为 适用场景
START_NOT_STICKY 不重建。除非有新的 startService 请求,服务就死透了 任务型服务:活干完就完,丢了可以重来(如一次性同步)
START_STICKY 重建并再次调用 onStartCommand,但 intent 是 null 常驻型服务:活着本身就是意义,如媒体播放器待机
START_REDELIVER_INTENT 重建,并用最后的 intent 重新调用 续跑型服务:任务参数重要,丢了必须原样恢复,如文件下载

Demo 里用 RadioGroup 切换 START_STICKY 和 START_NOT_STICKY,返回值实时生效。

这里最该记住的是 START_STICKY 的 null intent 陷阱:重建后 onStartCommand(intent=null) 会被调用,如果你的代码不写判空直接 intent.getStringExtra(...),服务重建的瞬间就是一次 NPE 崩溃。所以 STICKY 服务的标准骨架是:

kotlin 复制代码
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    if (intent == null) {
        // 系统重建路径:恢复"活着"的状态,但不要当成新任务处理
        resumeStandby()
    } else {
        handleCommand(intent)
    }
    return START_STICKY
}

还有一个系统侧的决策规则要知道:服务被杀的概率和它的形态强相关。官方明确的排序是------绑定了前台 Activity 的服务最难被杀;前台服务几乎不被杀;长时间运行的 started 服务随着时间推移在后台列表里排位越来越低,最容易被杀。这就是为什么"想活得久"的终极手段不是 START_STICKY,而是前台服务。

大厂实践

  • 参考方向:音乐播放器(STICKY:被杀后重建回到待播状态)、下载器(REDELIVER:断点任务原样恢复)、埋点上报(NOT_STICKY:丢了下轮再报)
  • 解决问题:返回值选错会导致两类事故------NOT_STICKY 用于常驻服务导致"服务莫名其妙就没了";REDELIVER 用于一次性任务导致旧任务被重复执行(重复下载、重复扣费类事故的原型)
  • 本节落点:Demo 区域① 的 RadioGroup 切换 + startMode 返回
  • 取舍判断:START_REDELIVER_INTENT 的"重放"语义要求任务本身幂等(同一个 intent 执行两次不出错),任务不幂等就别用它,否则重建等于制造重复数据

相关技术

  • API / 类:START_STICKY、START_NOT_STICKY、START_REDELIVER_INTENT
  • 系统机制:低内存回收优先级(前台服务 < 绑定前台 Activity 的服务 < 常驻 started 服务)、重建时 intent 置空/重放
  • 常见坑:STICKY 重建后 null intent 未判空 NPE;对非幂等任务用 REDELIVER 导致重复执行

四、混合状态:销毁的双条件规则

真实业务里最常踩的生命周期事故,发生在"既 start 又 bind"的服务上。典型场景:音乐服务先 startService 让它独立于页面存活(页面关了音乐不停),再 bindService 让页面能控制播放。

这时的销毁规则是官方文档里一句容易被略过的话:

"stopService() or stopSelf() doesn't actually stop the service until all of the clients unbind."

翻译:stop 请求 + 全部解绑,两个条件必须同时满足,服务才会销毁。 只满足一个,服务继续活着。

Demo 区域③把这个规则做成了四按钮的顺序实验(start → bind → stop → unbind):

  1. start:日志出现 onCreate → onStartCommand
  2. bind:日志出现 onBind
  3. stop:日志出现......什么都没有。onDestroy 没有执行------虽然 stop 请求发了,但绑定还在,服务不死
  4. unbind:日志出现 onUnbind → onDestroy------此刻双条件凑齐,服务才真正销毁

这个实验反过来做也成立:先 stop 再 bind,或者先 unbind 再 stop,只要两个条件最后凑齐,onDestroy 就在凑齐的那一刻执行。

少了这个认知会出什么事?最经典的是"我以为我停了":业务代码在页面退出时调了 stopService,就放心地把服务当成死了,但另一个页面还绑着它------服务继续跑、继续占内存、继续执行旧逻辑,排查时你会发现"这个服务怎么杀都杀不掉"。

大厂实践

  • 参考方向:音乐/音频类 App 的播放服务(start 保活 + bind 控制是这类服务的标准架构)
  • 解决问题:页面退出时"停"服务,但全局还有其他入口(悬浮控制条、通知栏控制、另一个页面)持有绑定,服务不销毁是正确行为,不是 bug------产品语义是"只要还有控制方,音乐就不该停"
  • 本节落点:Demo 区域③ 的四按钮顺序实验 + 日志区 onDestroy 出现时机
  • 取舍判断:混合状态的服务,停止逻辑必须收口到一个地方统一管理(谁最后离开谁负责 stop + unbind),散落在各个页面的"顺手 stopService"是混合状态事故的主要来源

相关技术

  • API / 类:stopService()、unbindService()、onUnbind()
  • 系统机制:销毁双条件(停止请求 + 零绑定)、两条件凑齐时即时销毁
  • 常见坑:以为 stopService 必销毁;多个页面各自 stop/unbind 缺乏统一管理

五、前台服务与后台限制:一条八年的收紧时间线

前台服务(Foreground Service,FGS)是"系统承诺几乎不杀你"的服务形态,代价是必须挂一条用户可见、不可划掉的常驻通知。它是系统和应用之间的一份契约:你告诉用户"我在跑、我在耗资源",系统就让你活着。

但这份契约在过去八年里被持续收紧。这条时间线是理解所有合规要求的主线:

版本 限制 影响
Android 8.0(API 26,2017) 后台执行限制:应用退到后台后,普通后台服务几分钟内被停止;后台 startService 抛 IllegalStateException 后台服务基本被判死刑,官方引导去 JobScheduler/WorkManager
Android 9(API 28,2018) FGS 需要 FOREGROUND_SERVICE 权限 普通权限,Manifest 声明即可
Android 12(API 31,2021) 后台禁止启动 FGS :应用不在前台时 startForegroundService 抛 ForegroundServiceStartNotAllowedException "收到广播偷偷拉活"的玩法终结,只有白名单豁免(FCM 高优先级推送、用户点击通知/小组件、精确闹钟、BOOT_COMPLETED 等)
Android 13(API 33,2022) 通知变成运行时权限 POST_NOTIFICATIONS FGS 的通知没有权限就不显示(服务本身不崩)
Android 14(API 34,2023) FGS 类型强制 :Manifest 必须声明 foregroundServiceType + 对应的 FOREGROUND_SERVICE_<type> 权限;部分类型(camera/mic/location/health)还要求启动时已持有运行时权限 缺类型启动抛 MissingForegroundServiceTypeException;权限不满足抛 SecurityException
Android 15(API 35,2024) dataSync / mediaPlayback 等类型禁止从 BOOT_COMPLETED 广播启动 开机自启拉活进一步受限

这个项目的 targetSdk 是 34,所以 Android 14 的三要素全部生效。Demo 的合规链路:

xml 复制代码
<!-- Manifest:权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
​
<!-- Manifest:服务声明带类型 -->
<service
    android:name=".Week15ForegroundService"
    android:exported="false"
    android:foregroundServiceType="dataSync" />
scss 复制代码
// Week15ForegroundService.kt
override fun onCreate() {
    super.onCreate()
    createNotificationChannel()
​
    val notification = buildNotification(0)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
        // Android 14+:必须传入类型常量,且必须和 Manifest 声明的一致
        startForeground(
            NOTIFICATION_ID,
            notification,
            ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
        )
    } else {
        startForeground(NOTIFICATION_ID, notification)
    }
}

这段代码里藏着三个"少了就崩"的点:

第一,类型三要素缺一不可。 Manifest 声明 foregroundServiceType、声明 FOREGROUND_SERVICE_DATA_SYNC 权限、startForeground() 传入匹配的类型常量。少了 Manifest 类型声明,Android 14+ 上 startForeground 直接抛 MissingForegroundServiceTypeException。选 dataSync 的原因是它是少数没有运行时权限前提 的类型------camera / microphone / location / health 类型要求启动时已经持有对应运行时权限,而且是"使用期间"(while-in-use)权限:应用在后台时系统认为你不持有它们,创建服务那一刻就抛 SecurityException。

第二,5 秒规则。 用 startForegroundService() 启动后,服务必须在约 5 秒内调用 startForeground(),超时就是 ANR。所以 startForeground() 要放在 onCreate 靠前的位置,别在前面塞重初始化。

第三,exported="false"。 官方安全建议:不给其他应用启动你服务的机会。还有一条相关的硬规则------启动和绑定服务永远用显式 Intent ,Android 5.0 起隐式 Intent bindService() 直接抛异常。

Android 12 的后台启动限制还有一个演示不了的细节值得说:Demo 从前台页面启动 FGS 是合法路径;如果同样的调用发生在应用退到后台之后(比如在一个静态广播接收器里),系统直接抛 ForegroundServiceStartNotAllowedException。合法的"后台拉起"路径只剩白名单:FCM 高优先级消息、用户点了你的通知或桌面小组件、精确闹钟触发、BOOT_COMPLETED(Android 15 起部分类型也被禁)等。这些豁免的共同特征很明显------要么用户刚刚和你互动过,要么用户明确授权过。

大厂实践

  • 参考方向:音乐播放(mediaPlayback 类型)、跑步/骑行记录(location 类型)、微信电话本类通话(phoneCall 类型)
  • 解决问题:这些场景的共同点是"用户明确知道任务在进行"------通知不是合规负担,而是产品承诺的一部分;类型声明让系统能向用户展示"这个 App 正在用你的麦克风/位置",滥用 FGS 保活的应用在 Play 审核会被拒
  • 本节落点:Demo 区域④ 的 dataSync 前台服务 + 常驻计时通知 + 通知权限申请
  • 取舍判断:FGS 的代价是用户可见的常驻通知和电量消耗,任务如果"几分钟内能完成且不需要用户感知",正确答案是 WorkManager(延迟容忍)而不是 FGS;短任务还有 shortService 类型(约 3 分钟时限,超时 ANR)可选

相关技术清单

  • API / 类:startForegroundService()、startForeground()、foregroundServiceType、ServiceInfo.FOREGROUND_SERVICE_TYPE_*、ForegroundServiceStartNotAllowedException、MissingForegroundServiceTypeException
  • 系统机制:后台执行限制(8.0)、后台启动 FGS 限制(12)、通知运行时权限(13)、类型强制(14)、BOOT_COMPLETED 类型限制(15)
  • 权限:FOREGROUND_SERVICE、FOREGROUND_SERVICE_<type>、POST_NOTIFICATIONS
  • 常见坑:Manifest 漏声明类型;startForeground() 调用太晚触发 ANR;while-in-use 类型在后台创建服务;隐式 Intent 启动服务

六、服务泄漏规避:三条最典型的泄漏路径

服务是"存活型组件",它的寿命天然比页面长------这正是泄漏的温床。一个存不住内存的服务,等于把"后台优化"四个字反转成了"后台负担"。三条路径按出现频率排:

路径一:静态引用持有 Activity。 Demo 里三个服务的 logListener 都是 companion(静态)变量,Activity 把自己赋值进去的那一刻,静态域就持有了 Activity 引用。页面退出后如果不置空,整个 Activity(连带它的 View 树、图片、数据)都无法回收。Demo 的 onDestroy 里专门做了清理:

kotlin 复制代码
override fun onDestroy() {
    if (isBound) {
        boundService?.setCountListener(null)
        unbindService(connection)
    }
    if (isMixedBound) unbindService(mixedConnection)
    // 静态引用清零:断开 服务 → Activity 的引用链
    Week15StartedService.logListener = null
    Week15BoundService.logListener = null
    Week15ForegroundService.logListener = null
    super.onDestroy()
}

路径二:回调/监听器没反注册。 绑定服务里客户端注册的回调(Demo 里的 countListener)、服务里注册的广播接收器、传感器监听、位置监听------onDestroy 是清理它们的唯一机会(第二节说过,服务没有 onStop)。Demo 的 Week15BoundService.onDestroy 里 countListener = null,断的就是这条链。

路径三:绑定没解除。 Activity 退出时 isBound 还是 true,服务就一直持有 ServiceConnection,间接持有 Activity。所以"页面 onDestroy 里检查并 unbind"是绑定模式的标准收尾动作------注意 unbindService 一个没绑定过的连接会直接抛异常,所以要用 isBound 状态位保护。

路径四(附赠):Handler/线程没停。 服务 onDestroy 后,如果它内部的 Handler.postDelayed 循环或裸线程还在跑,Runnable 持有服务引用,服务对象死而不僵。Demo 三个服务的 onDestroy 第一行都是 handler.removeCallbacks(...)。

大厂实践

  • 参考方向:成熟团队的内存治理(LeakCanary 上报的服务相关泄漏链中,静态回调和未解绑占比最高)
  • 解决问题:服务泄漏的内存不是"多一点"的问题------Activity 整套 View 树被拖住,动辄几十 MB;且泄漏会随页面反复进出累积,最终把应用推向 OOM 和被系统优先杀死的位置
  • 本节落点:Demo Week15ServiceActivity.onDestroy 的完整清理链 + 三个服务各自的资源释放
  • 取舍判断:Demo 用静态 listener 是为了单 Activity 教学最简化;真实项目服务与页面的通信应该用生命周期感知的方案(LocalBroadcast 已废弃,现代替代是 SharedFlow/LiveData 加作用域,或干脆把状态收进 ViewModel),从源头消除静态引用

相关技术

  • API / 类:unbindService()、Handler.removeCallbacks()、弱引用(WeakReference)
  • 系统机制:静态域引用链、ServiceConnection 间接持页、onDestroy 是唯一清理点
  • 性能工具:LeakCanary、adb shell dumpsys meminfo
  • 常见坑:静态回调忘了清;unbind 未绑定的连接抛异常;以为服务 onDestroy 后内部线程自动停

七、厂商后台限制适配:系统规则之外的第二层战场

通过了系统的所有合规检查,服务在国产 ROM 上还有第二层关卡:厂商自己的省电策略。小米的"神隐模式"、华为的"应用启动管理"、OPPO/vivo 的电池优化白名单------它们没有公开 API,行为不透明,同一段合规代码在不同厂商机器上存活时间可能差一个数量级。

能做的标准动作有两层。

第一层:系统级电池优化白名单,有官方 API。

scss 复制代码
// 检测:应用是否在电池优化白名单里
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
val ignoring = pm.isIgnoringBatteryOptimizations(packageName)
​
// 引导:跳系统的"忽略电池优化"设置页(不需要权限)
startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))

isIgnoringBatteryOptimizations 返回 true 意味着系统 Doze 模式和应用待机桶不会限制你。注意这个 API 只覆盖系统层的限制,不覆盖厂商自启动管理。

第二层:厂商自启动管理,没有 API,只能引导用户手动开。

各厂商入口(方向性说明,路径随版本变化):

厂商 设置入口方向
小米 设置 → 应用设置 → 应用管理 → 你的应用 → 自启动管理
华为 设置 → 应用和服务 → 应用启动管理 → 关闭自动管理,手动允许自启/关联启动/后台活动
OPPO 设置 → 电池 → 应用耗电管理 → 允许完全后台行为
vivo 设置 → 电池 → 后台耗电管理 → 允许后台高耗电

诚实的边界是:这层适配做到"检测 + 引导"就是工程上限了。真正的架构级答案是减少对流程度存活的依赖------能延迟的任务给 WorkManager(它走系统的 JobScheduler 通道,厂商限制相对克制),必须即时的任务走 FCM 高优先级推送(Android 12 白名单里明确有它),前台服务只留给"用户明确感知"的任务。

大厂实践

  • 参考方向:IM/推送类应用(对后台存活最敏感的一类,普遍做法就是"系统白名单检测 + 厂商设置引导 + FCM/厂商推送通道兜底"三层)
  • 解决问题:客服工单里"收不到消息/任务中断"的相当大比例最终定位到厂商省电策略;与其对抗,不如把关键路径迁移到厂商自己也维护的通道(各厂商推送联盟通道在 ROM 层有保活特权)
  • 本节落点:Demo 区域⑤ 的白名单检测 + 设置页跳转
  • 取舍判断:把用户引导到设置页是有转化损耗的------每一步跳转都在流失用户;所以只对"确实需要长存活"的核心功能做引导,别把它当成所有页面的标配弹窗

相关技术清单

  • API / 类:PowerManager.isIgnoringBatteryOptimizations()、Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS、Build.MANUFACTURER
  • 系统机制:Doze 模式、应用待机桶、厂商自启动管理(无 API)
  • 替代通道:WorkManager / JobScheduler、FCM 高优先级消息、厂商推送通道
  • 常见坑:以为电池白名单包含厂商自启动(两层独立);引导文案不说明"为什么要开"导致用户拒绝

八、方案决策矩阵

第15周的内容收拢成一张"按任务特征选方案"的表:

任务特征 正确方案 理由
只在用户使用页面期间需要计算 Activity/ViewModel 里开线程或协程 不需要 Service,官方明确这么说
用户离开也要继续 + 用户明确感知(播放/导航/记录) 前台服务(匹配的类型) 系统承诺几乎不杀,通知是契约
一次性后台任务,丢了可重来 started 服务 + START_NOT_STICKY 最轻量,不追求重建
常驻待命,活着本身有意义 started 服务 + START_STICKY(null intent 判空) 被杀后重建回待机态
长任务需断点续跑,参数重要 started 服务 + START_REDELIVER_INTENT(任务幂等) 重建时重放 intent
页面需要持续查询/控制服务 bound 服务 + LocalBinder 双向通信的标准结构
可延迟、不需要即时的后台工作 WorkManager 走系统调度通道,后台限制影响最小(后续周展开)
后台需要被"唤醒"拉活 FCM 高优先级消息 Android 12 白名单豁免路径

技术清零表

技术 它是什么 Demo 落点 真实项目价值 常见坑
Service 主线程模型 服务回调跑宿主进程主线程 日志区显示"回调线程:main" 决定耗时操作必须自建线程 把耗时任务直接写进回调 → ANR
Started 服务 startService 拉起的独立存活服务 区域① start/stop 一次性后台任务 以为 stopService 后还有 onStop 回调
Bound 服务 bindService 绑定的接口型服务 区域② bind/increment/unbind 页面与服务双向通信 忘记 BIND_AUTO_CREATE 静默失败
LocalBinder 同进程绑定通信句柄 getService() 拿实例调方法 客户端直接调服务方法 跨进程场景用它(需 AIDL/Messenger)
ServiceConnection 绑定状态回调接口 connection / mixedConnection 感知绑定建立与服务异常死亡 指望 onServiceDisconnected 做正常解绑收尾
startId 每次 startService 的递增请求编号 日志显示 startId 递增 stopSelf(startId) 防并发误杀 裸 stopSelf 杀掉新请求
START_STICKY 被杀后重建、intent 置空 RadioGroup 切换 常驻待机服务 null intent 未判空 NPE
START_NOT_STICKY 被杀后不重建 RadioGroup 切换 可重来的任务型服务 常驻服务误用导致"服务消失"
START_REDELIVER_INTENT 被杀后重建并重放 intent 文章语义讲解 断点续跑型任务 非幂等任务被重复执行
混合销毁双条件 stop 请求 + 零绑定才销毁 区域③ 四按钮顺序实验 音乐类 start+bind 架构的正确停止 以为 stopService 必销毁
前台服务(FGS) 挂常驻通知的免杀服务 区域④ dataSync 计时服务 用户感知型长任务 把 FGS 当保活工具滥用(审核风险)
FGS 类型三要素 Manifest 类型 + 类型权限 + startForeground 传类型 Manifest 声明 + startForeground(..., TYPE_DATA_SYNC) targetSdk 34 合规底线 缺 Manifest 类型声明 → MissingForegroundServiceTypeException
5 秒规则 startForegroundService 后必须 5 秒内 startForeground onCreate 开头调用 防 ANR 在 startForeground 前做重初始化
后台启动 FGS 限制 Android 12 起后台禁启,白名单豁免 文章时间线讲解 理解"广播拉活"为何失效 在后台路径调用 startForegroundService → ForegroundServiceStartNotAllowedException
POST_NOTIFICATIONS Android 13+ 通知运行时权限 区域④ 启动前申请 FGS 通知可见的前提 以为 FGS 通知不受该权限影响
电池优化白名单 系统级省电豁免名单 区域⑤ 检测 + 跳设置页 长存活功能的系统层保障 以为它覆盖厂商自启动管理
服务泄漏四路径 静态引用/未反注册/未解绑/未停线程 onDestroy 清理链示范 内存治理的高频失分点 服务 onDestroy 后内部线程继续跑
相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui