Android前台服务及通知使用总结
前言
最近练了练前台服务的通知栏那的RemoteView,对前台服务、后台服务稍微做了点总结,把通知的使用也整理了下,先写一篇文章吧,后面再写RemoteView在通知栏和小组件的使用。
关于前台应用、后台应用、前台服务、后台服务的概念,我这不讲,下面主要是使用及注意点。
前后台服务限制
在Android各个版本中,前后台服务和前后台应用的关系,比较复杂,下面好好梳理下,分了四种情况。
"前台应用" 启动 "前台服务"
前台应用启动服务限制不多,就是Android 8的以后要用startForegroundService来启动"前台服务":
kotlin
fun startForegroundServiceFromForeground() {
val intent = Intent(context, AudioService::class.java)
if (Build.VERSION.SDK_INT >= 26) {
// Android8 必须通过startForegroundService开启前台服务,Android9 需要添加权限
context.startForegroundService(intent)
}else {
context.startService(intent)
}
}
专门给context设计一个startForegroundService方法,就是为了防止启动前台服务却不显示通知的问题。
上面前台应用启动服务后,要让服务变成前台服务,需要在service里面通过startForeground方法向通知栏发送一个通知:
kotlin
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
// 创建通知启动前台服务(要在5s内)
val notification = mAudioPlayerManager.startNotification()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 安卓10要添加一个参数,在manifest中配置
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
)
} else {
startForeground(NOTIFICATION_ID, notification)
}
return START_STICKY
}
Android 10里面前台服务要在manifest中配置权限(Android 14也有要求,这里不讲):
xml
<!-- Android 9(API28)要求注册前台服务权限,Android 14还要求具体的工作类型 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
在service里面注意要尽快向通知栏发送通知,使service变为前台,才不会触发ANR。
"前台应用" 启动 "后台服务"
前台应用启动后台服务,没有限制,但是当这个app进入后台,它只有几分钟的窗口期可以创建和使用服务。
kotlin
fun startBackgroundServiceFromForeground() {
val intent = Intent(context, AudioService::class.java)
context.startService(intent)
}
"后台应用" 启动 "前台服务"
后台应用都已经在后台了,系统自然会对它做很多限制,下面就比较麻烦:
kotlin
fun startForegroundServiceFromBackgroud() {
val intent = Intent(context, AudioService::class.java)
if (Build.VERSION.SDK_INT >= 31) {
// Android12 后台调用startForegroundService被禁止,推荐使用WorkManager
}else if (Build.VERSION.SDK_INT >= 26) {
// Android8 禁止了后台启动前台服务,需要使用startForegroundService
context.startForegroundService(intent)
}else {
context.startService(intent)
}
}
后台应用启动"前台服务"还是有应用场景的,比如播放器应用进入后台了,我这歌还得继续播放啊,不能被随随便便就回收资源了吧!
和前面一样,Android8前使用startService,Android8以后用startForegroundService启动服务,在service里面5秒内发送通知到通知栏,转成前台服务即可。
只不过在Android12直接把这功能给禁止了,后台应用不能再启动"前台服务"了,官方推荐使用WorkManager。
"后台应用" 启动 "后台服务"
后台应用启动"后台服务",在Android 8里面是没有限制的,通过startService启动服务,在service里面向通知栏发通知就行了。
不过Android8后,Android禁止了后台应用启动"后台服务",推荐使用JobScheduler去处理后台任务,JobScheduler会在合适的时机执行,这样手机就更省电。
kotlin
fun startBackgroundServiceFromBackground() {
// Android8 禁止了后台应用启动后台服务,推荐使用JobScheduler
if (Build.VERSION.SDK_INT >= 26) {
// 使用jobScheduler
}else {
val intent = Intent(context, AudioService::class.java)
context.startService(intent)
}
}
JobScheduler简单使用
都说到这了,JobScheduler的使用也简单说下吧:
首先,创建JobService,实际就是一个特殊的Service,把要做的任务放onStartJob里面,onStartJob返回值我理解就是是否直接结束startCommand:
kotlin
// 最低兼容Android 5.0
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class AudioJobService: JobService() {
// JobService实际还是一个service
override fun onCreate() {
super.onCreate()
}
override fun onStartJob(params: JobParameters?): Boolean {
// 返回true,表示任务将被再次调度执行,如果返回false表示任务完全结束
// 不耗时的操作,这时你应该返回false
// 耗时的操作例如数据下载等,应该开启一个新线程,并且返回true,并且线程里面要用jobFinished结束任务
return true
}
// 在条件不满足时才触发(即失败了),返回true,表示任务将被再次调度执行,如果返回false表示任务完全结束
override fun onStopJob(params: JobParameters?): Boolean {
return false
}
}
然后,因为是Service,还得去manifest里面注册下,要设置一个固定的权限:
xml
<service
android:name=".AudioJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />
最后,就是要让系统在合适的时机调用我们的AudioJobService,下面是配置的示例代码:
kotlin
// jobScheduler是一个系统服务
val jobScheduler =
context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
// jobInfoc构造器
val builder = JobInfo.Builder(0, ComponentName(context, AudioJobService::class.java))
// 设置延迟调度时间
.setMinimumLatency(1000)
// 设置该Job截至时间,在截至时间前肯定会执行该Job
.setOverrideDeadline(2000)
// 设置所需网络类型
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
// 设置在DeviceIdle时执行Job
.setRequiresDeviceIdle(true)
// 设置在充电时执行Job
.setRequiresCharging(true)
// 设置一个额外的附加项
.setExtras(PersistableBundle().apply { putInt("Audio", 666) })
// 生成jobInfo
val jobInfo = builder.build()
// 把任务发布给系统
jobScheduler.schedule(jobInfo)
// 取消指定的Job
// mJobScheduler.cancel(jobId)
// 取消应用所有的Job
// mJobScheduler.cancelAll()
WorkManager简单使用
WorkManager是JetPack的一个库,简单来说,它和JobScheduler相比的话,有以下优点:
- 兼容性更强,最低API 14
- 调度灵活性强,内部包含JobScheduler、Firebase JobDispatcher、AlarmManager等
- 任务持久性保证,使用了持久性的任务调度策略,例如使用数据库来存储任务状态和进度
- 灵活的触发条件,有更多的触发条件,包括延迟执行、定期重复、网络连接等
使用的话,参考官方Codelib简单写下,先引入仓库:
kotlin
dependencies {
implementation "androidx.work:work-runtime:2.8.1"
}
继承Worker或者CoroutineWorker,实现任务逻辑,doWork已经在异步线程了,不用创建线程了:
kotlin
class AudioWorker(
val context: Context,
params: WorkerParameters
): Worker(context, params) {
// Worker 是一个在后台线程上同步执行工作的类,也可以使用 CoroutineWorker,它可与 Kotlin 协程进行互操作
override fun doWork(): Result {
// 获取传入数据
val inputData = inputData.getInt("Audio", 0)
Log.d("TAG", "doWork: inputData = $inputData")
// worker执行在异步线程了
val result = AudioLoader.loadAudiosByMediaStore(context)
Log.d("TAG", "AudioWorker doWork: result = $result")
// 创建传出数据
val outputData = Data.Builder().apply {
// 竟然不支持序列化数据传递
val strResult = result.map { it.toString() }.toTypedArray()
putStringArray("result", strResult)
}.build()
// Result包含三种: success、failure、retry
return Result.success(outputData)
}
}
然后和JobScheduler一样,去配置并发布任务到系统:
kotlin
// 创建任务触发条件
val constraints: Constraints = Constraints.Builder()
.setRequiresCharging(true)
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
// 创建请求,周期性请求用PeriodicWorkRequest
val request = OneTimeWorkRequest.Builder(AudioWorker::class.java)
// 设置触发条件
.setConstraints(constraints)
// 符合触发条件后,延迟执行
.setInitialDelay(10, TimeUnit.SECONDS)
// 设置指数退避算法
.setBackoffCriteria(BackoffPolicy.LINEAR,
WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
// 为任务设置Tag标签。设置Tag后,你就可以通过该tag跟踪任务的状态
.addTag("AudioTag")
// 传入数据
.setInputData(
Data.Builder().apply {
putInt("Audio", 666)
}.build()
)
.build()
// 将任务提交给系统
WorkManager.getInstance(context).enqueue(request)
// 跟踪状态
// val workInfo = WorkManager.getInstance(context).getWorkInfoById(request.id)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(request.id)
.observe(context as ComponentActivity) {
// 结束的时候打印一下输出数据
if (it.state.isFinished) {
Log.d("TAG", "getWorkInfoByIdLiveData: result = " +
"${it.outputData.getStringArray("result")}")
}
}
// 取消任务
// WorkManager.getInstance(context).cancelWorkById(request.id)
// 任务链
// WorkManager.getInstance(context).beginWith(request).then(request).enqueue()
使用场景感觉不多,下面是官方给的些经典场景:
- 定期查询最新新闻报道。
- 对图片应用过滤条件,然后保存图片。
- 定期将本地数据与网络上的数据同步。
Android通知的使用
通知还是比较简单的,就是在Android8以后需要创建一个通知渠道,其他没什么说的,我封装了一个工具类,可以参考下:
kotlin
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.Color
import android.os.Build
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.Builder
/**
* 向通知栏发送通知工具类
*
* @author fdk
* @date 2021/07/12
*/
class NotificationUtil(val context: Context) {
// 通知 渠道信息
companion object {
const val NOTIFICATION_ID = 1
const val CHANNEL_ID = "channel_silencefly96_notification_id"
const val CHANNEL_NAME = "channel_silencefly96_notification_name"
}
/** 消息管理器 */
private val mManager: NotificationManager by lazy {
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
/** 内部保存一个默认的通知创建器 */
private var mNotificationBuilder: Builder? = null
/**
* 创建通知的builder,可以统一设置各个通知
*
* @param channelId 当前类使用的channelId
* @param channelName 前类使用的channelName
*/
fun createNotificationBuilder(
channelId: String = CHANNEL_ID,
channelName: String = CHANNEL_NAME
): Builder {
if (mNotificationBuilder != null) {
return mNotificationBuilder!!
}
// Android8 需要创建通知渠道
if (Build.VERSION.SDK_INT >= 26) {
createNotificationChannel(channelId, channelName)
}
// 创建NotificationBuilder,通知由service发送
mNotificationBuilder = Builder(context, channelId)
.setSmallIcon(android.R.drawable.stat_notify_more) // 必须设置,否则会奔溃
.setLargeIcon(BitmapFactory.decodeResource(context.resources, android.R.drawable.stat_notify_more))
// .setCustomContentView(remoteViews) // 折叠后通知显示的布局
// .setCustomHeadsUpContentView(remoteViews) // 横幅样式显示的布局
// .setCustomBigContentView(remoteViews) // 展开后通知显示的布局
// .setContent(remoteViews) // 兼容低版本
// .setColor(ContextCompat.getColor(context, R.color.blue)) // 小图标的颜色
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(NotificationCompat.DEFAULT_ALL) // 默认配置,通知的提示音、震动等
// .setAutoCancel(true) // 允许点击后清除通知
// .setContentIntent(pendingIntent) // 调集跳转
return mNotificationBuilder!!
}
/**
* 创建通知渠道,高版本要求,不然无法发送通知
*
* @param importance 重要程度
*/
@RequiresApi(api = Build.VERSION_CODES.O)
fun createNotificationChannel(
channelId: String = CHANNEL_ID,
channelName: String = CHANNEL_NAME,
importance: Int = NotificationManager.IMPORTANCE_DEFAULT
) {
val channel = NotificationChannel(
channelId,
channelName,
importance
).apply {
// 是否在应用图标的右上角展示小红点
setShowBadge(false)
// 推送消息时是否让呼吸灯闪烁。
enableLights(true)
// 推送消息时是否让手机震动。
enableVibration(true)
// 呼吸灯颜色
lightColor = Color.BLUE
}
mManager.createNotificationChannel(channel)
}
/**
* 创建简单的通知
*
* @param title 通知标题
* @param content 通知内容
* @param resID 图标
* @param pendingIntent 点击跳转到指定页面的intent
* @param notificationBuilder 通知builder
*/
fun createNotification(
title: String,
content: String,
resID: Int,
pendingIntent: PendingIntent? = null,
notificationBuilder: Builder? = null
): Notification {
// 创建默认builder
val builder = notificationBuilder ?: createNotificationBuilder()
// 设置pendingIntent,为null的时候不要去覆盖之前的
if(pendingIntent != null) {
builder.setContentIntent(pendingIntent)
}
// 传入标题、内容、图标、跳转,创建标题
return builder
.setContentTitle(title)
.setContentText(content)
.setSmallIcon(resID)
// .setContentIntent(pendingIntent)
.build()
}
/**
* 发送或更新(notificationId要保持一致)简单通知到通知栏
*
* @param title 通知标题
* @param content 通知内容
* @param resID 图标
* @param pendingIntent 点击跳转到指定页面的intent
* @param notificationBuilder 通知builder
* @param notificationId 通知ID
*/
fun sendOrUpdateNotification(
title: String,
content: String,
resID: Int,
pendingIntent: PendingIntent? = null,
notificationBuilder: Builder? = null,
notificationId: Int = NOTIFICATION_ID
) {
val notification = createNotification(title, content, resID, pendingIntent, notificationBuilder)
// 通过NOTIFICATION_ID发送,可借此关闭
mManager.notify(notificationId, notification)
}
/**
* 创建自定义通知
*
* @param remoteViews 显示的自定义view
* @param pendingIntent 点击跳转到指定页面的intent
* @param notificationBuilder 通知builder
*/
fun createCustomNotification(
remoteViews: RemoteViews,
pendingIntent: PendingIntent? = null,
notificationBuilder: Builder? = null
): Notification {
// 创建默认builder
val builder = notificationBuilder ?: createNotificationBuilder()
// 设置pendingIntent,为null的时候不要去覆盖之前的
if(pendingIntent != null) {
builder.setContentIntent(pendingIntent)
}
// 创建通知
return builder
.setCustomContentView(remoteViews) // 折叠后通知显示的布局
.setCustomHeadsUpContentView(remoteViews) // 横幅样式显示的布局
.setCustomBigContentView(remoteViews) // 展开后通知显示的布局
.setContent(remoteViews) // 兼容低版本
.setDefaults(NotificationCompat.DEFAULT_LIGHTS) // 默认配置,通知的提示音、震动等
// .setContentIntent(pendingIntent) // 通知点击跳转
.build()
}
/**
* 创建或更新(notificationId要保持一致)自定义通知并发送
*
* @param remoteViews 显示的自定义view
* @param pendingIntent 点击跳转到指定页面的intent
* @param notificationBuilder 通知builder
* @param notificationId 通知ID
*/
fun sendOrUpdateCustomNotification(
remoteViews: RemoteViews,
pendingIntent: PendingIntent? = null,
notificationBuilder: Builder? = null,
notificationId: Int = NOTIFICATION_ID
) {
val notification =
createCustomNotification(remoteViews, pendingIntent, notificationBuilder)
// 通过NOTIFICATION_ID发送,可借此关闭
mManager.notify(notificationId, notification)
}
/**
* 关闭通知栏上对应notificationId的通知
*
* @param notificationId 通知ID
*/
fun cancelNotification(
notificationId: Int = NOTIFICATION_ID
) {
mManager.cancel(notificationId)
}
}
简单使用的话,用sendOrUpdateNotification就行了,带自定义remoteViews的使用createCustomNotification方法即可。
小结
总结了一下Android前台服务、后台服务和前台应用、后台应用,在不同API上的行为限制,并牵引出JobScheduler和WorkManager,简单使用介绍了下它们的使用。最后还总结了下Android通知的使用,封装成了一个工具类。