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

相关推荐
SRC_BLUE_1741 分钟前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道4 小时前
Android打包流程图
android
镭封5 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰4875 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛6 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二6 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
奶茶喵喵叫6 小时前
Android开发中的隐藏控件技巧
android
Winston Wood8 小时前
Android中Activity启动的模式
android
众乐认证8 小时前
Android Auto 不再用于旧手机
android·google·智能手机·android auto
三杯温开水8 小时前
新的服务器Centos7.6 安卓基础的环境配置(新服务器可直接粘贴使用配置)
android·运维·服务器