Android多平台Push推送服务集成组件化实践

Posted Ever69

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android多平台Push推送服务集成组件化实践相关的知识,希望对你有一定的参考价值。

众所周知,因为一些原因(hexie)谷哥服务在大陆不能使用,再加之android系统开源,所以国内基于Android源码定制的各种OS百花齐放,基本大一点的手机品牌厂商为了更好的用户体验都会有自己的深度定制版OS,比如MIUI、EMUI、Color OS、Flyme,Smartisan OS等等,那么既然谷哥服务不能用,反正系统都定制了,那么干脆把各种服务也自己定制一番吧,没有Google play?没关系,各种应用商店上!没有FCM(Firebase Cloud Messaging,GCM升级版,谷歌统一推送服务),没关系,自家推送服务上!小米推送服务、华为推送服务、Vivo推送服务...那没钱搞自家推送服务的厂商咋办?也没关系,专门搞推送服务的第三方公司满足你!极光、个推、友盟…这真是应了那句话,虽然倒下一个FCM,但是还有千千万万个FCM站了起来(手动滑稽)

这下可好,你一套服务我一套服务,开发直接傻眼了,这么多服务,我接哪家?—当然是都接啊,小孩子才做选择题。看到这可能有的开发已经开始口吐芬芳了,比如我,其实这也是被逼无奈,在手机厂商百家齐放的中国,想用一套推送服务去达到较高的通知到达率是不可能的,且不说有多少用户会主动关闭应用通知,光是想让第三方的服务能够在这些厂商的OS中稳定存活下去就是件难事。所以干脆就服务对应厂商,有自家服务的开启自家服务,没有的就开启第三方服务,这样起码可以保证推送到达率达到一个我们能影响到的最大值 ,因为针对那些有自家服务的厂商,除非用户自己关掉通知权限,那么我们的推送一定会到达用户的手机,因为这个服务是自家OS来维护的,基本不会存在不能用的情况,那么针对没有自家服务的厂商,只能启用第三方的服务了,如果希望提高第三方服务的通知到达率,可以考虑第三方服务多开,比如同时开着极光和个推的服务,如果这时个推服务挂了,还有极光可以接受,不过这也不是万全之策,毕竟有可能这俩服务都挂了...但是它还是可以在一定程度上提高我们的通知成功送达用户手机上的几率。

既然都要了,那使用什么样的方式能够比较好的去接入这么多平台服务呢?我先说一下我们项目之前的做法吧,就是生接,什么扩展、复用、可移植方面的东西都没有,我要接小米、华为、Vivo、个推四个平台的推送服务,就直接把四家各自需要继承实现的Receiver或Service重写一遍,里面的消息处理逻辑也是各有一份,虽然都是相同的,但是就没有封装,这就很难受了,因为如果我处理消息的逻辑要变,那么这四家的服务实现里都要更改一遍,扩展也是如此,直接从已有的实现里复制一份到新的就ok了。可移植性呢?抱歉,如果有其他项目要用,只能将全部文件代码拷贝过去...这下你应该明白为什么我会在段落开头提问了吧,那么有什么比较好的方式接入吗?肯定是有的,之所以会出现我刚才讲的那种情况,完全就是因为懒,开发的时候懒得去想办法方案把这个东西做好,所谓前人挖坑,后人填坑说的就是如此了。

接下来我们针对当前集成方式分析一下存在什么问题,怎么优化?

目前服务集成后存在的问题

  1. 消息的处理逻辑分散,各个服务都维护一套自己的逻辑
  2. 扩展性差,违反开闭原则
  3. 不够灵活,不利于其他项目复用

针对存在的问题,有什么优化方案呢?

  1. 第一个问题,我希望能有一个统一的地方去处理各家推送服务消息的逻辑,这样就不会存在多份相同的代码,方便对各家通知消息处理的管理。
  2. 第二个问题,我希望各个服务之间完全独立,不产生关系。
  3. 第三个问题,我希望其他项目也能很方便的使用我封装的这套推送集成服务,傻瓜式使用, 省去再次集成的步骤。
  4. 第一第二个优化可以通过设计模式来实现,第三个优化呢,我打算把推送服务从项目中独立出来,搞成依赖的形式引入项目,也就是组件化。

Ok,确定了优化点后,我们再把组件化需要实现的功能再列一遍

  1. 根据手机平台开启对应推送服务,如没有自家推送服务,则开启第三方推送服务,支持默认和指定开启第三方平台,并且可以多开
  2. 各个推送平台之间代码完全解耦,不产生关系
  3. 所有平台的推送消息都要在一个地方进行处理
  4. 对透传消息要进行重复过滤,推送服务多开情况下,可能两个平台都会收到同一条消息(通知消息的处理不受我们控制,是SDK直接向系统弹出通知的,所以我们不对它进行处理)
  5. 方便扩展其他推送平台,对扩展开放,对修改封闭
  6. 可作为依赖直接引入不同项目

如何进行多平台消息的统一处理

列出了六点需求,2、3、4应该是比较重要或者是比较费心思的,看到这里,相信大家心里也清楚我们本次组件化的难点不在于如何组件,而在于如何对各平台的消息进行统一处理,我先放一张图,大家看完后心里大概能有个思路。

一般我们集成推送服务,都需要实现SDK中封装好的Broadcast Receiver或者Service,然后实现里面不同的方法,对各种推送事件进行处理,那么我为了实现对各家推送消息进行统一处理,所以选择在各家服务的实现里再发一次广播到我自己定义的广播中去,以此达到统一处理的目的,你可以理解为不同的地方政府向中央政府进行事务汇报,由中央政府统一进行决定,这么一说,是不是就有内味儿了~

拿小米推送的实现来说,我在它需要实现的Receiver中的每个方法都通过PushReceiverManager调度了一次。

public class MIPushMessageReceiver extends PushMessageReceiver 

    private String TAG = "pushService";

    /**
     * 透传消息
     *
     * @param context
     * @param message
     */
    @Override
    public void onReceivePassThroughMessage(Context context, MiPushMessage message) 
        if (message != null) 
            PushReceiverManager.getInstance().onMessageReceiver(context, convert2ReceiverInfo(message));
        
    

    /**
     * 通知消息送到了
     *
     * @param context
     * @param message
     */
    @Override
    public void onNotificationMessageArrived(Context context, MiPushMessage message) 
        if (message != null) 
            PushReceiverManager.getInstance().onNotificationReceiver(context, convert2ReceiverInfo(message));
        
    

    /**
     * 用户点击通知消息
     *
     * @param context
     * @param message
     */
    @Override
    public void onNotificationMessageClicked(Context context, MiPushMessage message) 
        if (message != null) 
            PushReceiverManager.getInstance().onNotificationOpened(context, convert2ReceiverInfo(message));
        
    

    @Override
    public void onReceiveRegisterResult(Context context, MiPushCommandMessage message) 
        if (message == null) 
            LogUtil.e(TAG, "小米推送服务注册失败");
            return;
        
        if (MiPushClient.COMMAND_REGISTER.equals(message.getCommand())) 
            if (message.getResultCode() == ErrorCode.SUCCESS && message.getCommandArguments() != null && !message.getCommandArguments().isEmpty()) 
                LogUtil.e(TAG, "小米推送服务注册成功" + message.getCommandArguments().get(0));
                //注册id
                ReceiverInfo info = new ReceiverInfo(PushTargetEnum.XIAOMI, message.getCommandArguments().get(0));
                PushReceiverManager.getInstance().setPushTargetCid(PushTargetEnum.XIAOMI, message.getCommandArguments().get(0));
                PushReceiverManager.getInstance().onRegistration(context, info);
             else 
                LogUtil.e(TAG, "小米推送服务注册异常" + message.getResultCode());
            
        
    

    private ReceiverInfo convert2ReceiverInfo(MiPushMessage message) 
        return message.getContent() == null ? null : new ReceiverInfo(PushTargetEnum.XIAOMI, message.getContent());
    

,咦?说好的再发一次广播呢?别急,在看看PushReceiverManager这个类

public class PushReceiverManager 

    /**
     * 注册行为
     */
    static final int TYPE_REGISTRATION = 0;
    /**
     * 收到自定义消息
     */
    static final int TYPE_MESSAGE = 1;
    /**
     * 收到通知
     */
    static final int TYPE_NOTIFICATION = 2;
    /**
     * 点击通知
     */
    static final int TYPE_NOTIFICATION_OPENED = 3;
    
    ...

    /**
     * 聚合的广播action
     */
    private static final String ACTION = "BasePushBroadcastReceiver";


    private static PushReceiverManager mInstance;

    private ArrayList<BasePushTarget> mPushTargets;

    private Context mContext;

    private PushReceiverManager() 
    

    public static PushReceiverManager getInstance() 
        if (mInstance == null) 
            synchronized (PushReceiverManager.class) 
                if (mInstance == null) 
                    mInstance = new PushReceiverManager();
                
            
        
        return mInstance;
    

    public void setPushTargets(ArrayList<BasePushTarget> targets) 
        mPushTargets = targets;
    

    /**
     * 将各个推送平台注册成功后返回的cid设置到本地
     *
     * @param target
     * @param cid
     */
    public void setPushTargetCid(PushTargetEnum target, String cid) 
        for (BasePushTarget pushTarget : mPushTargets) 
            if (pushTarget.getTargetEnum() == target) 
                pushTarget.setCid(cid);
                break;
            
        
    

    /**
     * 发送信息到自定义的聚合广播
     *
     * @param context
     * @param type
     * @param info
     */
    private void sendBroadcast(Context context, int type, ReceiverInfo info) 
        Intent intent = new Intent(ACTION);
        intent.putExtra(BasePushBroadcastReceiver.KEY_TYPE, type);
        intent.putExtra(BasePushBroadcastReceiver.KEY_INFO, info);
        intent.setPackage(context.getPackageName());
        context.sendBroadcast(intent);
    

    /**
     * 推送服务注册成功的回调
     *
     * @param context
     * @param info
     */
    public void onRegistration(Context context, ReceiverInfo info) 
        if (info == null || TextUtils.isEmpty(info.content)) 
            return;
        
        sendBroadcast(context, TYPE_REGISTRATION, info);
    

    /**
     * 收到通知的回调
     *
     * @param context
     * @param info
     */
    public void onNotificationReceiver(Context context, ReceiverInfo info) 
        if (info == null || TextUtils.isEmpty(info.content)) 
            return;
        
        sendBroadcast(context, TYPE_NOTIFICATION, info);
    

    /**
     * 通知被点击的回调
     *
     * @param context
     * @param info
     */
    public void onNotificationOpened(Context context, ReceiverInfo info) 
        if (info == null || TextUtils.isEmpty(info.content)) 
            return;
        
        sendBroadcast(context, TYPE_NOTIFICATION_OPENED, info);
    

    ...


在这个类我定义了通知的很多行为,根据行为的不同发送不同类型的广播,说白了这就是一个发送广播的封装类。

咦?又不对了,说好的透传消息的重复过滤呢?怎么没看到代码。

别问号了,我只是想单独说一下它,所以就把它的代码从上面的类里抠出来单讲了。

过滤重复消息这块,就要请出我们的老朋友——Handler,因为它内部持有一个消息队列,所以对于我们过滤重复消息这块再适合不过了,其实广播也可以实现这个效果,但是我不想让这个动作在外面进行处理。

//过滤条件
public static String FILTER_CONDITION = null;

private Handler mHandler = new Handler(new Handler.Callback() 
        @Override
        public boolean handleMessage(Message message) 
            if (message.what == TYPE_MESSAGE) 
                try 
                    ReceiverInfo info = (ReceiverInfo) message.obj;
                    JSONObject jsonObject = new JSONObject(info.content);
                    if (FILTER_CONDITION != null) 
                        if (jsonObject.has(FILTER_CONDITION)) 
                            if (!PushCacheUtil.hasPushId(mContext, jsonObject.getString(FILTER_CONDITION))) 
                                sendBroadcast(mContext, TYPE_MESSAGE, info);
                            
                         else 
                            onFilterConditionMissing(mContext, info);
                        
                     else 
                        sendBroadcast(mContext, TYPE_MESSAGE, info);
                    
                 catch (JSONException e) 
                    e.printStackTrace();
                
            
            return true;
        
    );

 /**
     * 收到透传消息的回调
     *
     * @param context
     * @param info
     */
    public void onMessageReceiver(Context context, ReceiverInfo info) 
        if (info == null || TextUtils.isEmpty(info.content)) 
            return;
        
        mContext = context;
        //通过handler判断重复消息
        mHandler.sendMessage(mHandler.obtainMessage(TYPE_MESSAGE, info));
    

FILTER_CONDITION是过滤条件,是每个消息的唯一码,因为Handler内部的消息队列是一个有序队列,所以它会对消息逐一进行处理,这样我们就可以通过数据库或者本地存储对推送消息进行一个条件判断,在发送透传消息的广播前,在本地对消息的唯一码进行查询,如果有则过滤这条消息,没有就存储唯一码并发送广播。

这是我们的广播基类

public abstract class BasePushBroadcastReceiver extends BroadcastReceiver 

    public static final String KEY_TYPE = "type";
    public static final String KEY_INFO = "info";

    public BasePushBroadcastReceiver() 
    

    @Override
    public void onReceive(Context context, Intent intent) 
        if (intent == null) 
            return;
        
        ReceiverInfo info = intent.getParcelableExtra(KEY_INFO);
        int type = intent.getIntExtra(KEY_TYPE, 0);
        switch (type) 
            case PushReceiverManager.TYPE_REGISTRATION:
                onRegister(context, info);
                break;
            case PushReceiverManager.TYPE_NOTIFICATION:
                onNotification(context, info);
                break;
            case PushReceiverManager.TYPE_MESSAGE:
                onMessage(context, info);
                break;
            case PushReceiverManager.TYPE_NOTIFICATION_OPENED:
                onOpened(context, info);
                break;
            ...

            default:
                break;
        
    

    /**
     * 推送服务注册成功,返回cid
     *
     * @param context
     * @param info    info.getContent() 对应的就是具体的推送平台的注册id ,例如小米的regid和华为的TMID
     */
    public abstract void onRegister(Context context, ReceiverInfo info);

    /**
     * 收到透传消息
     *
     * @param context
     * @param info    info.getContent对应的是消息文本
     */
    public abstract void onMessage(Context context, ReceiverInfo info);

    /**
     * 收到通知
     *
     * @param context
     * @param info    info.getContent对应的是消息文本
     */
    public abstract void onNotification(Context context, ReceiverInfo info);

    /**
     * 通知被点击
     *
     * @param context
     * @param info    info.getContent对应的是消息文本
     */
    public abstract void onOpened(Context context, ReceiverInfo info);

    ...

引用了此组件化依赖的项目只需实现这个广播就可以完成对多家推送服务平台消息的处理,是不是很方便~

最后

OK,这个多平台推送消息的统一处理完成后,其他的需求点都是洒洒水了,就不在这赘述了,很容易就能实现。

最后就是把我们的代码打成AAR,上传到我们的Maven服务器,供项目引用了~

以上是关于Android多平台Push推送服务集成组件化实践的主要内容,如果未能解决你的问题,请参考以下文章

Android推送接入总结--个推版

Android 华为推送服务集成

Android OPPO推送服务集成

技术干货 | 闲鱼:一个优秀的 Push 平台,需要经历怎样的前世今生

Android 小米推送服务集成

Android 小米推送服务集成