Firebase背景通知:搞定JSON解析与自定义图片显示
2025-05-06 21:27:11
Firebase 背景通知:搞定 JSON Body 解析与自定义图片显示
用 Firebase Cloud Messaging (FCM) 推送通知,标题是字符串,这没毛病。但 body 部分,如果塞的是 JSON 数据,并且还想在通知里头带上一张自定义图片,那麻烦就来了。
App 在前台运行时,onMessageReceived()
方法能正常接收到通知,解析 JSON、显示图片都好说。
一旦 App 退到后台或者被干掉了,onMessageReceived()
就拜拜了。Firebase 会自个儿把通知怼到系统通知栏。这时候,JSON 格式的 body 内容没法儿解析,自定义图片更是想都别想,Firebase 只会用默认图标。这体验,简直了!
一、到底为啥会这样?消息类型的锅!
要弄明白这个问题,得先搞清楚 FCM 的两种消息类型:
-
通知消息 (Notification Messages):
- 有时也叫“显示消息”。这类消息,FCM SDK 会自动处理,直接在系统通知栏展示。
- 优点:简单省事,不用写啥代码。
- 缺点:自定义能力差。当 App 在后台或被杀死时,
onMessageReceived()
不会 被调用。通知内容(标题、文本)直接取自notification
载荷,图片通常是应用图标。
-
数据消息 (Data Messages):
- 这类消息,无论 App 在前台、后台还是被杀死,
onMessageReceived()
总是 会被调用(App 被强行停止除外)。 - 优点:完全控制权。你可以在
onMessageReceived()
里解析数据,然后自己构建和显示一个完全自定义的通知。 - 缺点:稍微麻烦点,得自己写代码处理。
- 这类消息,无论 App 在前台、后台还是被杀死,
我们遇到的问题,症结就在于:当 App 在后台时,如果发送的是包含 notification
载荷的 FCM 消息 ,系统就会接管通知的显示,导致我们无法介入处理 JSON body 和自定义图片。
图片来源: Firebase官方文档,展示了消息处理流程
当你的 FCM 载荷长这样:
{
"to": "DEVICE_TOKEN",
"notification": {
"title": "打折啦!",
"body": "{\"product_name\": \"神仙水\", \"discount\": \"5折\", \"expires_at\": \"2024-12-31\"}"
// "image": "some_image_url" // 这个 image 字段,虽然 FCM 支持,但行为可能不完全符合预期,尤其是在后台
},
"data": {
"extra_info": "一些额外数据"
}
}
App 在后台时,notification
里的 body
会被直接当成字符串显示,JSON 根本没机会解析。data
载荷里的数据虽然能收到,但得等用户点击通知,在启动的 Activity 的 Intent 里才能拿到,对通知本身的展示没帮助。
二、解决方案:把控制权夺回来!
核心思路:统一使用数据消息 (Data Messages) 。这样,onMessageReceived()
就能稳定发挥作用,我们想怎么玩都行。
方案:只发送数据消息 (Data-Only Messages)
这是最推荐,也是最能解决问题的方法。
1. 原理和作用
完全抛弃 FCM 载荷中的 notification
字段,只用 data
字段来传递所有信息,包括标题、JSON 格式的 body、图片 URL 等。
这样,不论 App 处于何种状态(前台、后台、被杀死后首次接收),消息都会被路由到 FirebaseMessagingService
的 onMessageReceived()
方法。我们就能在这里:
- 解析
data
载荷中的 JSON body。 - 根据图片 URL 下载图片。
- 构建一个完全自定义的本地通知,并显示它。
2. 发送端调整 (以 Firebase Admin SDK for Node.js 为例)
假设你想发送的 JSON body 结构是:
{
"item_name": "酷炫T恤",
"price": "¥99.9",
"promo_details": "夏日特惠,全场包邮!",
"image_url": "https://example.com/tshirt.png"
}
你需要把它作为字符串放到 data
载荷的一个字段里,比如叫 custom_payload
。同时,通知的标题、要显示的图片 URL 也都放进 data
。
// 使用 Firebase Admin SDK (Node.js)
const admin = require('firebase-admin');
// 初始化你的 Firebase Admin SDK
// admin.initializeApp({ ... });
const deviceRegistrationToken = 'YOUR_DEVICE_TOKEN';
const jsonDataBody = {
item_name: "酷炫T恤",
price: "¥99.9",
promo_details: "夏日特惠,全场包邮!"
};
const message = {
data: {
notification_title: "上新通知!", // 以前放 notification.title 的内容
notification_body_json: JSON.stringify(jsonDataBody), // 以前放 notification.body 的 JSON 内容
notification_image_url: "https://example.com/tshirt.png" // 图片 URL
// 你还可以加其他自定义数据字段
// "click_action": "FLUTTER_NOTIFICATION_CLICK", // 如果是Flutter,可能需要这个
// "screen": "/product/123" // 自定义深链接参数
},
token: deviceRegistrationToken
};
admin.messaging().send(message)
.then((response) => {
console.log('Successfully sent message:', response);
})
.catch((error) => {
console.log('Error sending message:', error);
});
看清楚,我们没有 notification
对象了,所有东西都在 data
里。
3. 客户端处理 (FirebaseMessagingService
- Android Java/Kotlin)
在你的 FirebaseMessagingService
子类中,覆写 onMessageReceived()
方法:
首先,确保你的 AndroidManifest.xml
配置正确:
<manifest ...>
<application ...>
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- Android 8.0 (API level 26) 及以上版本需要通知渠道 -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
<!-- 你可能还需要自定义通知图标和颜色 -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification_custom" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/colorAccent" />
</application>
</manifest>
然后是 MyFirebaseMessagingService.java
:
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import org.json.JSONObject; // 使用 org.json 进行简单解析
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private static final String TAG = "MyFirebaseMsgService";
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Log.d(TAG, "From: " + remoteMessage.getFrom());
// Check if message contains a data payload.
if (remoteMessage.getData().size() > 0) {
Log.d(TAG, "Message data payload: " + remoteMessage.getData());
Map<String, String> data = remoteMessage.getData();
String title = data.get("notification_title");
String jsonBodyStr = data.get("notification_body_json");
String imageUrl = data.get("notification_image_url");
String parsedBodyText = ""; // 解析后的 JSON body 用于通知内容
// 1. 解析 JSON Body
if (jsonBodyStr != null) {
try {
JSONObject jsonObject = new JSONObject(jsonBodyStr);
String itemName = jsonObject.optString("item_name", "");
String price = jsonObject.optString("price", "");
String promoDetails = jsonObject.optString("promo_details", "");
// 你可以根据需要组合这些信息
parsedBodyText = itemName + "仅售" + price + "!" + promoDetails;
if (parsedBodyText.isEmpty()){
parsedBodyText = "点击查看详情"; // 兜底文案
}
} catch (Exception e) {
Log.e(TAG, "JSON parsing error: " + e.getMessage());
parsedBodyText = "您有一条新消息,点击查看。"; // 解析失败的兜底文案
}
} else {
// 如果服务端忘了传 notification_body_json,给个默认提示
parsedBodyText = "您有一条新消息";
}
// 2. 下载图片 (这是一个简单示例,实际项目中推荐使用 Glide, Picasso 等库)
Bitmap largeIconBitmap = null;
if (imageUrl != null && !imageUrl.isEmpty()) {
largeIconBitmap = getBitmapFromUrl(imageUrl);
}
// 3. 构建并显示通知
sendNotification(title, parsedBodyText, largeIconBitmap, data);
}
// 如果消息包含 notification 载荷,这里也可以处理,但对于本问题,我们关注 data 消息
if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
// 如果你的策略是混合使用,可以在这里处理只含 notification 的消息
// 但对于需要自定义图片和 JSON 解析的场景,主要依赖上面的 data 处理逻辑
}
}
private Bitmap getBitmapFromUrl(String imageUrl) {
try {
URL url = new URL(imageUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
return BitmapFactory.decodeStream(input);
} catch (IOException e) {
Log.e(TAG, "Error downloading image: " + e.getMessage());
return null;
}
}
private void sendNotification(String title, String messageBody, Bitmap image, Map<String, String> data) {
Intent intent = new Intent(this, MainActivity.class); // 点击通知后打开的 Activity
// 你可以将 data 里的其他数据通过 intent extras 传递给 Activity
for (Map.Entry<String, String> entry : data.entrySet()) {
intent.putExtra(entry.getKey(), entry.getValue());
}
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
String channelId = getString(R.string.default_notification_channel_id); // 在 strings.xml 中定义
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ic_notification_custom) // 小图标,必须
.setContentTitle(title)
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent)
.setStyle(new NotificationCompat.BigTextStyle().bigText(messageBody)); // 让长文本能完整显示
if (image != null) {
notificationBuilder.setLargeIcon(image);
// 如果图片较大,且想作为通知的主要内容展示 (像微信聊天图片那样),可以使用 BigPictureStyle
// notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle()
// .bigPicture(image)
// .bigLargeIcon(null)); // 设置了 BigPictureStyle 后,大图标通常设为 null 或不设
}
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Android 8.0 (API level 26) 及以上版本需要通知渠道.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId,
"默认通知", // 用户可见的渠道名称
NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}
notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
}
}
说明和注意事项:
-
ic_notification_custom
: 这是一个必须提供的小图标,通常是应用 Logo 的单色剪影。放在res/drawable
目录下。 -
default_notification_channel_id
: 在res/values/strings.xml
中定义,例如:<resources> <string name="default_notification_channel_id">fcm_default_channel</string> </resources>
-
JSON 解析 : 上例中用了
org.json.JSONObject
。对于复杂 JSON,推荐使用 Gson 或 Moshi 库,会更健壮、方便。 -
图片下载 :
getBitmapFromUrl()
方法是一个非常基础的同步下载实现。在实际生产中,它会阻塞onMessageReceived
(虽然它不在主线程,但也不宜耗时过长)。强烈推荐使用 Glide、Picasso 或 Coil 等图片加载库 ,它们能处理缓存、异步加载、图片转换等,更高效也更稳定。例如用 Glide:// 伪代码,需要在合适的作用域和线程中执行 // Glide.with(applicationContext) // .asBitmap() // .load(imageUrl) // .into(new CustomTarget<Bitmap>() { // @Override // public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { // // 在这里用 resource (下载好的Bitmap) 更新通知 // sendNotification(title, parsedBodyText, resource, data); // } // @Override // public void onLoadCleared(@Nullable Drawable placeholder) { } // }); // 这引入了异步问题,因为 onMessageReceived 可能已经返回了。 // 一个常见的做法是 onMessageReceived 中先发一个基础通知,然后启动一个 JobIntentService 或 WorkManager 任务去下载图片并更新通知。 // 或者,如果下载时间可控(比如图片服务器很快,图片不大),简单同步下载有时也行,但要注意 ANR 风险。
更简单直接的、如果允许一点点延迟(例如图片非常小,或者有缓存机制时):
可以在getBitmapFromUrl
内使用 Glide 的同步加载 API(如果你的 Glide 配置支持,并且确认你是在后台线程中调用它):// private Bitmap getBitmapFromUrlWithGlide(String imageUrl) { // try { // return Glide.with(this) // .asBitmap() // .load(imageUrl) // .submit() // submit() returns a Future // .get(); // .get() blocks, so ensure this is not on main thread. // } catch (Exception e) { // Log.e(TAG, "Error downloading image with Glide: " + e.getMessage()); // return null; // } // }
实际项目中,由于
onMessageReceived
的生命周期限制(通常 10-20 秒内要处理完毕),处理图片下载这类耗时操作,更稳妥的方式是启动一个WorkManager
任务来完成下载和显示通知。 -
通知渠道 (Notification Channels) : Android 8.0 (API 26) 以上是必需的。用户可以在系统设置里管理应用的通知渠道。
4. 安全建议
- HTTPS : 确保你的图片 URL 使用 HTTPS,防止中间人攻击。
- 服务端校验 : 服务端在发送
notification_image_url
之前,应对 URL 进行校验,确保是合法的、可访问的图片资源地址,避免指向恶意内容或无效链接。 - 客户端数据校验 : 客户端在解析
notification_body_json
时,要做好异常处理,防止因为 JSON 格式错误或缺少字段导致应用崩溃。使用optString
,optInt
等方法会更安全。 - 资源大小限制 : 考虑通知图片的尺寸和文件大小。太大的图片下载耗时,也消耗用户流量,甚至可能导致下载失败或通知显示异常。服务端可以对图片进行预处理,提供适合通知展示的尺寸。
5. 进阶使用技巧
- 自定义通知布局 : 如果标准的通知样式无法满足需求,可以考虑使用
NotificationCompat.Builder
的setCustomContentView()
,setCustomBigContentView()
等方法,配合 RemoteViews 创建完全自定义的通知界面。但这会增加复杂性。 - 通知操作按钮 : 使用
addAction()
方法给通知添加快速操作按钮,例如“立即购买”、“查看详情”等。// 示例:添加一个“查看”按钮 Intent viewIntent = new Intent(this, DetailActivity.class); viewIntent.putExtra("product_id", "123"); // 假设从 data 中获取 product_id PendingIntent viewPendingIntent = PendingIntent.getActivity(this, 1, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); notificationBuilder.addAction(R.drawable.ic_action_view, "查看", viewPendingIntent);
- 分组通知 : 如果短时间内收到多条相关通知,可以使用
setGroup()
将它们聚合起来,提升用户体验。 - 深链接处理 : 在
PendingIntent
中配置的Intent
,可以携带参数,并结合intent-filter
实现深链接,直接打开 App 的特定页面。比如前面MainActivity
的intent.putExtra(entry.getKey(), entry.getValue());
就是为这个做准备的。在MainActivity
的onCreate
或onNewIntent
中可以获取这些数据:// 在 MainActivity.java 的 onCreate // String screen = getIntent().getStringExtra("screen"); // if (screen != null && screen.equals("/product/123")) { // // 导航到产品123的页面 // }
- 使用 WorkManager 处理图片下载 : 为了确保
onMessageReceived
不会因为图片下载超时而被系统终止,可以将图片下载和通知更新的任务交给WorkManager
。onMessageReceived
快速返回,启动一个OneTimeWorkRequest
。onMessageReceived
:保存通知数据,启动 Worker。ImageDownloaderWorker
:执行下载,下载完成后构建并显示通知。
通过这种“只发送数据消息”的策略,就能完全掌控 App 在后台或被杀死状态下 FCM 通知的行为,轻松实现 JSON Body 的解析和自定义图片的展示。代码可能多了点,但换来的是灵活度和最佳用户体验。