Android: 保活篇
Admin Lv3

安卓并不保证每个应用都能体面地走完自己的生命周期,当资源紧张或者它心情不好时没有实例可以幸免于难。当然,拥有特权的系统级和一些善于自我保护的聪明应用除外…

构建 Foreground Service

通常情况下,一个Service实例启动后会默默地在后台运行,但如果它调用了这个接口

1
startForeground(id, notification)

安卓会让它跑到前台,成为一个 Foreground Service,不会轻易地干掉它以及它所属的应用;当然,这也带来一个副作用 – Foreground Service 存活期间需要在通知栏里留下一个持续存在的通知来去让用户知晓其存在。当完成使命后可以调用如下接口在自我了断

1
2
stopForeground(true)
stopSelf()

或者什么也不干等着被用户和其他组件干掉

实现

新增一个普通的Service,并在onCreate钩子下设置好通知信息然后调用startForeground()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private fun createNotification(): Notification {
companion object {
private const val CHANNEL_ID = "app_channel"
}
// Android 8.0 之后需要先构建 NotificationChannel 才能推送通知
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"App Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Foreground Service")
.setContentText("Foreground service starts")
.setContentIntent(pendingIntent)
.build()
}

override fun onCreate() {
...
val notification = createNotification()
startForeground(1, notification)
}

override fun onStartCommand(...) {
...
return START_STICKY // 如果这个服务因为资源原因被干掉了系统会在之后重启它
}

在Manifest文件里声明一下需要的权限以及Service本身

1
2
3
4
5
6
7
8
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application ...>
<service
android:name=".ForegroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="mediaPlayback"/>
</application>

启动

1
2
3
4
5
6
7
8
9
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

startForegroundService(Intent(this, ForegroundService::class.java))
finish()
}
}

startForegroundService()假定开始的服务是前台服务,被启动的服务在5秒内没有调用startForeground()则会被停止并且报错。

终止

1
2
Intent serviceIntent = new Intent(this, ForegroundService::class.java)
stopService(serviceIntent)

获取 Wake Lock

由电池供电的安卓设备在长时间未被用户使用后会自动进入挂起状态以延长电池寿命,该状态下CPU停止执行指令。为了不被停止,应用可以尝试获取 Wake Lock 阻止设备进入挂起状态。同一时间可以有多个应用持有 Wake Lock,且有应用持有 Wake Lock 时设备不会被挂起。

Wake Lock 类型

自安卓4.2之后只支持 PARTIAL_WAKE_LOCK 一种类型,此类 Wake Lock 只能保证CPU不会被暂停,但不能保证屏幕不会被熄灭。需要保持屏幕不灭需要附上

1
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

声明权限

WAKE_LOCK是一项普通权限,只用在Manifest文件里声明

1
<uses-permission android:name="android.permission.WAKE_LOCK" />

获取

1
2
3
4
5
6
val wakeLock: PowerManager.WakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag").apply {
acquire(MAX_HOLD_TIME_IN_MS)
}
}

释放

1
wakelock.release()

对抗 Doze Mode

WAKE_LOCK是一项普通权限,这意味着应用在启动后不需要弹窗向用户获取该权限,用户很容易忽视它。再加上acquire()可以无期限的持有 Wake Lock,应用很容易滥用这个权限,对设备能耗造成负担。

安卓6.0之后出台了 Doze Mode(低能耗模式,注意与低电量模式是两个概念)来取进一步的保护电池,延长续航时间。使用电池电量的设备长时间没有被用户使用会进入 Doze Mode,该模式下网络和后台活动会进入 休眠-维护 的循环,休眠期间的活动会被延迟到维护期间完成,随着时间的推进休眠期会越来越长。
图片溜走了
当被用户使用或者接电源时设备会退出 Doze Mode 。持有 Wake Lock 的应用仍会受其约束。

无视 Doze Mode

安卓6.0后应用可以获取REQUEST_IGNORE_BATTERY_OPTIMIZATIONS权限来脱离的 Doze Mode 影响

1
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

这是一项危险权限,除了在Manifest里声明外,还需要在程序里主动向用户请求

1
2
3
4
5
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:$packageName")
startActivity(intent)
}

结语

上述方案虽然实现简单,但由于创建 Foreground Service 带来的强制通知以及申请无视 Doze Mode 带来的弹窗请求,程序并不能很好的隐藏自己,不够优雅。