FCM后台通知:图标白色?点击无效?解决方法
2025-03-25 13:46:23
搞定 FCM 后台通知:图标变白圈、点击意图不对?看这里!
用 Firebase Cloud Messaging (FCM) 给 App 推送消息挺方便,但你可能遇到过这种头疼事:App 在前台时,通知好好的,图标是你设置的,点击也能跳到指定页面或打开链接;可一旦 App 退到后台或者被杀掉,收到的通知图标就变成了一个白色的圆圈(或方块),点一下通知,竟然只是启动了 App,而不是执行你预设的操作,比如打开某个网页。
这到底是咋回事?明明代码里 setSmallIcon
和 PendingIntent
都设置好了呀?别急,咱们来分析分析,再看看怎么解决。
一、 问题在哪?为啥后台表现不一样?
这背后其实有两个关键点在“捣鬼”:
-
小图标 (
setSmallIcon
) 的显示规则变了:- 从 Android 5.0 (Lollipop) 开始,系统状态栏通知图标(就是用
setSmallIcon
设置的那个)有个硬性要求:必须是纯色图标,带 Alpha 透明通道 。系统会自动把它渲染成白色(在深色背景下)或深灰色(在浅色背景下)。如果你给的是一个彩色的、复杂的图标,系统没办法正确处理,很多时候就直接显示成一个纯色的色块(通常是白色),形状跟你图标的轮廓差不多,所以看起来就是个白圈或白方块。 - 前台时为啥能显示?这可能是因为前台通知的渲染方式有时略有不同,或者你看到的不是状态栏里那个小小的图标,而是通知下拉后展开的大图标(
setLargeIcon
设置的,这个可以是彩色的)。但状态栏那个小图标,规则是统一的。
- 从 Android 5.0 (Lollipop) 开始,系统状态栏通知图标(就是用
-
FCM 消息类型与 App 状态决定了谁来处理通知:
- FCM 可以发送两种主要类型的消息:通知消息 (Notification Message) 和 数据消息 (Data Message) ,也可以两者混合。
- 当 App 在前台运行时: 不管你发的是通知消息还是数据消息,FCM SDK 都会把消息传递给你自己写的
FirebaseMessagingService
的onMessageReceived
方法。也就是说,你的sendNotification
函数会被调用,完全由你自己的代码来创建和显示通知。图标、点击意图(Intent)自然都是你代码里写的那样。 - 当 App 在后台或已关闭时:
- 如果发送的是 通知消息 *:为了省电和效率,Android 系统(或者说 Google Play 服务)会直接接管* 这个通知的显示!它会读取你从 FCM 控制台或服务器发送的
notification
载荷里的title
,body
,icon
,sound
,click_action
等字段,然后自己创建并显示通知。你的FirebaseMessagingService
的onMessageReceived
方法根本不会被调用! 这就解释了为什么图标和点击行为不对了:系统用了它自己的一套逻辑,而不是你 App 里的sendNotification
函数。默认情况下,点击系统处理的通知,行为通常就是启动你的 App 主 Activity。 - *如果发送的是 数据消息 *:这时,系统会将消息传递给你的
FirebaseMessagingService
的onMessageReceived
方法(后台运行时可能会有延迟,受系统Doze模式等影响)。这样一来,你的sendNotification
函数就有机会执行,通知的图标和点击行为就能按你的代码来。
- 如果发送的是 通知消息 *:为了省电和效率,Android 系统(或者说 Google Play 服务)会直接接管* 这个通知的显示!它会读取你从 FCM 控制台或服务器发送的
现在清楚了吧?你遇到的问题,根源在于:
- 后台收到的“通知消息”是系统直接显示的,没走你代码里的
sendNotification
。 - 提供给
setSmallIcon
的图标不符合 Android 系统规范。
二、 对症下药:解决方案来了
知道了原因,解决起来就思路清晰了。我们需要分别处理图标和点击意图的问题。
方案一:让小图标乖乖现身
原理: 提供一个符合 Android 规范的单色 Alpha 图标。
操作步骤:
-
准备图标资源:
- 你需要创建一个只包含白色和透明部分 的图标。通常是 PNG 格式,或者更推荐使用 Vector Drawable (XML 矢量图形)。
- 图标内容应该是你希望在状态栏显示的那个小小的、简洁的标识。想想 Wi-Fi、蓝牙那些系统图标,就那个感觉。
- 确保图标尺寸合适,一般状态栏图标建议尺寸为 24x24 dp。对于不同分辨率,系统会自动缩放,或者你也可以提供不同
mipmap
目录下的版本 (mdpi, hdpi, xhdpi, etc.)。 - 快速生成工具: 可以使用 Android Studio 自带的 "Image Asset" 或 "Vector Asset" 工具来创建。右键点击
res
目录 -> New -> Image Asset/Vector Asset。选择 "Notification Icons" 类型,它会自动帮你处理成单色。也可以用 Figma、Sketch 等设计工具导出符合要求的资源。 - 假设你生成了一个名为
ic_notification_mono.xml
(Vector Drawable) 或ic_notification_mono.png
的图标。
-
在代码中引用新图标:
修改sendNotification
函数,将setSmallIcon
指向这个新的单色图标:// Build the notification val notification = NotificationCompat.Builder(context, channelID) .setSmallIcon(R.drawable.ic_notification_mono) // <--- 使用单色图标! .setContentTitle(title) .setContentText(message) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true) .setContentIntent(pendingIntent) // 如果需要,可以设置彩色的大图标,它会在通知下拉展开时显示 // .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.your_color_launcher_icon)) .build()
注意事项:
- 这个修改同时适用于前台和后台 。因为即使是后台,如果你采用下面的“数据消息”方案让
sendNotification
得以执行,这个图标也是它来设置的。如果是系统处理的“通知消息”,它也会尝试读取你 Manifest 里设置的默认图标,或者你在 FCM 载荷里指定的icon
字段,但最佳实践还是在客户端代码里用setSmallIcon
指定。确保这个资源存在且符合规范至关重要。 - 测试时,务必将 App 完全杀掉(从最近任务列表划掉)再发送通知,模拟后台/关闭状态。
方案二:让后台点击行为听你的话
如前所述,后台的“通知消息”默认不走你的 onMessageReceived
。要让点击行为符合预期(比如打开特定 URL),你有两种主要思路:
思路 A:强制走 onMessageReceived
(推荐)
原理: 从服务端发送 纯数据消息 (Data Message) ,或者包含 data
载荷的消息(根据平台和需求配置)。这样,即使 App 在后台,系统也会唤醒你的 FirebaseMessagingService
(受系统限制) 并调用 onMessageReceived
,让你有机会执行 sendNotification
,从而使用你在代码里精心构造的 PendingIntent
。
操作步骤:
-
修改服务端发送的 FCM 载荷:
- 确保你的服务器发送的 JSON 载荷中,不包含
notification
键,只包含data
键。把原来放在notification
里的title
、body
等信息,都挪到data
对象里。
例如,修改前的载荷可能是:
{ "to": "DEVICE_TOKEN", "notification": { "title": "App Update Available!", "body": "Click to see what's new.", "icon": "notification_icon", // 这个 icon 字段效果往往不如客户端设置的好 "click_action": "YOUR_ACTIVITY_TO_OPEN" // 这个也容易出问题 }, "data": { "url": "https://github.com/georgeclensy/escape" // 其他自定义数据 } }
修改后的载荷应该是 (只保留
data
) :{ "to": "DEVICE_TOKEN", "priority": "high", // 建议设为 high,增加后台唤醒概率 "data": { "title": "App Update Available!", "message": "Click to see what's new.", "url_to_open": "https://github.com/georgeclensy/escape", // 任何你需要的信息都放在 data 里 "channel_id": "updates", "channel_name": "Updates" } }
注意: 使用
data
消息时,后台唤醒onMessageReceived
依赖于设备状态(Doze 模式、应用限制等)。设置priority: "high"
(Android) 有助于提高送达和处理优先级,但不能完全保证实时。 - 确保你的服务器发送的 JSON 载荷中,不包含
-
修改
onMessageReceived
以解析data
载荷:
现在onMessageReceived
会被调用了(即使在后台),你需要从remoteMessage.data
而不是remoteMessage.notification
中提取信息。import android.content.Intent import android.net.Uri import android.util.Log import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage class MyFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) Log.d("FCM", "From: ${remoteMessage.from}") // 重点:现在从 data 载荷获取数据 remoteMessage.data.isNotEmpty().let { Log.d("FCM", "Message data payload: " + remoteMessage.data) // 从 data 中取出你发送的字段 val title = remoteMessage.data["title"] val message = remoteMessage.data["message"] val urlString = remoteMessage.data["url_to_open"] // 你可以传递 channel ID 等信息,如果需要动态创建的话 val channelId = remoteMessage.data["channel_id"] ?: "default_channel" val channelName = remoteMessage.data["channel_name"] ?: "Default Channel" if (!title.isNullOrBlank() && !message.isNullOrBlank()) { // 创建用于点击通知的 Intent val notificationIntent: Intent = if (!urlString.isNullOrBlank()) { // 如果有 URL,创建 ACTION_VIEW Intent Intent(Intent.ACTION_VIEW, Uri.parse(urlString)).apply { // 重要:添加 FLAG_ACTIVITY_NEW_TASK 是个好习惯, // 因为 Service 的 Context 启动 Activity 需要这个 flag。 // 如果你用 Activity Context 创建 Intent 则不需要。 // 也可以考虑 FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP // 取决于你希望如何管理 Activity 栈。 flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP } } else { // 没有 URL,可以默认启动主 Activity // 假设你的主 Activity 是 MainActivity Intent(this, MainActivity::class.java).apply { // 注意 Context 换成了 this (Service) flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP } // 或者根据 data 里其他字段决定跳转到哪个 Activity } // 调用你的通知发送函数 sendNotification( this, // 使用 Service Context title, message, channelId, channelName, notificationIntent ) } } // 如果你的消息同时包含了 notification 和 data,这里的逻辑会更复杂 // 但我们推荐使用纯 data 消息来统一处理行为 } // onNewToken 和 sendNotification 函数保持不变 (除了图标已在方案一修改) // ... (确保 sendNotification 函数在这里或者可以被 MyFirebaseMessagingService 访问) } // 你的 sendNotification 函数 (确认图标已改为单色) // fun sendNotification(...) { ... }
这种方法的优点:
- 行为统一: 无论 App 在前台、后台还是关闭,通知的创建和显示逻辑都由你的
sendNotification
函数控制。 - 灵活性高: 你可以在
onMessageReceived
中执行任意复杂的逻辑,根据data
内容动态决定通知样式、点击行为等。
安全建议:
- 对于
PendingIntent
,使用PendingIntent.FLAG_IMMUTABLE
是 Android 12 (API 31) 及更高版本的强制要求和安全最佳实践。它能防止其他应用篡改你的PendingIntent
。如果你的Intent
包含需要修改的数据(很少见于通知点击),才考虑FLAG_MUTABLE
并做好安全防护。你代码里FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
的组合是正确的,表示如果存在相同请求码的 PendingIntent,则更新其 Extra 数据,同时保持其不可变性。 - 对从
remoteMessage.data
获取的 URL 或其他数据进行校验,防止潜在的安全风险。
进阶技巧:
- 你可以利用
data
载荷传递更丰富的指令,比如不仅仅是打开 URL,还可以触发应用内的特定动作(如下载、同步、导航到特定页面片段等)。这通常需要你设计一套健壮的“深层链接”(Deep Linking) 机制或内部路由方案。
思路 B:利用 notification
载荷的标准字段(有限制)
原理: 如果你必须或倾向于使用“通知消息”,可以尝试利用 FCM notification
载荷里的一些标准字段,让系统“猜对”你的意图。但这通常功能有限,且不如数据消息灵活。
-
click_action
: 这个字段可以指定一个 Action 字符串 ,对应你 AndroidManifest.xml 中某个 Activity 的<intent-filter>
的<action>
。当用户点击系统处理的通知时,系统会尝试发出一个带有这个 Action 的 Intent 来启动匹配的 Activity。- 局限性:
- 你需要在 Manifest 里为目标 Activity (比如一个专门处理通知跳转的透明 Activity,或者你的主 Activity) 配置好对应的 Intent Filter。
- 传递动态数据(比如特定的 URL)比较麻烦。你通常只能在 Intent Filter 中声明要启动哪个 Activity,该 Activity 启动后需要自己从 Intent 的 extras (如果有的话,系统如何传递不确定) 或通过其他方式获取具体操作所需的数据。
- 对于简单地打开 主 Activity,这通常是默认行为,
click_action
可能不是必须的。对于打开 特定 URL,直接用click_action
难以直接实现Intent.ACTION_VIEW
的效果。
- 局限性:
-
link
: (较新,作为android.notification.click_action
的替代方案之一,并跨平台) 在notification
载荷下可以尝试添加link
字段,值为你想打开的 URL。{ "to": "DEVICE_TOKEN", "notification": { "title": "Check this out!", "body": "Visit our website.", "link": "https://your-target-url.com" // 尝试使用 link 字段 } }
- 效果: FCM SDK 或系统可能会识别这个
link
并尝试创建一个ACTION_VIEW
Intent 来打开它。但这并非所有 Android 版本或设备都保证的行为 ,且可能没有使用FLAG_ACTIVITY_NEW_TASK
,在某些情况下表现可能不符合预期。它可能没有你通过PendingIntent
控制得那么精确。
- 效果: FCM SDK 或系统可能会识别这个
对比与选择:
- 方案 A (数据消息) 是目前控制 Android 后台通知行为最可靠、最灵活 的方式。它保证了行为一致性,并允许你在 App 代码中完全掌控通知的呈现和交互。
- 方案 B (利用
notification
字段) 更像是利用系统提供的“快捷方式”,适用于非常简单的场景(如总是打开主 Activity),或者在特定情况下尝试利用link
字段。但对于需要精确控制、传递动态数据或执行复杂操作的场景,往往不够用或者行为不稳定。
综上,强烈推荐采用 方案 A (数据消息) 来解决后台点击意图的问题,并结合 方案一 解决图标问题。
三、 总结一下
遇到 FCM 后台通知图标变白、点击行为不对的问题时:
- 图标问题: 确保
setSmallIcon()
使用的是纯白 + 透明背景 的单色图标资源(推荐 Vector Drawable)。 - 点击意图问题: 最佳实践是改用数据消息 (Data Message) ,从服务器只发送
data
载荷(或优先处理data
),然后在FirebaseMessagingService
的onMessageReceived
中解析data
内容,并调用你自己的sendNotification
函数来构建和显示通知,包括设置正确的PendingIntent
。记得给 Intent 设置合适的 Flags (FLAG_ACTIVITY_NEW_TASK
,FLAG_ACTIVITY_CLEAR_TOP
等) 并使用PendingIntent.FLAG_IMMUTABLE
。
完成这两步修改后,你的 FCM 通知在 App 后台或关闭状态下,应该就能正常显示小图标,并且点击后执行你预期的操作了。