Android前台服务及通知使用总结

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相比的话,有以下优点:

  1. 兼容性更强,最低API 14
  2. 调度灵活性强,内部包含JobScheduler、Firebase JobDispatcher、AlarmManager等
  3. 任务持久性保证,使用了持久性的任务调度策略,例如使用数据库来存储任务状态和进度
  4. 灵活的触发条件,有更多的触发条件,包括延迟执行、定期重复、网络连接等

使用的话,参考官方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()

使用场景感觉不多,下面是官方给的些经典场景:

  1. 定期查询最新新闻报道。
  2. 对图片应用过滤条件,然后保存图片。
  3. 定期将本地数据与网络上的数据同步。

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通知的使用,封装成了一个工具类。

相关推荐
沐言人生3 小时前
Android10 Framework—Init进程-8.服务端属性文件创建和mmap映射
android
沐言人生3 小时前
Android10 Framework—Init进程-9.服务端属性值初始化
android·android studio·android jetpack
追光天使4 小时前
【Mac】和【安卓手机】 通过有线方式实现投屏
android·macos·智能手机·投屏·有线
小雨cc5566ru4 小时前
uniapp+Android智慧居家养老服务平台 0fjae微信小程序
android·微信小程序·uni-app
一切皆是定数5 小时前
Android车载——VehicleHal初始化(Android 11)
android·gitee
一切皆是定数5 小时前
Android车载——VehicleHal运行流程(Android 11)
android
problc5 小时前
Android 组件化利器:WMRouter 与 DRouter 的选择与实践
android·java
图王大胜6 小时前
Android SystemUI组件(11)SystemUIVisibility解读
android·framework·systemui·visibility
服装学院的IT男10 小时前
【Android 13源码分析】Activity生命周期之onCreate,onStart,onResume-2
android
Arms20610 小时前
android 全面屏最底部栏沉浸式
android