推送 Firebase Cloud Messaging
完成 Firebase Cloud Messaging 接入、Token 管理与刷新、前台/后台消息处理、notification 与 data 消息区分、Topic 订阅等核心推送能力
学习目标
- 完成 FCM SDK 接入与 google-services.json 配置
- 实现 FirebaseMessagingService 处理前台与后台消息
- 管理注册 Token 的获取、刷新与上报后端
- 区分 notification 与 data 两种消息类型的处理路径
- 使用 Topic 订阅实现分群推送
学习目标
FCM(Firebase Cloud Messaging)是 Google 提供的免费跨平台推送服务,是除华为/小米等厂商通道外最常用的海外推送方案。本章目标:
- 完成 FCM SDK 接入与
google-services.json配置; - 自定义
FirebaseMessagingService处理前台与后台消息; - 管理注册 Token 的获取、刷新与上报后端;
- 区分 notification 与 data 两种消息的处理路径;
- 使用 Topic 订阅实现分群推送。
FCM 接入准备
1. 创建 Firebase 项目
- 访问 Firebase 控制台;
- 新建 Project,添加 Android 应用,填写包名(如
com.example.myapp); - 下载
google-services.json文件并放入项目的app/目录; - 在控制台 → 项目设置 → 云消息传递 中获取 Server Key(旧版)或配置 OAuth2 服务账号(新版推荐)。
2. Gradle 配置
// 项目根目录 build.gradle
plugins {
id 'com.android.application' version '8.5.0' apply false
id 'com.google.gms.google-services' version '4.4.2' apply false
}
// app/build.gradle
plugins {
id 'com.android.application'
id 'com.google.gms.google-services'
}
android {
namespace 'com.example.myapp'
compileSdk 34
defaultConfig {
applicationId "com.example.myapp"
minSdk 21
targetSdk 34
}
}
dependencies {
// 使用 Firebase BoM 统一版本
implementation platform('com.google.firebase:firebase-bom:33.4.0')
implementation 'com.google.firebase:firebase-messaging'
// 可选:Firebase Analytics 用于统计推送转化
implementation 'com.google.firebase:firebase-analytics'
}
3. google-services.json 放置
将控制台下载的 google-services.json 放到 app/ 目录下,与 build.gradle 同级。google-services 插件会在编译期把它解析成 firebase_options.xml 等资源。
4. Manifest 声明 Service
<!-- AndroidManifest.xml -->
<application ...>
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- 可选:自定义默认通知图标 -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/purple_500" />
</application>
FirebaseMessagingService 实现
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private static final String TAG = "FCM";
private static final String CHANNEL_ID = "channel_push";
@Override
public void onNewToken(@NonNull String token) {
Log.d(TAG, "Refreshed token: " + token);
sendTokenToServer(token);
}
@Override
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
Log.d(TAG, "From: " + remoteMessage.getFrom());
// 1. 处理 notification 负载(仅前台时才会回调)
if (remoteMessage.getNotification() != null) {
String title = remoteMessage.getNotification().getTitle();
String body = remoteMessage.getNotification().getBody();
showNotification(title, body);
}
// 2. 处理 data 负载(无论前后台都会回调)
if (!remoteMessage.getData().isEmpty()) {
Map<String, String> data = remoteMessage.getData();
String type = data.get("type");
String id = data.get("id");
handleDataMessage(type, id);
}
}
private void sendTokenToServer(String token) {
// 上报到自己的后端
ApiClient.getInstance().uploadFcmToken(token);
}
private void showNotification(String title, String body) {
ensureChannel();
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title != null ? title : "新消息")
.setContentText(body)
.setAutoCancel(true)
.setContentIntent(pi)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build();
NotificationManagerCompat.from(this).notify(9001, notification);
}
private void ensureChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
"推送通知", NotificationManager.IMPORTANCE_HIGH);
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm != null) {
nm.createNotificationChannel(channel);
}
}
}
private void handleDataMessage(String type, String id) {
// 根据 type 决定跳转 / 拉取 / 缓存等
Intent intent = new Intent("com.example.myapp.PUSH_RECEIVED");
intent.putExtra("type", type);
intent.putExtra("id", id);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
}
Token 获取与刷新
1. 获取当前 Token
public class MainActivity extends AppCompatActivity {
private static final String TAG = "FCM_TOKEN";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fetchFcmToken();
}
private void fetchFcmToken() {
FirebaseMessaging.getInstance().getToken()
.addOnCompleteListener(task -> {
if (!task.isSuccessful()) {
Log.w(TAG, "Fetching FCM token failed", task.getException());
return;
}
String token = task.getResult();
Log.d(TAG, "Token: " + token);
ApiClient.getInstance().uploadFcmToken(token);
});
}
}
2. 监听 Token 刷新
Token 在以下场景会刷新:
- App 在新设备上恢复数据;
- 用户卸载/重装 App;
- 用户清除 Google Play Services 数据;
- App 升级改变了
FirebaseApp初始化配置。
只需在 MyFirebaseMessagingService.onNewToken 中处理即可,系统会自动回调。
3. 后端推送示例(参考)
服务端通过 HTTP v1 API 推送:
POST https://fcm.googleapis.com/v1/projects/<project_id>/messages:send
Authorization: Bearer <OAuth2 access_token>
Content-Type: application/json
{
"message": {
"token": "<device_token>",
"notification": {
"title": "Hello",
"body": "World"
},
"data": {
"type": "promotion",
"id": "p_001"
}
}
}
前台消息与后台消息
1. 前台消息
App 处于前台时,所有消息(含 notification 类型)都会回调 onMessageReceived,由 App 自行展示通知。这是 FCM 给开发者最大的灵活度,可自定义 UI 提示而非走系统通知。
2. 后台消息
- 仅 notification:系统托盘自动展示,不会回调
onMessageReceived;点击通知会启动默认 Launcher Activity 并把RemoteMessage数据放进Intent.extras。 - 含 data:若后台且包含 data 字段,系统托盘同样自动展示 notification 部分;用户点击后才会回调
onMessageReceived或通过Intentextras 传递到启动 Activity。 - 纯 data 后台消息:在后台时 不会 立刻回调
onMessageReceived,需要等用户点击通知后由系统在启动 Activity 时传递;若想后台也立刻处理,必须把它做成 高优先级 data 消息 并保持为前台/后台但服务存在。
| 消息类型 | 前台 | 后台 |
|---|---|---|
| 纯 notification | onMessageReceived |
系统托盘自动显示 |
| notification + data | onMessageReceived |
系统托盘显示 notification;点击后 data 通过 Intent extras 传递 |
| 纯 data | onMessageReceived |
高优先级才会立刻回调;普通会被延迟到下次 App 前台 |
消息类型:notification vs data
1. notification 消息
{
"message": {
"token": "...",
"notification": {
"title": "促销活动",
"body": "全场 5 折"
}
}
}
特点:
- 由 FCM SDK 自动展示(后台时);
- 不可自定义通知样式(前台可拦截);
- 适合“只需展示”的简单消息。
2. data 消息
{
"message": {
"token": "...",
"data": {
"type": "chat_message",
"id": "msg_9283",
"title": "Alice",
"body": "在吗?"
},
"android": {
"priority": "high"
}
}
}
特点:
- 由
onMessageReceived全权处理; - 可自定义通知样式、跳转、缓存等;
- 适合“业务化”的推送(IM、订单、运动数据)。
3. 推荐做法
- IM/订单等业务消息用 纯 data + high priority;
- 营销类简单通知用 notification + data(让系统托盘兜底展示,data 用于点击跳转)。
Topic 订阅
1. 客户端订阅
// 订阅"科技"主题
FirebaseMessaging.getInstance().subscribeToTopic("tech_news")
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Log.d("FCM", "Subscribed to tech_news");
} else {
Log.w("FCM", "Subscribe failed", task.getException());
}
});
// 取消订阅
FirebaseMessaging.getInstance().unsubscribeFromTopic("tech_news");
2. 后端向 Topic 推送
{
"message": {
"topic": "tech_news",
"notification": {
"title": "科技快讯",
"body": "Android 15 正式发布"
}
}
}
3. 条件订阅(如“住在上海且订阅科技”)
{
"message": {
"topic": "shanghai AND tech_news",
"data": { "type": "local" }
}
}
Android 13+ 通知权限
FCM 后台消息展示系统通知也需要 POST_NOTIFICATIONS:
private final ActivityResultLauncher<String> mRequestNoti =
registerForActivityResult(new ActivityResultContracts.RequestPermission(),
granted -> {
if (granted) {
fetchFcmToken();
} else {
Toast.makeText(this, "通知权限被拒绝,部分推送将无法显示",
Toast.LENGTH_SHORT).show();
}
});
private void ensureNotificationPermissionThenFetchToken() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
mRequestNoti.launch(Manifest.permission.POST_NOTIFICATIONS);
return;
}
}
fetchFcmToken();
}
国内厂商通道补充
在国内市场,FCM 因为依赖 Google Play Services 在大部分国行机型不可用。生产环境通常会接入华为、小米、OPPO、VIVO、魅族等厂商推送:
- 上报 Token 时一并上报厂商 Token 与品牌;
- 服务端按品牌分发:华为用 HMS Push、小米用 MiPush;
- 通用业务消息通道用统一封装(如极光、个推、友盟 Push)。
常见坑与最佳实践
google-services.json放错位置:必须放在app/模块根目录,不是项目根目录,否则插件找不到。- 包名不一致:Firebase 控制台注册的包名必须与
applicationId完全一致(含 build flavor 后缀),否则getToken返回空。 - 后台 data 消息被延迟:必须设置
android.priority = "high"才能立刻回调,普通优先级会被合并延迟。 - 在
onMessageReceived中主线程耗时操作:会被系统杀掉。建议把数据写入数据库或转交 WorkManager。 onNewToken上报失败没有重试:Token 刷新后不上报后端,老 Token 失效,推送丢失。建议本地缓存并做重试队列。PendingIntent未指定FLAG_IMMUTABLE:Android 12+ 崩溃,FCM 自身构建的通知在 SDK 升级后才修复,自定义构建通知务必自己加。- Topic 订阅过多:每个 App 默认无订阅上限,但建议只订阅与用户强相关的主题,避免大量无效推送。
- DOZE 模式下的高优先级限制:Android 6+ Doze 模式下,FCM 高优先级消息有每日配额(Firebase 后台限流),过多会自动降级。
- 未配置
default_notification_icon:后台通知会显示默认 Firebase 图标,体验割裂。
章节小结
- FCM 接入需
google-services.json+firebase-bom依赖 + Manifest 注册 Service; FirebaseMessagingService.onMessageReceived处理前台消息,onNewToken监听 Token 刷新;- notification 消息由系统托盘展示,data 消息由 App 自行处理,建议业务消息用 data;
- Topic 订阅适合分群推送,客户端订阅后端按 topic 推送;
- Android 13+ 通知权限申请、国内厂商通道补充,是 FCM 落地到生产的关键补充。
下一章预告
第 17 章将进入 Service 与后台任务:Service 类型(Started / Bound)、startService 与 bindService、前台服务(startForeground)、onStartCommand 返回值、Android 8+ 后台执行限制与保活替代方案。