React Native接入小米推送

前言

应用接入第三方推送这个需求老板早就提过了,但是因为一方面担心推送用不好效果会适得其反,另一方面因为之前每次版本迭代的需求都比较急,所以接入推送这个事就一直没有完成,最近新版本的Android和iOS都已经上线了,下一版的迭代需求也没有太多,所以自然就把接入第三方推送这个需求放到下一版迭代开发中了。

调研

市面上有很多第三方推送,比如做推送起家的极光,信鸽(腾讯推送),个推。还有一些大公司的推送像友盟,阿里推送,百度推送,腾讯推送(信鸽),还有一些手机厂商自家的推送像小米推送,华为推送,魅族推送等

因为我们的产品是用React Native开发的,所以在调研选取推送之前,除了一些常见的推送硬指标之外,我们还希望能找到有官方支持React Native应用接入的推送。主要是考虑到现在整个客户端就我一个开发人员,Android方面倒是没有问题,iOS如果没有官方文档的话,我怕接入过程会不太顺利,毕竟现在我的iOS水平还很低。。

根据实际情况,看了一下每家的推送,好像就发现一个极光推送有官方支持React Native接入的文档和Demo,本来打算选择极光的。但是,又考察了一些关于推送到达率的这些硬指标之后,又对比了一下我们的产品umeng后台统计的机型数据之后,最终决定接入小米推送。

难点

  • 小米推送并没有官方提供的React Native接入文档或者Demo
  • 推送消息在React Native层面消息收取和分发

尤其是第二个技术难点,我们知道推送接入成功之后,收到推送消息以及用户点击推送消息这些事件的接受都是在Android和iOS的原生层面进行的,如何将消息传递到React Native层面进行消息的处理和页面的跳转是一大难题。

正式开始

下面就讲讲从接入到实现点击消息在React Native层面上跳转的过程吧

原生端接入小米推送SDK

这个官方文档上有针对原生iOS和Android的详细的接入文档,首先在小米推送后台创建好应用,iOS和Android版的都需要创建,然后可以获取对应的app id和key,接入之前iOS还需要创建APNs证书,包括测试证书和线上证书,创建APNs证书的过程官方文档并没有详细写,这个网上有很多教程教你怎么创建

原生接入推送SDK这一步并没有什么难度或者大坑,参考官方的文档和提供的Demo基本都不会有什么问题的,所以就不展开讲了

消息的获取和传递
Android

在Android 原生层面推送消息的接收主要是自定义的继承PushMessageReceiver类重写的onNotificationMessageArrivedonNotificationMessageClicked两个方法(只针对通知栏消息,透传消息请参考文档)。

我们已经知道在Android设备上通知栏推送消息到达和点击回调的对应方法,那么下一步需要做的就是如何将Android 原生层面收到的消息到达和用户点击消息的回调行为传到React Native层面,然后由React Native层面进行分发和处理。

在解决这个问题之前,还有一点需要注意的就是:在推送消息送达App的时候,App有两种可能,一种是App正在前台运行,一种是不在前台(退出了应用),这两种状态会有点区别:

应用在前台的时候通过点击通知栏消息是不会回调onNotificationMessageClicked方法的。这一点需要注意,因为我们后面判断用户是通过桌面icon打开的应用,还是通过点击通知栏消息打开的应用(可能需要进一步处理消息和跳转)

那我们就分来开说两种情况都是如何将消息到达和点击与否传给React Native层面进行处理的

  • 应用不在前台(退出应用了)

    我们在应用打开加载React Native主页的componentDidMount或者componentWillMount里面去主动查询Android原生端是否有推送消息到达,这次打开应用是否是通过点击通知消息,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    NativeMiPushAndroid.getNotificationMessage()
    .then((data) => {
    //如果是的话,那就通过该方法去处理消息和进行React Native页面跳转
    this._toPageByNotification(data)
    //完事之后记得将是通过点击通知栏消息打开应用的标志位复原
    NativeMiPushAndroid.setNotificationNull()
    })
    .catch(({code, message}) => {
    //复原标志位
    NativeMiPushAndroid.setNotificationNull()
    })

    在Android原生层面NativeMiPushAndroidgetNotificationMessage的方法实现如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public void getNotificationMessage(Promise promise) {
    //如果不是通过通知栏消息打开的应用
    if (!openByClickNotification) {
    promise.reject("-2", "not click by notification");
    return;
    }
    //是通过通知栏消息打开的应用
    if (miPushMessage != null) {
    WritableMap map = new WritableNativeMap();
    for (Map.Entry<String, String> entry : miPushMessage.getExtra().entrySet()) {
    map.putString(entry.getKey(), entry.getValue());
    }
    promise.resolve(map);
    } else {
    promise.reject("-1", "has no push message");
    }
    }

    在自定义的PushMessageReceiver里面将收到到达的消息通过Handler传到NativeMiPushAndroid中,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Override
    public void onNotificationMessageClicked(Context context, MiPushMessage miPushMessage) {
    MiPushModule.setOpenFlag(true);
    Message msg = Message.obtain();
    msg.obj = miPushMessage;
    MiPushModule.getHandler().sendMessage(msg);
    mMessage = miPushMessage.getContent();
    if (!TextUtils.isEmpty(miPushMessage.getTopic())) {
    mTopic = miPushMessage.getTopic();
    } else if (!TextUtils.isEmpty(miPushMessage.getAlias())) {
    mAlias = miPushMessage.getAlias();
    } else if (!TextUtils.isEmpty(miPushMessage.getUserAccount())) {
    mUserAccount = miPushMessage.getUserAccount();
    }
    Intent intent = new Intent(context, MainActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
    context.startActivity(intent);
    }

    基本主要的代码逻辑就是这些了,有些人可能会问,为什么不在自定义的PushMessageReceiver里面直接将消息通过广播从Android端发送到React端,然后React端收到消息后直接处理??

    这个问题问的很好,原因也很简单:就是在应用并不在前台运行态的情况下,收到消息之后,当用户点击消息回调onNotificationMessageClicked的时候,我先把App启动到前台,然后发送广播给React Native层,这个时候React Native因为启动顺序加React Native加载原理的原因是收不到Android原生发的消息的,所以这个时候只能是通过打开应用启动React Native设置的主页面的componentDidMount方法去查询是否有推送消息,是否是通过推送消息打开的App

  • 应用在前台

    这个就比较简单了,因为此时应用已经启动了,可以直接在Android原生层面发送广播把消息传给React Native层面进行处理了,核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public static Handler pushHandler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
    if (msg.obj instanceof MiPushMessage) {
    miPushMessage = (MiPushMessage) msg.obj;
    WritableMap map = new WritableNativeMap();
    for (Map.Entry<String, String> entry : miPushMessage.getExtra().entrySet()) {
    map.putString(entry.getKey(), entry.getValue());
    }
    sendEvent(reactContext, "openByNotificationOnAndroid", map);
    }
    }
    };

    private static void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
    reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
    }

    在React Native层面的主页componentDidMount方法里面注册广播:

    1
    DeviceEventEmitter.addListener('openByNotificationOnAndroid', this.openByNotificationMsg)
iOS(针对iOS10及以上)

在iOS上其实原理也是一样的,也是分为两种情况:

  • 应用在前台运行

    这种情况下是通过发送广播将消息从iOS原生发送到React Native层面

  • 应用没有运行

    在打开应用回调AppDelegate类中的didFinishLaunchingWithOptions方法时,将消息保存下来,然后在React Native层面的home页面中去主动查询是否有推送消息以及这次应用是否是通过点击通知栏消息打开的

需要注意的是iOS并不会自动在后台数据中统计用户点击通知栏的数据,所以如果我们需要这个点击数据(一般都需要)就要在用户两种上述两种情况下分别在didFinishLaunchingWithOptionsdidReceiveNotificationResponse里面去调用MiPushSDK.openAppNotify(messageId)方法,官方文档里面有说明

就这样就算是解决了React Native接入小米推送,并且可以在React Native层面进行推送消息的获取和处理工作

总结

为了针对App的不同状态,自己采用的是在非运行状态下:在React Native层Home页面加载的时候在componentDidMount方法里面去主动和原生去通信查询推送消息情况;在运行状态下右原生主动发送广播来通知和传递推送消息给React Native层面,由React Native层面进行分发和处理

自己在应用不在前台(退出的情况)如何将消息从原生送到React Native层面走了一些弯路,不熟悉iOS原生开发也走了一些弯路,不过最终还好都把功能实现了。

运行了一段时间,感觉小米推送的到达率和各项数据还可以吧,算是在行业平均水平之上吧。因为Android如果非小米手机,应用进程被杀死之后,是收不到小米推送的消息的,所以以后要达到更好的推送效果,肯定需要接入华为和魅族推送,针对不同的设备走不同的推送平台通道