Firebase 推送教程

Firebase 云信息传递 (FCM) 是 Google 官方提供的一种跨平台消息传递解决方案,可供免费、可靠地传递消息。

准备工作

  1. 注册成为 Firebase 开发者
    Firebase Console 注册账号并登陆。
  2. 添加项目
    点击 “添加项目”,填写项目名称和国家 / 地区。添加完成后进入项目。
  3. 添加应用
    在此只演示安卓系统,点击 “Overview 概览” 页面的 “将 Firebase 添加到您的 Android 应用” 按钮。填写 Android 应用包名(不可更改),别名(选填),调试签名证书 SHA1(可选)。
  4. 下载配置文件
    点击 “注册应用”,下载配置文件 “google-services.json”。(此配置文件也可以在 Overview 的右边设置中的 “项目设置” 中查看、下载。)
    在 Android Studio 中切换到项目视图,查看项目根目录(即 app 目录),并将配置文件放入到此根目录下。
    修改 Gradle:项目级 build.gradle(<project>/build.gradle):
    1
    2
    3
    4
    5
    6
    buildscript {
    dependencies {
    // Add this line
    classpath 'com.google.gms:google-services:3.1.0'
    }
    }

    应用级 build.gradle(<project>/<app-module>/build.gradle):
    1
    2
    3
    ...
    // Add to the bottom of the file (一定要放最后)
    apply plugin: 'com.google.gms.google-services'

    按 IDE 中显示的栏中的 “立即同步”(Sync now)。

服务器端

本文使用 Python 作为服务器脚本语言。
PyFCM 源码:PyFCM

环境配置

pip 方式:pip install pyfcm

服务端主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from pyfcm import FCMNotification

push_service = FCMNotification(api_key="<api-key>")

# 或者设置一个代理
# proxy_dict = {
"http" : "http://127.0.0.1",
"https" : "http://127.0.0.1",
}
# push_service = FCMNotification(api_key="<api-key>", proxy_dict=proxy_dict)

# api-key可以从https://console.firebase.google.com/project/<project-name>/settings/cloudmessaging获得,即firebase console→对应项目→设置→云消息传递→服务器密钥。

message_title = "xxxxx" #通知的标题
message_body = "xxxxx" #通知的内容
data_message = {
"a" : "b",
"c" : "d",
"e" : "f"
} # 数据消息,为键值对

# 如果是发送给单台设备。
registration_id = "<device registration_id>" #Android应用的token
# 发送通知
result = push_service.notify_single_device(registration_id=registration_id, message_title=message_title, message_body=message_body)
# 发送数据
result = push_service.single_device_data_message(registration_id=registration_id, data_message=data_message)
# 发送带有数据的通知
result = push_service.notify_single_device(registration_id=registration_id, message_body=message_body, data_message=data_message)

# 如果是发送给多台设备消息通知。
registration_ids = ["<device registration_id 1>", "<device registration_id 2>", ...]
# 发送通知
result = push_service.notify_multiple_devices(registration_ids=registration_ids, message_title=message_title, message_body=message_body)
# 发送数据
result = push_service.multiple_devices_data_message(registration_ids=registration_ids, data_message=data_message)
# 发送带有数据的通知
result = push_service.notify_multiple_devices(registration_ids=registration_ids, message_body=message_body, data_message=data_message)

# 其他配置
# 发送一个高优先级的数据
extra_kwargs = {
'priority': 'high'
}
result = push_service.notify_single_device(registration_id=registration_id, data_message=data_message, extra_kwargs=extra_kwargs)
# 发送一个低优先级的数据
# low_priority默认为False
result = push_service.notify_multiple_devices(registration_ids=registration_ids, message_body=message, low_priority=True)

# 获得有效的registration_ids(可以用于清洗数据库中有效的registration_ids)
registration_ids = ['reg id 1', 'reg id 2', 'reg id 3', 'reg id 4', ...]
valid_registration_ids = push_service.clean_registration_ids(registration_ids)

# 发送消息至主题
result = push_service.notify_topic_subscribers(topic_name="xxx", message_body=message)
# 有条件的发送消息至主题,如果用户订阅了TopicA和TopicB或者TopicA和TopicC则能收到消息。
topic_condition = "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)"
result = push_service.notify_topic_subscribers(message_body=message, condition=topic_condition)

# 其他可选配置
collapse_key (str): 设置折叠消息
delay_while_idle (bool): 如果设置为真则表示消息将不会送达直到设备被激活。
time_to_live (int): 消息将会被保留多少秒,最多保留4周,默认值也是4周。
restricted_package_name (str): 应用包名必须匹配才收的到消息。
dry_run (bool): 如果设置为真则表示没有消息将会被送达,但是请求还是会被测试。

# 返回数据
response_dict = {
'multicast_ids': list(), #多播消息的唯一ID
'success': 0, #没有报错的消息数目
'failure': 0, #没有处理的消息数目
'canonical_ids': 0, #包括标准registration token的结果数目
'results': list(), #表示消息状态的字典
'topic_message_id': None or str
}

Android 上的主题消息传递

在发布 / 订阅模式下,利用 FCM 主题消息传递功能,可以将消息发送至已经选择加入特定主题的多台设备。根据需要撰写主题消息,FCM 将处理消息路由并将消息可靠地传送至正确的设备。
关于主题,请注意以下事项:

  1. 主题消息传递不限制每个应用拥有的主题和订阅数。
  2. 目前,主题消息的有效负载不得超过 2KB。
  3. 主题消息传递最适合传递新闻、天气或其他可通过公开途径获得的信息等内容。
  4. 主题消息针对吞吐量(而非延迟)进行了优化。要将消息快速安全地传送到单台设备或小规模设备组,应将消息定位至注册令牌,而非主题。
  5. 如果需要向一位用户的多台设备发送消息,可考虑针对这些使用情形进行设备组消息传递。

设置消息的优先级

  • 普通优先级:这是数据消息的默认优先级。普通优先级消息不会让休眠设备打开网络连接,为了省电,它们可能会被延迟传递。如果是对时间不太敏感的消息,例如新电子邮件通知或其他要同步的数据,建议选择普通传递优先级。
  • 高优先级:这是通知消息的默认优先级。FCM 会立即尝试传递高优先级消息,允许 FCM 服务在可能的情况下唤醒休眠设备并打开与应用服务器的网络连接。例如,带有即时通讯、聊天或语音通话提醒功能的应用通常需要打开网络连接并确保 FCM 及时将消息传递给设备。如果消息属于时间关键型且需要用户立刻交互,请设置高优先级,但需要注意的是,将消息设置为高优先级会比普通优先级耗费更多电池电量。
    有效值为 normal 和 high。

不可折叠消息和可折叠消息

不可折叠消息表示每一条消息都将被传递至设备。不可折叠消息可传递一些有用内容至手机应用,从而联系服务器以获取数据,这与 “ping” 相反。默认情况下,消息不可折叠,但通知消息(始终是可折叠消息)除外。
聊天消息或关键消息都是典型的不可折叠消息。例如,在 IM 应用中,可能想要传递每一条消息,因为它们的内容各不相同。
在不折叠的情况下,最多可存储 100 条消息。达到此限值后,所有存储的消息都将被舍弃。设备在重新联网后将收到一条特殊消息,提示已达到此上限。之后,应用可以正常处理该状况,一般情况下会请求与应用服务器完全同步。
可折叠消息在还未被传递至设备的情况下可能会被新消息替代。
两种常见的可折叠消息是 “发送以同步” 消息和通知消息。“发送以同步” 消息是一个 “ping”,它会告诉移动应用从服务器同步数据。为用户更新最新比分的体育应用就属于这种消息。只有最新的消息是相关的。
要将消息标记为可折叠,请在消息有效负载中添加 collapse_key 参数。FCM 允许应用服务器在任意指定时间内为每台设备使用最多 4 个不同的折叠键。也就是说,FCM 连接服务器可以为每台设备同时存储 4 条不同的可折叠 “发送以同步” 消息,每一条都含有不同的折叠键。如果超出此限值,FCM 将仅保留 4 个折叠键,具体保留哪几个不一定。

Android 客户端

官方集成文档:在 Android 上设置 Firebase 云消息传递客户端应用

将 Firebase 添加到 Android 项目

前提条件:

  1. 运行 Android 4.0 (Ice Cream Sandwich) 或更高版本以及 Google Play 服务 11.0.4 或更高版本的设备。
  2. Google 代码库中的 Google Play Services SDK,可通过 Android SDK Manager 获得。
  3. 最新版本的 Android Studio,1.5 版或更高版本。

如果使用最新版本的 Android Studio(2.2 版或更高版本),建议使用 Firebase 智能助理来将的应用关联至 Firebase。Firebase 智能助理可以关联现有的项目,或者为创建一个新项目,并自动安装任何必要的 Gradle 依赖项。

  1. 依次点击 Tools>Firebase 以打开 Assistant 窗口。
  2. 点击以展开所列功能之一(例如 Cloud Messaging),然后点击所提供的教程链接。
  3. 点击 Connect to Firebase 按钮以关联至 Firebase,并向应用添加必要的代码。

添加所需的依赖

  1. 向根项目级 build.gradle 文件添加规则,以纳入 Google 服务插件:
    1
    2
    3
    4
    5
    6
    7
    buildscript {
    // ...
    dependencies {
    // ...
    classpath 'com.google.gms:google-services:3.1.0'
    }
    }
  2. 在模块 Gradle 文件(通常是 app/build.gradle)中,在文件的底部添加 apply plugin 代码行,以启用 Gradle 插件:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    apply plugin: 'com.android.application'
    android {
    // ...
    }
    dependencies {
    // ...
    compile 'com.google.firebase:firebase-messaging:11.4.2' #修改为最新版本
    compile 'com.google.android.gms:play-services-base:11.4.2' #修改为最新版本
    }
    // 一定要放在最底部
    apply plugin: 'com.google.gms.google-services'
  3. 如果 FCM 对于 Android 应用的功能至关重要,请务必在应用的 build.gradle 中设置 minSdkVersion 8 或更高版本。这可确保 Android 应用无法安装在不能让其正常运行的环境中。

修改应用清单 AndroidManifest.xml

  1. 一项继承 FirebaseMessagingService 的服务。如果希望在后台进行除接收应用通知之外的消息处理,则必须添加此服务。要接收前台应用中的通知、接收数据有效负载以及发送上行消息等,必须继承此服务。
    1
    2
    3
    4
    5
    <service android:name=".MyFirebaseMessagingService">
    <intent-filter>
    <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
    </service>
  2. 一项继承 FirebaseInstanceIdService 的服务,用于处理注册令牌的创建、轮替和更新。如果要发送至特定设备或者创建设备组,则必须添加此服务。
    1
    2
    3
    4
    5
    <service android:name=".MyFirebaseInstanceIDService">
    <intent-filter>
    <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
    </intent-filter>
    </service>
  3. (可选)在应用组件中,用于设置通知的默认图标、颜色和通知渠道(Android O 的新功能)的元数据元素。如果传入的消息未明确设置图标、颜色或通知渠道,则 Android 会使用这些值。
    1
    2
    3
    4
    5
    6
    <meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_stat_ic_notification" />
    <meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/colorAccent" />

获取设备注册令牌

初次启动应用时,FCM SDK 会为客户端应用实例生成一个注册令牌。如果希望定位至单台设备或创建设备组,则需要通过继承 FirebaseInstanceIdService 来访问此令牌。
令牌会在初始启动后生成,建议检索最新更新的注册令牌。
注册令牌可能会在发生下列情况时更改:
- 应用删除实例 ID
- 应用在新设备上恢复
- 用户卸载 / 重新安装应用
- 用户清除应用数据

  1. 检索当前注册令牌
    如果需要检索当前令牌,调用 FirebaseInstanceId.getInstance ().getToken ()。如果令牌尚未生成,此方法将返回 null。
  2. 监控令牌的生成
    每次生成新的令牌时,都会触发 onTokenRefresh 回调,因此,在上下文中调用 getToken 可以确保访问的是当前可用的注册令牌。确保已将服务添加到清单文件中,然后在 onTokenRefresh 的上下文中调用 getToken,并记录相应值,如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    @Override
    public void onTokenRefresh() {
    // 获得注册令牌
    String refreshedToken = FirebaseInstanceId.getInstance().getToken();
    Log.d(TAG, "Refreshed token: " + refreshedToken);
    // 后续操作,如发送令牌到服务器
    sendRegistrationToServer(refreshedToken);
    }

    获取该令牌后,可以将其发送到应用服务器并进行存储。

检查 Google Play 服务

依靠 Play 服务 SDK 运行的应用在访问 Google Play 服务功能之前,应始终检查设备是否拥有兼容的 Google Play 服务 APK。建议在以下两个位置进行检查:主 Activity 的 onCreate () 方法中,及其 onResume () 方法中。在 onCreate () 中检查可确保该应用在检查成功之前无法使用。在 onResume () 中检查可确保当用户通过一些其他方式返回正在运行的应用(比如通过返回按钮)时,检查仍将继续进行。
如果设备没有兼容的 Google Play 服务版本,应用可以调用 GoogleApiAvailability.makeGooglePlayServicesAvailable(this),以便让用户从 Play 商店下载 Google Play 服务,需要 import com.google.android.gms.common.GoogleApiAvailability;

为客户端应用订阅主题

客户端应用可以订阅任何现有主题,也可创建新主题。当客户端应用订阅新的主题名称(Firebase 项目中尚不存在的名称)时,系统会在 FCM 中创建使用这个名称的新主题,随后任何客户端都可订阅该主题。
若要订阅某个主题,客户端应用需使用 FCM 主题名称调用 Firebase 云消息传递 subscribeToTopic ():FirebaseMessaging.getInstance().subscribeToTopic("news");
若要退订,客户端应用需使用主题名称调用 Firebase 云消息传递 unsubscribeFromTopic ()。

处理消息

要接收消息,使用继承 FirebaseMessagingService 的服务。重写 onMessageReceived 和 onDeletedMessages 回调。
在收到消息后的 10 秒内处理该消息。超过 10 秒后,Android 无法保证能够执行,并可能随时终止进程。如果应用需要更多时间来处理消息,使用 Firebase Job Dispatcher。
onMessageReceived 是为大多数消息类型提供的,但有以下例外情况:当应用在后台时送达的通知消息。在这种情况下,通知将传送至设备的系统任务栏。默认情况下,用户点按通知即可打开应用启动器。同时具备通知和数据有效负载的消息,无论应用在前台还是后台。在这种情况下,通知将传送至设备的系统任务栏,数据有效负载则传送至启动器 Activity 的 intent 的 extras 参数中。

应用状态 通知 数据 两者
前台 onMessageReceived onMessageReceived onMessageReceived
后台 系统任务栏 onMessageReceived 通知:系统任务栏
数据:intent 的 extras 参数。

注意:当需要发送通知(和数据)但是又希望不需要用户打开通知消息的情况下进行其他操作,则可以只发送数据,但是在 onMessageReceived 函数中手动生成 notification。

重写 onMessageReceived

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Log.d(TAG, "From: " + remoteMessage.getFrom());

// 检查是否收到数据
if (remoteMessage.getData().size() > 0) {
Log.d(TAG, "Message data payload: " + remoteMessage.getData());
if (/*如果数据处理超过10秒*/ true) {
scheduleJob();
} else {
// 如果数据处理在10秒以内
handleNow();
}
}

// 检查数据是否携带通知
if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
}
}

如果服务器只发送数据,但是需要应用主动生成通知则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class MyFirebaseMessagingService extends FirebaseMessagingService {

private static final String TAG = "MyFirebaseMsgService";
Map<String, String> messageBody;

@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
if (remoteMessage.getData().size() > 0) {
Log.d(TAG, "Message data payload: " + remoteMessage.getData());
messageBody = remoteMessage.getData();
sendNotification(messageBody.get("xxx")); # 这里messageBody类似json格式,messageBody.get("xxx")获得key为xxx的值。
}
}

private void sendNotification(String messageBody) {
Intent intent = new Intent(this, <Activity>.class); # 这里指定用户点击通知后所需打开的activity
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, "default")
.setSmallIcon(R.drawable.ic_stat_ic_notification)
.setContentTitle("Typing-Proof")
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent);
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager != null) {
notificationManager.notify(0, notificationBuilder.build());
}
}

重写 onDeletedMessages

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

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

当应用位于后台时,Android 会将通知消息转发至系统任务栏。默认情况下,用户点按通知时将打开应用启动器。
这包括同时含有通知和数据有效负载的消息(以及从通知控制台发送的所有消息)。
在这些情况下,通知将传送至设备的系统任务栏,数据有效负载则传送至启动器 Activity 的 intent 的 extras 参数中。