通知与后台任务:让App在后台也能“活”起来
各位同学,今天我们来聊聊Android开发中一个非常“接地气”的话题——通知与后台任务。说白了,就是你的App在用户没打开它的时候,怎么跟用户“打招呼”,以及怎么在后台偷偷干活。
我刚开始做Android开发那会儿,对这块儿也是一头雾水。记得有一次,我写了个下载功能,用户一按Home键,下载就断了。用户跑来骂我:“你这App是不是有毛病?” 嗯,从那以后,我就把后台任务这块儿研究透了。
1. 通知渠道:Android 8.0 的分水岭
先说说通知。Android 8.0(API 26)之后,通知系统来了个大改版。你想想看,以前的通知多乱啊,所有App的通知都挤在一起,用户想关掉某个烦人的通知,只能一刀切——把整个App的通知全关了。
Google 觉得这样不行,于是引入了通知渠道(Notification Channel)。说白了,就是给通知分门别类。比如你的App有“订单通知”、“促销通知”、“系统通知”,每个都可以单独设置重要性、声音、震动等。
核心要点:创建通知必须先创建渠道,否则通知发不出来。这是强制要求。
来看代码怎么写:
// 创建通知渠道(一般在 Application 的 onCreate 中执行)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"order_channel", // 渠道ID
"订单通知", // 用户可见的渠道名称
NotificationManager.IMPORTANCE_HIGH // 重要性级别
).apply {
description = "包含订单状态变更的通知"
enableVibration(true)
setSound(
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
Notification.AUDIO_ATTRIBUTES_DEFAULT
)
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
这里有个坑,我踩过。渠道一旦创建,就不能再修改重要性级别了。用户可以在系统设置里改,但你的代码改不了。所以创建渠道前,一定要想好每个渠道的定位。
我的习惯:在 Application 的 onCreate 里统一创建所有渠道。如果渠道已经存在,系统会忽略重复创建,所以放心调用。
2. 通知样式:别让用户觉得你Low
通知不只是显示一行文字。Android 提供了好几种样式,让你的通知看起来更专业。
| 样式类型 | 适用场景 | 关键类 |
|---|---|---|
| 大文本样式 | 长消息、邮件正文 | BigTextStyle |
| 大图样式 | 图片分享、商品展示 | BigPictureStyle |
| 收件箱样式 | 聊天记录、邮件列表 | InboxStyle |
| 媒体样式 | 音乐播放、视频控制 | MediaStyle |
| 消息样式 | 即时通讯(带头像) | MessagingStyle |
举个例子,如果你要做个聊天App,用 MessagingStyle 会非常合适:
val message = Notification.MessagingStyle.Message(
"你好,明天下午三点开会",
System.currentTimeMillis(),
"张三"
)
val style = Notification.MessagingStyle("我")
.setConversationTitle("项目讨论组")
.addMessage(message)
val notification = NotificationCompat.Builder(this, "chat_channel")
.setStyle(style)
.setSmallIcon(R.drawable.ic_chat)
.build()
我个人建议,能用样式就别用默认的。用户第一眼看到的就是通知,样式好看了,用户对App的印象也会好很多。
3. 前台服务:让任务“光明正大”地跑
后台任务有个大问题:系统会杀进程。Android 从 8.0 开始,对后台执行限制越来越严。那如果你的App需要长时间运行一个任务,比如播放音乐、记录运动轨迹,怎么办?
答案就是前台服务(Foreground Service)。前台服务会显示一个持续的通知,告诉用户:“嘿,我正在后台干活呢,别杀我。”
注意:前台服务必须有一个对应的通知,而且这个通知不能被用户划掉。这是系统的硬性要求。
代码实现分三步:
- 创建通知渠道和通知
- 在 Service 的
onStartCommand中调用startForeground() - 在
onDestroy中调用stopForeground()
class MusicService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = createNotification()
startForeground(NOTIFICATION_ID, notification)
// 开始播放音乐...
return START_STICKY
}
private fun createNotification(): Notification {
return NotificationCompat.Builder(this, "music_channel")
.setContentTitle("正在播放")
.setContentText("晴天 - 周杰伦")
.setSmallIcon(R.drawable.ic_music)
.setOngoing(true) // 不可划掉
.build()
}
override fun onDestroy() {
stopForeground(true) // true 表示移除通知
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
}
我曾经在一个运动App里用过前台服务。用户开始跑步后,服务一直在后台记录GPS数据。如果不做前台服务,跑着跑着系统就把进程杀了,用户数据就丢了。那用户体验得多差啊。
4. WorkManager:最优雅的后台任务方案
说到后台任务,我得好好夸夸 WorkManager。这是 Google 官方推荐的方案,也是我目前在项目中最常用的。
为什么推荐它?因为它太聪明了。它会根据系统状态(电量、网络、空闲等)来决定什么时候执行任务。而且它保证任务一定会执行,哪怕App被杀了、手机重启了。
适用场景:上传日志、同步数据、定期清理缓存等不需要立即执行的任务。
来看一个简单的例子:
// 1. 定义任务
class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
// 执行上传操作
return try {
uploadLogs()
Result.success()
} catch (e: Exception) {
Result.retry() // 失败后重试
}
}
}
// 2. 调度任务
val workRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // 需要网络
.setRequiresBatteryNotLow(true) // 电量不能太低
.build()
)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
WorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()
WorkManager.getInstance(this).enqueue(workRequest)
你想想看,以前我们做这种任务,得自己写Service、自己处理网络监听、自己管理重试逻辑。现在 WorkManager 全包了。我现在的项目里,所有非紧急的后台任务都用它。
避坑指南:WorkManager 的 doWork() 运行在后台线程,所以别在里面做UI操作。另外,任务默认是异步的,如果你需要同步执行,可以用 ListenableWorker。
5. JobScheduler 与 AlarmManager:老牌选手的用武之地
虽然 WorkManager 很强大,但有些场景还是得用 JobScheduler 和 AlarmManager。
JobScheduler 是 Android 5.0 引入的,它和 WorkManager 有点像,但更底层。如果你需要更精细地控制调度条件,可以用它。不过说实话,现在 WorkManager 底层就是用的 JobScheduler,所以大多数情况下直接用 WorkManager 就行。
AlarmManager 就不一样了。它是用来做精确的定时任务的。比如闹钟App,必须在指定时间响铃,这个 WorkManager 做不到。
// 设置一个30秒后的精确闹钟
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(this, AlarmReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
this, 0, intent, PendingIntent.FLAG_IMMUTABLE
)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + 30_000,
pendingIntent
)
注意:从 Android 12 开始,精确闹钟需要申请 SCHEDULE_EXACT_ALARM 权限,而且用户可以在设置里关闭。所以别太依赖它。
我记得有一次,我做一个定时提醒的App,一开始用的 WorkManager,结果发现它不精确,有时候会延迟几分钟。后来换成 AlarmManager,问题就解决了。所以选工具要看场景。
总结一下
通知和后台任务这块儿,说白了就是平衡用户体验和系统限制。我的建议是:
- 通知:一定要用渠道,样式尽量丰富
- 前台服务:长时间运行的任务必须用,别偷懒
- WorkManager:90%的后台任务用它就够了
- AlarmManager:只有精确定时才用它
嗯,今天就讲到这里。这些知识点在实际项目中天天都会用到,大家一定要动手试试。代码写多了,自然就熟了。