向多台设备发送消息

如需向多台设备发送消息,请使用主题消息传递。借助此功能,您可以将消息发送至已经选择订阅特定主题的多台设备。

本教程主要介绍如何使用适用于 FCMAdmin SDKREST API 从应用服务器发送主题消息,并在 Android 应用中接收和处理此类消息。内容涵盖后台应用和前台应用的消息处理方式。我们将介绍实现上述目标所需执行的步骤,包括从设置到验证的所有步骤。

设置 SDK

如果您已针对 FCM 设置 Android 客户端应用,或已经完成发送第一条消息所需执行的步骤,则本部分可能包含了您已经完成的步骤。

准备工作

  • 安装最新版本的 Android Studio,或更新为最新版本。

  • 请确保您的项目满足以下要求(请注意,某些产品可能具有更严格的要求):

    • 目标 API 级别为 21 (Lollipop) 或更高
    • 使用 Android 5.0 或更高版本
    • 使用 Jetpack (AndroidX),同时需要满足以下版本要求:
      • com.android.tools.build:gradle v7.3.0 或更高版本
      • compileSdkVersion 28 或更高版本
  • 设置一台实体设备或使用模拟器运行您的应用。
    请注意,依赖于 Google Play 服务的 Firebase SDK 要求设备或模拟器上必须安装 Google Play 服务。

  • 使用您的 Google 账号登录 Firebase

如果您还没有 Android 项目,只是想试用某一 Firebase 产品,可以下载一个我们的快速入门示例

创建 Firebase 项目

您必须先创建一个要关联到 Android 应用的 Firebase 项目,然后才能将 Firebase 添加到您的 Android 应用。如需详细了解 Firebase 项目,请访问了解 Firebase 项目

在 Firebase 中注册您的应用

如需在 Android 应用中使用 Firebase,您需要在 Firebase 项目中注册您的应用。注册应用的过程通常称为将应用“添加”到项目中。

  1. 前往 Firebase 控制台

  2. 在项目概览页面的中心位置,点击 Android 图标 () 或添加应用,启动设置工作流。

  3. Android 软件包名称字段中输入应用的软件包名称。

  4. (可选)输入其他应用信息:应用别名调试签名证书 SHA-1

  5. 点击注册应用

添加 Firebase 配置文件

  1. 下载 Firebase Android 配置文件 (google-services.json),然后将其添加到您的应用:

    1. 点击下载 google-services.json 以获取 Firebase Android 配置文件。

    2. 将配置文件移到应用的模块(应用级)根目录中。

  2. 为了确保 Firebase SDK 可以访问 google-services.json 配置文件中的值,您需要具有 Google 服务 Gradle 插件 (google-services)。

    1. 在您的根级(项目级)Gradle 文件(<project>/build.gradle.kts<project>/build.gradle)中,将 Google 服务插件添加为依赖项:

      Kotlin

      plugins {
        id("com.android.application") version "7.3.0" apply false
        // ...
      
        // Add the dependency for the Google services Gradle plugin
        id("com.google.gms.google-services") version "4.4.2" apply false
      }

      Groovy

      plugins {
        id 'com.android.application' version '7.3.0' apply false
        // ...
      
        // Add the dependency for the Google services Gradle plugin
        id 'com.google.gms.google-services' version '4.4.2' apply false
      }
    2. 在您的模块(应用级)Gradle 文件(通常是 <project>/<app-module>/build.gradle.kts<project>/<app-module>/build.gradle)中,添加 Google 服务插件:

      Kotlin

      plugins {
        id("com.android.application")
      
        // Add the Google services Gradle plugin
        id("com.google.gms.google-services")
        // ...
      }

      Groovy

      plugins {
        id 'com.android.application'
      
        // Add the Google services Gradle plugin
        id 'com.google.gms.google-services'
        // ...
      }

将 Firebase SDK 添加至您的应用

  1. 模块(应用级)Gradle 文件(通常是 <project>/<app-module>/build.gradle.kts<project>/<app-module>/build.gradle)中,添加 Firebase Cloud Messaging 库的依赖项。我们建议使用 Firebase Android BoM 来实现库版本控制。

    为了获得最佳的 Firebase Cloud Messaging 使用体验,我们建议您在 Firebase 项目中启用 Google Analytics,并将 Firebase SDK for Google Analytics 添加到您的应用中。

    dependencies {
        // Import the BoM for the Firebase platform
        implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
    
        // Add the dependencies for the Firebase Cloud Messaging and Analytics libraries
        // When using the BoM, you don't specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-messaging")
        implementation("com.google.firebase:firebase-analytics")
    }

    借助 Firebase Android BoM,可确保您的应用使用的始终是 Firebase Android 库的兼容版本。

    (替代方法) 在不使用 BoM 的情况下添加 Firebase 库依赖项

    如果您选择不使用 Firebase BoM,则必须在每个 Firebase 库的依赖项行中指定相应的库版本。

    请注意,如果您在应用中使用多个 Firebase 库,我们强烈建议您使用 BoM 来管理库版本,从而确保所有版本都兼容。

    dependencies {
        // Add the dependencies for the Firebase Cloud Messaging and Analytics libraries
        // When NOT using the BoM, you must specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-messaging:24.1.0")
        implementation("com.google.firebase:firebase-analytics:22.1.2")
    }
    是否想要查找 Kotlin 专用的库模块?2023 年 10 月 (Firebase BoM 32.5.0) 开始,Kotlin 和 Java 开发者可以依赖于主库模块(如需了解详情,请参阅关于此计划的常见问题解答)。

  2. 将您的 Android 项目与 Gradle 文件同步。

为客户端应用订阅主题

客户端应用可以订阅任何现有主题,也可以创建新主题。当客户端应用订阅新的主题名称(您的 Firebase 项目中尚不存在的主题)时,系统会在 FCM 中创建一个以该名称命名的新主题,此后任何客户端都可订阅该主题。

如需订阅某个主题,客户端应用需使用 FCM 主题名称调用 Firebase Cloud Messaging subscribeToTopic()。此方法会返回一个 Task,完成监听器可以使用它来确定订阅是否成功:

Kotlin

Firebase.messaging.subscribeToTopic("weather")
    .addOnCompleteListener { task ->
        var msg = "Subscribed"
        if (!task.isSuccessful) {
            msg = "Subscribe failed"
        }
        Log.d(TAG, msg)
        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
    }

Java

FirebaseMessaging.getInstance().subscribeToTopic("weather")
        .addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                String msg = "Subscribed";
                if (!task.isSuccessful()) {
                    msg = "Subscribe failed";
                }
                Log.d(TAG, msg);
                Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
            }
        });

如需退订,客户端应用需使用主题名称调用 Firebase Cloud Messaging unsubscribeFromTopic() 方法。

接收和处理主题消息

FCM 传送主题消息的方式与传送其他下行消息的方式相同。

如需接收消息,请使用扩展 FirebaseMessagingService 的服务。 您的服务应该重写 onMessageReceivedonDeletedMessages 回调方法。

处理消息的时间范围可能会短于 20 秒,具体取决于调用 onMessageReceived 之前发生的延迟,包括操作系统延迟、应用启动时间、主线程被其他操作阻止或先前的 onMessageReceived 调用耗时过长。这个时间段过后,各种操作系统行为(如 Android 的进程终止或 Android O 的后台执行限制)可能会影响您完成消息处理的能力。

对于大多数消息类型,您都可以使用 onMessageReceived,但以下情况除外:

  • 当应用在后台时送达的通知消息。在这种情况下,通知将传送至设备的系统任务栏。默认情况下,用户点按通知即可打开应用启动器。

  • 在后台接收的既包含通知又具有数据载荷的消息。 在这种情况下,通知将传送至设备的系统任务栏,数据载荷则传送至启动器 Activity 的 intent 的 extras 属性中。

总结:

应用状态 通知 数据 两者皆有
前台 onMessageReceived onMessageReceived onMessageReceived
背景 系统任务栏 onMessageReceived 通知:系统任务栏
数据:intent 的 extras 属性。
如需详细了解消息类型,请参阅通知和数据消息

修改应用清单

如需使用 FirebaseMessagingService,您需要将以下内容添加到您的应用清单:

<service
    android:name=".java.MyFirebaseMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

此外,建议您设置默认值以便自定义通知的外观。您可以指定自定义默认图标和颜色,当通知载荷中未设置相应的值时,系统会使用这些默认值。

将以下代码行添加到 application 标记内,以设置自定义默认图标和颜色:

<!-- Set custom default icon. This is used when no icon is set for incoming notification messages.
     See README(https://goo.gl/l4GJaQ) for more. -->
<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_stat_ic_notification" />
<!-- Set color used with incoming notification messages. This is used when no color is set for the incoming
     notification message. See README(https://goo.gl/6BKBk7) for more. -->
<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/colorAccent" />

对于以下消息,Android 会显示自定义的默认图标:

  • Notifications Composer 发送的所有通知信息。
  • 未在通知载荷中明确设置图标的通知消息。

对于以下消息,Android 会使用自定义的默认颜色:

  • Notifications Composer 发送的所有通知信息。
  • 未在通知载荷中明确设置颜色的通知消息。

如果未设置自定义默认图标,而且通知载荷中也未设置图标,Android 会显示以白色渲染的应用图标。

重写 onMessageReceived

通过重写 FirebaseMessagingService.onMessageReceived 方法,您可以根据收到的 RemoteMessage 对象执行操作并获取消息数据:

Kotlin

override fun onMessageReceived(remoteMessage: RemoteMessage) {
    // TODO(developer): Handle FCM messages here.
    // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
    Log.d(TAG, "From: ${remoteMessage.from}")

    // Check if message contains a data payload.
    if (remoteMessage.data.isNotEmpty()) {
        Log.d(TAG, "Message data payload: ${remoteMessage.data}")

        // Check if data needs to be processed by long running job
        if (needsToBeScheduled()) {
            // For long-running tasks (10 seconds or more) use WorkManager.
            scheduleJob()
        } else {
            // Handle message within 10 seconds
            handleNow()
        }
    }

    // Check if message contains a notification payload.
    remoteMessage.notification?.let {
        Log.d(TAG, "Message Notification Body: ${it.body}")
    }

    // Also if you intend on generating your own notifications as a result of a received FCM
    // message, here is where that should be initiated. See sendNotification method below.
}

Java

@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
    // TODO(developer): Handle FCM messages here.
    // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
    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());

        if (/* Check if data needs to be processed by long running job */ true) {
            // For long-running tasks (10 seconds or more) use WorkManager.
            scheduleJob();
        } else {
            // Handle message within 10 seconds
            handleNow();
        }

    }

    // Check if message contains a notification payload.
    if (remoteMessage.getNotification() != null) {
        Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
    }

    // Also if you intend on generating your own notifications as a result of a received FCM
    // message, here is where that should be initiated. See sendNotification method below.
}

重写 onDeletedMessages

在某些情况下,FCM 可能不会传递消息。比如说,如果在特定设备连接到 FCM 时,您的应用在该设备上的待处理消息过多(超过 100 条),或者设备超过一个月未连接到 FCM,就会发生这种情况。在这些情况下,您可能会收到对 FirebaseMessagingService.onDeletedMessages() 的回调。当应用实例收到此回调时,应该执行一次与应用服务器的完全同步。如果您在过去 4 周内未向该设备上的应用发送消息,FCM 将不会调用 onDeletedMessages()

在后台应用中处理通知消息

当您的应用在后台运行时,Android 会将通知消息传送至系统任务栏。默认情况下,用户点按通知即可打开应用启动器。

此类消息包括既具有通知也具有数据载荷的消息(以及所有从 Notifications 控制台发送的消息)。 在这些情况下,通知将传送至设备的系统任务栏,数据载荷则传送至启动器 Activity 的 intent 的 extras 属性。

如需详细了解发送到您应用的消息,请参阅 FCM 报告信息中心。该信息中心会记录在 Apple 和 Android 设备上发送和打开的消息数量,以及 Android 应用的“展示次数”(用户看到的通知条数)数据。

构建发送请求

创建主题后,您可以通过两种方法向主题发送消息:一种是在客户端为客户端应用实例订阅该主题,另一种是使用服务器 API。如果您是首次构建 FCM 发送请求,请参阅关于您的服务器环境和 FCM 的指南,了解重要的背景信息和设置信息。

在后端的发送逻辑中,按如下所述的方式指定所需的主题名称:

Node.js

// The topic name can be optionally prefixed with "/topics/".
const topic = 'highScores';

const message = {
  data: {
    score: '850',
    time: '2:45'
  },
  topic: topic
};

// Send a message to devices subscribed to the provided topic.
getMessaging().send(message)
  .then((response) => {
    // Response is a message ID string.
    console.log('Successfully sent message:', response);
  })
  .catch((error) => {
    console.log('Error sending message:', error);
  });

Java

// The topic name can be optionally prefixed with "/topics/".
String topic = "highScores";

// See documentation on defining a message payload.
Message message = Message.builder()
    .putData("score", "850")
    .putData("time", "2:45")
    .setTopic(topic)
    .build();

// Send a message to the devices subscribed to the provided topic.
String response = FirebaseMessaging.getInstance().send(message);
// Response is a message ID string.
System.out.println("Successfully sent message: " + response);

Python

# The topic name can be optionally prefixed with "/topics/".
topic = 'highScores'

# See documentation on defining a message payload.
message = messaging.Message(
    data={
        'score': '850',
        'time': '2:45',
    },
    topic=topic,
)

# Send a message to the devices subscribed to the provided topic.
response = messaging.send(message)
# Response is a message ID string.
print('Successfully sent message:', response)

Go

// The topic name can be optionally prefixed with "/topics/".
topic := "highScores"

// See documentation on defining a message payload.
message := &messaging.Message{
	Data: map[string]string{
		"score": "850",
		"time":  "2:45",
	},
	Topic: topic,
}

// Send a message to the devices subscribed to the provided topic.
response, err := client.Send(ctx, message)
if err != nil {
	log.Fatalln(err)
}
// Response is a message ID string.
fmt.Println("Successfully sent message:", response)

C#

// The topic name can be optionally prefixed with "/topics/".
var topic = "highScores";

// See documentation on defining a message payload.
var message = new Message()
{
    Data = new Dictionary<string, string>()
    {
        { "score", "850" },
        { "time", "2:45" },
    },
    Topic = topic,
};

// Send a message to the devices subscribed to the provided topic.
string response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
// Response is a message ID string.
Console.WriteLine("Successfully sent message: " + response);

REST

POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1

Content-Type: application/json
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA
{
  "message":{
    "topic" : "foo-bar",
    "notification" : {
      "body" : "This is a Firebase Cloud Messaging Topic Message!",
      "title" : "FCM Message"
      }
   }
}

cURL 命令:

curl -X POST -H "Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA" -H "Content-Type: application/json" -d '{
  "message": {
    "topic" : "foo-bar",
    "notification": {
      "body": "This is a Firebase Cloud Messaging Topic Message!",
      "title": "FCM Message"
    }
  }
}' https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1

如需向主题组合发送消息,请指定一个条件,该条件是一个指定目标主题的布尔表达式。例如,如需向既订阅了 TopicA,又订阅了 TopicBTopicC 的设备发送消息,请设置如下条件:

"'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)"

FCM 首先对括号中的所有条件求值,然后从左至右对表达式求值。在上述表达式中,只订阅了其中某个主题的用户将不会接收到消息。同样地,未订阅 TopicA 的用户也不会接收到消息。订阅下列组合的用户才会接收到消息:

  • TopicATopicB
  • TopicATopicC

您最多可以在条件表达式中包含五个主题。

根据条件发送消息:

Node.js

// Define a condition which will send to devices which are subscribed
// to either the Google stock or the tech industry topics.
const condition = '\'stock-GOOG\' in topics || \'industry-tech\' in topics';

// See documentation on defining a message payload.
const message = {
  notification: {
    title: '$FooCorp up 1.43% on the day',
    body: '$FooCorp gained 11.80 points to close at 835.67, up 1.43% on the day.'
  },
  condition: condition
};

// Send a message to devices subscribed to the combination of topics
// specified by the provided condition.
getMessaging().send(message)
  .then((response) => {
    // Response is a message ID string.
    console.log('Successfully sent message:', response);
  })
  .catch((error) => {
    console.log('Error sending message:', error);
  });

Java

// Define a condition which will send to devices which are subscribed
// to either the Google stock or the tech industry topics.
String condition = "'stock-GOOG' in topics || 'industry-tech' in topics";

// See documentation on defining a message payload.
Message message = Message.builder()
    .setNotification(Notification.builder()
        .setTitle("$GOOG up 1.43% on the day")
        .setBody("$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.")
        .build())
    .setCondition(condition)
    .build();

// Send a message to devices subscribed to the combination of topics
// specified by the provided condition.
String response = FirebaseMessaging.getInstance().send(message);
// Response is a message ID string.
System.out.println("Successfully sent message: " + response);

Python

# Define a condition which will send to devices which are subscribed
# to either the Google stock or the tech industry topics.
condition = "'stock-GOOG' in topics || 'industry-tech' in topics"

# See documentation on defining a message payload.
message = messaging.Message(
    notification=messaging.Notification(
        title='$GOOG up 1.43% on the day',
        body='$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.',
    ),
    condition=condition,
)

# Send a message to devices subscribed to the combination of topics
# specified by the provided condition.
response = messaging.send(message)
# Response is a message ID string.
print('Successfully sent message:', response)

Go

// Define a condition which will send to devices which are subscribed
// to either the Google stock or the tech industry topics.
condition := "'stock-GOOG' in topics || 'industry-tech' in topics"

// See documentation on defining a message payload.
message := &messaging.Message{
	Data: map[string]string{
		"score": "850",
		"time":  "2:45",
	},
	Condition: condition,
}

// Send a message to devices subscribed to the combination of topics
// specified by the provided condition.
response, err := client.Send(ctx, message)
if err != nil {
	log.Fatalln(err)
}
// Response is a message ID string.
fmt.Println("Successfully sent message:", response)

C#

// Define a condition which will send to devices which are subscribed
// to either the Google stock or the tech industry topics.
var condition = "'stock-GOOG' in topics || 'industry-tech' in topics";

// See documentation on defining a message payload.
var message = new Message()
{
    Notification = new Notification()
    {
        Title = "$GOOG up 1.43% on the day",
        Body = "$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.",
    },
    Condition = condition,
};

// Send a message to devices subscribed to the combination of topics
// specified by the provided condition.
string response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
// Response is a message ID string.
Console.WriteLine("Successfully sent message: " + response);

REST

POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1

Content-Type: application/json
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA
{
   "message":{
    "condition": "'dogs' in topics || 'cats' in topics",
    "notification" : {
      "body" : "This is a Firebase Cloud Messaging Topic Message!",
      "title" : "FCM Message",
    }
  }
}

cURL 命令:

curl -X POST -H "Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA" -H "Content-Type: application/json" -d '{
  "notification": {
    "title": "FCM Message",
    "body": "This is a Firebase Cloud Messaging Topic Message!",
  },
  "condition": "'dogs' in topics || 'cats' in topics"
}' https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1

后续步骤