Android APP集成FCM介绍

1. 集成需求介绍

安卓系统的APP,如果涉及推送系统,一般会基于Socket在后台维持一个与APP服务器通信的长连接。但是这个长连接不稳定,当APP退到后台运行以后,系统会关闭APP并收回分配的资源,Socket长连接会被断掉,从而使APP失去与服务器的通信联系。

APP与服务器的通信断开之后,APP就不再能收到服务器发出的通知,导致用户不能及时收到相应的通知而错失及时处理问题的机会。

类似苹果推送,在安卓手系统上,谷歌提供推送服务FCM(Firebase Cloud Messaging)。FCM的运行依赖谷歌提供的Google Play Services服务,但是因为网络问题谷歌的服务无法在国内使用,中国大陆手机厂商都会把安卓手机上的谷歌相关的服务移除,结果在大陆地区就没有类似苹果设备统一的推送服务可用。

大陆地区有很多第三方的推送服务商,提供的服务大致分为两种,一种是手机厂商自己提供的推送服务,例如华为和小米;另一种是非手机厂商提供的消息推送服务,例如百度推送、极光推送、腾讯信鸽等,比较杂乱,效果也众说纷纭。

大陆地区有一个叫做统一推送联盟的组织,2017年由中国信息通信研究院泰尔终端实验室发起(包含:华为、小米、vivo、OPPO、三星等手机厂商;百度、阿里、腾讯、奇虎科技等互联网企业;个推、极光等第三方推送商),意在构建一个国内可用的,统一的推送服务入口,逐步解决国内安卓推送碎片化的局面。效果如何,需要持续关注。

2. FCM原理介绍

FCM集成的实现包括用于发送和接收的两个主要组件:

  • 一个受信任的环境,例如向用户APP发送消息的后台服务;
  • 一个接收消息的客户端应用;

FCM提供了连接两个组件的服务:

  • FCM的消息服务,连接用户消息服务器和用户设备。提供了HTTP和XMPP API供用户服务器管理和发送消息,还提供了Admin SDK支持开发一个基于移动设备的消息管理程序;

  • Google Play services,用于接收FCM消息服务发来的消息,分发到客户端SDK进行处理;

运行集成FCM的客户端的移动设备上需要运行安卓4.0及以上版本系统,并安装运行Google Play services服务15.0.0或者更高版本。Google Play services服务在终端设备上是常驻的,开启后不会被系统自动关闭,这样就能保证客户端在有网络连接的情况下,随时收到来自FCM消息服务器的通知和消息。如果移动终端没有安装Google Play services或者因为网络原因无法连接到谷歌的FCM消息服务器,就不能正常地接收到消息。

FCM的主要功能 功能描述
发送通知消息或数据消息 发送向用户显示的通知消息,或者发送数据消息并完全确定应用代码中会发生的情况
通用消息定位 使用以下三种方式中的任意一种将消息分发到客户端应用:分发至单一设备、分发至群组设备、分发至订阅特定主题的设备。
从客户应用发送消息 通过FCM可靠而省电的连接通道,将确认消息、聊天消息及其他消息从设备发回至服务器

FCM可以发送通知类消息和数据消息两种,这两种消息的有效负载上线均为4KB,数据中的Token是指后面要说到的注册令牌。

  • 通知类消息负载
{
  "message":{
    "token":"...",
    "notification":{
      "title":"Portugal vs. Denmark",
      "body":"great match!"
    }
  }
}
  • 数据类消息负载
{
  "message":{
    "token":"...",
    "data":{
      "Nick" : "Mario",
      "body" : "great match!",
      "Room" : "PortugalVSDenmark"
    }
  }
}

3. FCM和APP内推送的协调

应用在后台运行时,通知类消息负载会被传递到通知面板;应用在前台运行时,通知类型的消息负载不会被传递到通知面板,APP可以在FirebaseMessagingService中定义的回调函数onMessageReceived中处理接收到的消息负载,包括通知类型负载和数据类型的负载;数据类型的消息负载不论在前后台运行,都可以在这里接收处理。

如果希望对FCM展示的通知有点击效果,例如点击某个通知后可以调起某个APP内的界面或者功能,可以给消息同时加上通知类型负载和数据类型负载,通知类型负载会被显示在通知面板中,数据类型的负载可以在点击事件后,在启动器Intent的extras中获取处理。

集成FCM就要在APP内的推送和FCM之间就需要做一个协调,避免出现一个用户通知在移动设备上被展示两遍的现象,一遍来做FCM,一遍来自APP内推送通道。

建议的方式是,只对通知面板的显示做协调处理,对APP内功能性的弹出式提醒或或静默式的触发操作依旧只在APP内推送通道处理,既FCM只负责传递需要在通知面板展示的通知类消息推送。

APP启动设置默认关闭FCM通道,在Application初始化过程中检查Google Play Services的可用性,一旦确定打开FCM就关闭APP内推送通道的Notification功能。

4. 集成流程介绍

  • 第一步,要到Firebase控制台申请创建APP,按照下面的的步骤,可以得到一个名为google-services.json的配置文件,把这个文件下载并放在工程内app/目录下。




  • 第二步,配置Google Services插件,向根级build.gradle文件中添加规则,以引入google-services插件和Google的maven仓库:

buildscript {
    // ...
    dependencies {
        // ...
        classpath 'com.google.gms:google-services:4.0.1' // google-services plugin
    }
}
allprojects {
    // ...
    repositories {
        // ...
        maven {
            url "https://maven.google.com" // Google's Maven repository
        }
    }
}
  • 第三步,在app/build.gradle的底部添加apply plugin代码,启用google-services插件,并添加FCM所需要的库

需要及时更新firebase-core和firebase-messaging,否则可能会收不到FCM服务器发来的消息。

apply plugin: 'com.android.application'
android {
  // ...
}
dependencies {
  // ...
  compile 'com.google.firebase:firebase-core:16.0.1'
  compile 'com.google.firebase:firebase-messaging:17.0.0'
}
// 虽然通常把apply放在文件顶部,但在文档中明确要求放在文件的底部,就放在底部
apply plugin: 'com.google.gms.google-services'
  • 第四步,在应用清单中设置通知面板图标和颜色并设置FCM默认不启动,ic_notification图片不宜过大,过大的图片在一些手机上显示不出来,建议100*100左右合适,不要留边框或者边缘透明区域;
<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/color_src" />
<meta-data android:name="firebase_messaging_auto_init_enabled"
    android:value="false" />
  • 第五步,APP初始化过程中检查Google Play Services的可用性,并决定是否开启FCM,如果开启则根据“协调”中的建议或者自选的逻辑关闭APP内推送通知的Notification功能。
// 示例程序,这个方法是写在Application类中的,所以这里的this指的是Application实例对象
private void checkGoogleService() {
    final int available = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this);
    switch (available) {
        case ConnectionResult.SUCCESS:
            FirebaseMessaging.getInstance().setAutoInitEnabled(true);
            // TODO:对APP推送通知的处理
            break;
        case ConnectionResult.SERVICE_MISSING:
            Log.d(TAG,"check google service service missing");
            break;
        case ConnectionResult.SERVICE_UPDATING:
            Log.d(TAG,"check google service service updating");
            break;
        case ConnectionResult.SERVICE_DISABLED:
            Log.d(TAG,"check google service service disabled");
            break;
        case ConnectionResult.SERVICE_INVALID:
            LogEx.d(TAG,"check google service service invalid");
            break;
        case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
            Log.d(TAG,"check google service version too old");
            ToastUtil.showLENGTH_LONG("Google play service need to update");
            break;
        default:
            Log.d(TAG,"check google service default: available = " + available);
            break;
    }
}
  • 第六步,注册令牌。FCM消息服务器是通过一个叫注册令牌的字符串来定位设备的,APP初次启动时,FCM SDK会为客户端应用生成一个注册令牌,客户端应用需要获取到这个注册令牌并发送到APP服务器并和当前APP用户关联保存,当APP服务器需要向指定用户发送通知消息,需要检索到与用户关联保存的注册令牌,通过这个注册令牌向FCM消息服务器发送通知消息,FCM消息服务器通过这个注册令牌,定位移动设备,并发送通知消息。

注册令牌在以下几种情况会有变更:

    1. 通过FirebaseInstanceId.getInstance().deleteToken()主动删除注册令牌
    1. 用户卸载重装APP
    1. 用户清除应用数据
    1. 应用在新设备上恢复

以上可知,注册令牌跟客户端APP具体的登录用户是没有关联的。所以,在APP启动完成后要确认获取注册令牌并与用户关联保存到APP服务器,在用户退出客户端APP时,要通知APP服务器把关联保存的注册令牌删除,避免服务器错误地把已经退出的用户的通知消息,发送到无效的设备上。

监听注册令牌的生成,需要继承FirebaseInstanceIdService并在onTokenRefresh回调中使用FirebaseInstanceId.getInstance().getToken()方法获取并在本地持久化保存。

<service android:name="com.fcm.MyFireBaseInstanceIDService">
    <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
    </intent-filter>
</service>
package com.fcm;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.FirebaseInstanceIdService;
public class MyFireBaseInstanceIDService extends FirebaseInstanceIdService {
    @Override
    public void onTokenRefresh() {
        String token = FirebaseInstanceId.getInstance().getToken();
        // TODO: 本地持久化保存获取到的注册令牌
    }
}
  • 第七步,如果按照”协调”中的建议,FCM只处理通知类型的消息,这一步不需要。如果希望对FCM消息做更多的处理,需要继承PalFishFireBaseMessagingService并在onMessageReceived回调方法中拆解消息内容,根据APP内部协议进行相应的处理。
<service android:name="com.fcm.MyFireBaseMessagingService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
package com.fcm;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
public class MyFireBaseMessagingService extends FirebaseMessagingService {
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // TODO: 处理接收到的FCM消息
    }
    // 在某些情况下,FCM 可能不会传递消息。如果在特定设备连接 FCM 时,您的应用在该设备上的待处理消息过多(超过 100 条),
    // 或者如果设备超过一个月未连接到 FCM,就会发生这种情况。在这些情况下,您可能会收到对 FirebaseMessagingService.onDeletedMessages() 的回调。
    // 当应用实例收到此回调时,应会执行一次与您的应用服务器的完全同步。如果您在过去 4 周内未向该设备上的应用发送消息,FCM 将不会调用 onDeletedMessages()。
    @Override
    public void onDeletedMessages() {
    }
}

5. 集成过程中的问题

    1. 在app/build.gradle中引入firebase-core和firebase-messaging库时,遇到firebase-messaging版本不匹配,导致开启FCM功能FirebaseMessaging.getInstance().setAutoInitEnabled(true)时,出现问题:No virtual method zzcz(Z)V in class com.google.firebase.iid.FirebaseInstanceId,解决办法就是调整firebase-core和firebase-messaging的版本,两个库的版本可以在maven仓库中搜索。
    1. 检查Google Play Services可用性的方法GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)并不总是可靠的,例如中国大陆运行的手机,如果安装了Google Play Services服务,返回值也会是true,对于从中国大陆以外进入大陆地区的用户,会因为网络不通无法接收FCM消息,而启动时又依据这个判定打开FCM并关闭APP内推动通知,导致用户在一段时间内收不到任何通知。建议在APP设置中给一个FCM的开关,如果遇到这种情况,关闭FCM开关之后不做可用性检查。