前言
当我们的App正在与用户频繁交互时,需要处理某个耗时任务,而任务的结果需要立即反馈。这时因为主线程用于处理UI和用户交互逻辑,如果有太多的耗时的逻辑在主线程中执行,就会阻塞主线程,引发ANR异常,导致APK卡顿甚至崩溃。因此需要一个后台线程来处理耗时任务。
当我们的App并未与用户频繁交互,但是,App本身需要周期性的从服务器同步数据或者获取数据(常见的心跳连接、收发消息长连接)。这时需要一个后台进程来处理,因为仅仅是后台线程,不能保证长久存活,也不能保证任务能执行完毕。
关于进程保活的文章有很多了,推荐几篇不错的文章:
腾讯——张兴华 原文链接找不到了,这是别人转发
关于 Android 进程保活,你所需要知道的一切
但是,官方说明自己看:
Note: You should only use a foreground service for tasks the user expects the system to execute immediately or without interruption. Such cases include uploading a photo to social media, or playing music even while the music-player app is not in the foreground. You should not start a foreground service simply to prevent the system from determining that your app is idle.
本文的目的在于参考官方文档,解决后台进程处理的相关问题,未必要保活。
一、低电耗(Doze)模式
Doze会通过推迟应用程序的后台CPU和网络活动来减少电池消耗。
系统会定期退出Doze一段时间,让应用程序完成延期活动。在此维护窗口期间,系统将运行所有挂起的同步,作业和警报,并允许应用程序访问网络。
在每个维护窗口结束时,系统再次进入Doze,暂停网络访问并推迟作业,同步和警报。随着时间的推移,系统会越来越少地安排维护窗口,有助于在设备未连接到充电器时长时间不活动时减少电池消耗。
一旦用户通过移动设备,打开屏幕或连接充电器唤醒设备,系统退出Doze并且所有应用程序恢复正常活动。
功能限制
在低电耗模式下,您的应用会受到以下限制:
- 暂停访问网络。
- 系统忽略唤醒锁定。
- 标准 AlarmManager 闹钟(包括
setExact()
和setWindow()
)推迟到下一个维护期。 - 如果您需要设置在设备处于低电耗模式时触发的闹钟,请使用
setAndAllowWhileIdle()
或setExactAndAllowWhileIdle()
,但9分钟内只能触发一次闹钟。使用setAlarmClock()
设置的闹钟将继续正常触发,系统会在这些闹钟触发之前不久退出低电耗模式。 - 系统不执行 WLAN 扫描。
- 系统不允许运行同步适配器。
- 系统不允许运行 JobScheduler。
adb指令强制进入Doze模式
$ adb shell dumpsys battery unplug
$ adb shell dumpsys deviceidle step 切换到下一个状态,idle和active状态之间切换
$ adb shell dumpsys deviceidle -h 查看帮助
$ adb shell dumpsys deviceidle force-idle [light|deep] 强制进入idle状态
$ adb shell dumpsys deviceidle force-inactive 强制进入inactive状态
$ adb shell dumpsys deviceidle unforce 强制解除idle、inactive状态,进入active状态
$ adb shell dumpsys deviceidle get [light|deep|force|screen|charging|network] 获取相应的当前状态
$ adb shell dumpsys battery reset 复位默认值,恢复设备激活状态。
二、应用待机模式(App Standby)
App Standby
推迟用户最近未与之交互的应用的后台网络活动。
adb指令强制进入standby模式
$ adb shell dumpsys battery unplug
$ adb shell am set-inactive <packageName> true 进入standby 模式
$ adb shell am set-inactive <packageName> false 退出standby模式,恢复到激活状态
$ adb shell am get-inactive <packageName> 获取当前状态
$ adb shell dumpsys battery reset 复位默认值。
三、Android不同版本对省电的优化
为了最大化电池并强制执行良好的应用行为,当用户看不到应用(或前台服务通知)时,Android会限制后台工作。
模拟App在后台运行的环境:
adb shell cmd appops set <package_name> RUN_IN_BACKGROUND ignore
adb shell cmd appops set <package_name> RUN_IN_BACKGROUND allow
Android 6.0
(API级别23)
引入了Doze
模式和App Standby
管理。当屏幕关闭且设备静止时,Doze
模式会限制应用程序行为。应用程序待机将未使用的应用程序置于特殊状态,限制其网络访问,作业和同步。
Android 7.0
(API级别24)
限制了隐式广播并引入了 Doze-on-the-Go
(缩短了进入Doze模式的时间,相关限制分时分优先级进行)。
三个被限制的隐式广播:
CONNECTIVITY_ACTION
ACTION_NEW_PICTURE
ACTION_NEW_VIDEO
Android 8.0
(API级别26)
进一步限制了后台行为,例如在后台获取位置和释放缓存的唤醒锁。
限制后台进程:应用在前台时,可以随意创建前台和后台服务;应用刚刚进入后台时拥有一个几分钟的窗口,此时也可以随意创建前台和后台服务;进入idle状态时,系统会终止应用的后台服务。
多数情况下JobScheduler
(官方Demo)可以取代后台服务,JobIntentService
取代IntentService
。
后台应用不能通过startService()
启动服务了,只能通过startForegroundService()
方法启动服务,然后应用有5秒钟时间调用startForground()
方法将服务置为前台服务,如果规定时间内没有调用该方法,app就会抛出 ANR 异常。
不能注册隐式广播接收者了(所谓隐式广播就是没有指定目标应用的广播,哪个应用都可以接收这样的广播)。
依然能注册显式广播。
不再有需要签名权限的广播,因为以后广播只会发送给签名相同的app了。
Android 9
(API级别28)
引入了 App Standby Buckets
,其中应用程序对资源的请求根据应用程序使用模式进行动态优先级排序,共有5个级别。
Active
: 正在被使用的或最近常被使用的App
Working Set
: 周期性频繁被使用的App
Frequent
: 常被使用,但不是每天都用的App
Rare
: 很少被使用的App
Never
: 安装后未被使用过的App
四、针对省电优化的解决方案
对于解决如何确保应用能优雅地处理后台任务,官方提供了最佳实践方案:
WorkManager不熟悉,别的都很好理解,对于WorkManager 说明一下:
对于可延迟的、异步的、即使您的设备或应用程序重新启动也要运行的工作,请使用 WorkManager。WorkManager 可以在满足工作条件(如网络可用性和功率)时优雅地运行可延迟的后台工作。
- WorkManager可以向后兼容到
API 14+
。-
API 23+
及以上版本实际上使用的是 JobScheduler -
API 14-22
使用的是AlarmManager& BroadcastReceiver
-
- 可以添加任务执行的约束条件,如网络状态、充电状态
- 可以处理一次性的异步任务、周期性的任务
- 可以监视,管理已经安排好的任务
- 任务执行是有先后顺序的,先触发的先执行
- 能确保任务得到执行,即使应用重启或设备重启
- 是遵从省电特性的,符合省电优化要求
使用WorkManager的典型场景:
- 发送logs、analytics 到后台服务器
- 周期性的从服务器获取或同步数据