(4.2.12)浅谈第三方推送[推送基础]:百度推送、小米推送、华为推送

市面上目前做免费推送服务的有很多,友盟、极光、百度、小米、华为等,由于android机型的多样性,在使用单独的一种推送时,往往会造成一些机型无法获取(当然,内部原因可能很复杂)。

Android 第三方 Push 推送方案使用调查

因此往往需要同时集成多个第三方推送,并且能做到:

  • 自动识别机型以选取对应的SDK
  • 也希望设定阀门,在多次建联失败时,可以自动切换SDK,以尝试不同的推送服务
  • 记录状态,自动选定上次记录的推送服务SDK

目前,采用了百度、小米和华为的推送SDK,层次上以百度为主覆盖大量机型,小米和华为为辅覆盖专有机型。
三种SDK在基本的使用方法上大同小异,现简单加以说明:

1. 什么是推送

在移动互联网时代,移动端的开发都必须涉及 客户端/服务端的概念,但是从字面上看,服务端和客户端是完全独立的两套逻辑

客户端可以主动的向服务端请求数据,譬如最常见的登录,用户输入账户密码,点击确定按钮—客户端主动把信息抛给给服务端,并等待服务端返回—-服务端返回校验状态–客户端拿到,并呈现相应的UI状态

注意,上述流程的起点是客户端主动发起请求那么问题来了,一旦服务端的数据发生了变更,如何主动告知客户端

那么问题来了,一旦服务端的数据发生了变更,如何主动告知客户端呢? 譬如,你的服务器收到了一个紧急邮件,但是你没在电脑前边,服务器如果通知到你呢?

这种服务器主动发起的信息交互,就是推送

关于推送的具体概念和不同实现方式,可以自行查看http://www.cnblogs.com/liangqihui/p/3539984.html

  1. 轮训方式 客户端不断向服务器主动请求
  2. 长连接方式 客户端和服务端维持一个长连接,类似一个双向流通的管道,有信息就丢进去,对方一直在保持监听

1.1 端内推送、端外推送、第三方推送这些名词到底是啥意思的?

在进行一下概念之前,先声明一点,也存在一些app的端内推送和端外推送是混合在一起的,譬如完全使用第三方的推送构建自己的聊天或业务逻辑,再譬如完全不依赖第三方,把自己的端内推送做的牛逼到可以用作端外

其实这些概念的界限很模糊,我们就kdzl本身来谈一下:

这里写图片描述

  • 端内推送和端外推送是一对性质相同的概念,这里的端其实指的就是我们的应用app本身

    • 端内推送,意味是我们应用自己构建了与自己服务器的长连接,这个长连接有很大程度上依赖于我们应用进程本身,譬如我们kdzl使用的PB模式的通道,就是建立在底层自己实现了的一个TCP连接上,时刻保持着与我们自己的服务器的连接状态,接收相关信息
    • 端外推送,往往意味着独立于应用实现了的推送服务,这个服务不依赖于我们自身,可以是google android提供的GCM,也可以是公司自己开发的一套推送机制,但就国内而言往往是有其他的公司开发的SDK,我们直接集成就可以,也
  • 第三方推送则是一个独立的概念,主要描述了推送的一种实现方式,主要涉及推送整个流程中是否涉及第三方的服务器

好了,我们结合实际的kdzl分析下:

  • 端内推送 = 我们自己的实现了的PB格式的信息长连接,主要涉及我们自己的业务逻辑,,,譬如用户A给用户B发了一条聊天信息,或者A处理了B的某个流程,都需要端内推送告知用户B状态改变了
  • 端外推送 = 第三方推送(百度推送、华为推送、小米推送),只实现 位于通知栏的状态提醒,不牵扯业务逻辑

现在可以看出来,kdzl的推送分为了两种,而且基本是相互独立的,这点大家要先明白

由此,我们就可以解答这样一个疑问了:
“为什么会出现,通知栏我看到了X给我发的聊天信息,但是我打开应用却没看到,等了一段时间才看到?”
“为什么X给我发的信息,我在应用里的IM聊天界面已经看多了,过了一会,又收到了位于通知栏的新信息提醒?”

2. 既然已经有了端内推送,为什么要使用第三方推送?

我们要知道端内推送的TCP通道是依赖于 应用进程而存在的,进程在活跃状态下(前台或者后台)下,那么TCP通道的推送是基本稳定的;但是我们知道,android会在内存不足的状况下,会回收进程,并且 大量android用户甚至会主动通过系统一键清除或者第三方的一键清除kill我们的进程,这自然就导致我们端内长连接关闭,在此时我们如何通知用户一些信息呢? 这就需要第三方推送了
第三方推送其实也是维持了一个与第三方服务器的长连接服务,同样它也面临同样的问题,但是这个长连接服务比较稳定:

  • 手机厂商自定义的长连接,不会被kill
    譬如小米手机运行的小米推送,小米推送所在进程为系统常驻进程,这个进程不会被系统回收,而且一键清除也不会关闭,所以稳定

  • 厂商合作
    微信作为超级应用,其实是手机厂主动和微信合作,这些我们不细说;厂商合作的结果就是 应用所在进程 被作为系统进程或者高权限进程存活,从而不能或者很难被 一键清除等操作kill, 应用不会被kill,那么自然长连接稳定。
    系统进程衍生的进程还是系统进程,微信主进程和推送进程分离,即便主进程被偶尔kill,推送进程不见得被kill;
    大家可以验证下,我们知道微信其实是有logo界面的,就是那个地球;如果你能看到地球,说明应用是从死亡状态重新激活;如果你一键清除后,再打开微信,还是么考到logo,那就说明应用根本没被kill

  • 平台适配
    做一款稳定的推送是十分耗费人力物力的,尤其是android这样机型千千万,各种自定义的,要在不同的机型上尽量保证存活;

  • 全家桶的相互唤醒
    百度推送作为非手机厂商的推送,在一键清除时候长连接也会很大几率被kill,但是当你启动集成百度推送的其他应用的时候,会把百度推送的长连接唤醒。
    譬如kdzl 和 百度地图都集成了百度推送,当kdzl被kill的时候,长连接也会被系统kill,这时候自然无法收到第三方的推送;但是当你打开百度地图的时候,由于kdzl和百度地图实际上在一台手机上共享一个长连接通道,kdzl的推送就也可收到了;
    这种全家桶由于集成用户众多,相互唤醒概率很大,从而保证长连接的自启动,进而实现稳定

3. 什么是第三方推送

“第三方推送”向开发者提供的消息推送服务;通过利用云端与客户端之间建立稳定、可靠的长连接来为开发者提供向客户端应用推送实时消息服务。

为了使用第三方推送,往往需要继承对应第三方平台的相关资源文件,这些文件一般被封装为一个SDK,借助SDK,应用的开发者可以通过少量的代码,就可以将推送服务接入的APP应用中,同时可选择使用(1)自己搭建服务器使用对应第三方平台服务API 或者(2)直接使用第三方平台服务控制台的方式进行通知信息的发送。

为了实现推送服务,实质上需要以下4个模块:

  • App应用(App Client)
    自己开发的应用,需要接入push,只需要和pushService打交道提出请求即可

  • pushService(Push Client)
    运行在手机上的推送服务,直接代理App应用与“第三方推送服务器”打交道
    用于与服务器建立稳定push通道,上报标签、 LBS信息,通知推送(含富媒体)等
    集成SDK后,一般由SDK维护

  • 第三方推送服务器(推送控制台,Push Server)
    pushService监听的服务器,该服务器用于发送通知或者信息,同时也会处理设置标签、用户记录等操作

  • 自己的业务服务器(App Server)
    实现自己的业务功能,往往需要接入“第三方推送服务器”所设定的一些API,从而可以自动的发送通知,而不再需要人工登录“推送控制台”进行信息推送

4. 典型应用交互流程

典型应用交互流程

这里写图片描述

4.1 参数说明

  • TMID
    用户标识唯一标识,一部用户终端。
    此标识在应用通过开放接口注册到PUSH平台时会获得,需要应用客户端将此标识传递给应用服务端
    在不同的平台有不同的叫法:小米regid,百度channeid+userid,华为token

4.2 流程简要

4.2.1 注册流程

  1. 请求TMID—-注册请求,委托推送服务去开启推送服务
    一般在app启动(activity或者application的onCreate等函数中,不确定,不同平台有不同设定)时,通过调用对应的SDK方法,请求PushService,为当前设备注册并开启推送服务

  2. 请求分配TMID—-接收到应用请求,向“第三方服务器”发起注册请求
    该方法一般由SDK默认实现和控制,应用开发者只需要在步骤1中调用对应的接口方法后,pushservice会向服务器提出申请
    该申请一般是异步申请

  3. TMID应答
    该pushService一直运行在手机中,该service在接收到“第三方服务器”给出的应答后,会发送广播,通知对应的接受者,并转发信息
    该方法一般由SDK默认实现和控制,

  4. TMID响应
    App应用往往需要继承SDK提供“receiver接收器”,并进行注册。
    pushService发出的广播,会被所继承的“接收器”中的对应方法接收,并调用

  5. 上报TMID
    App应用获取到“第三方推送服务器”分配给自己的TMID时,需要将TMID上传给“自己的业务服务器”,从而可以长久保存该ID,并且告知业务服务器通过该TMID,可向对应的业务用户发送通知。
    譬如,1:1的聊天信息,需要做定向推送,A发送给B的信息,需要得知B的TMID码
    再譬如,非定向信息,覆盖全部用户的热定新闻通知,不需要TMID码,全部推送即可。然而实际上,虽然“业务服务器”没有给出TMID码,其实是第三方服务器对“所有该App的TMID码”进行了推送。实质上和上个例子一样

4.2.2 推送流程

我们以“1:1的聊天信息”为例,A发送给B信息:

  1. 根据TMID推送信息
    业务服务器调用“第三方平台”给出的对应API方法,向“推送服务器”发出推送信息的申请,定向推送需要给出B用户的TMID码

  2. 根据TMID找到手机并下发
    第三方的推送服务器会通过TMID码找到对应的手机,并定向发出推送信息

  3. PushService转发信息
    该service一直运行在手机中,监听“ 第三方的推送服务器”的推送,获取到推送信息, 会发送广播,通知对应的接受者,并转发信息

  4. 信息响应
    App应用往往需要继承SDK提供“receiver接收器”,并进行注册。
    pushService发出的广播,会被所继承的“接收器”中的对应方法接收,并调用。
    可以在该方法中实现自己的业务操作。

事件流程

App应用中设定标签,设定别名等操作的过程,与注册流程基本一致

4.2.3 自拟的平台优先级 百度>小米>华为

为什么百度排第一呢,后台和客服反映说百度推送一堆问题,为什么还是采用百度第一呢? 原因两点:

  1. 百度推送的注册流程成功率高
  2. 百度推送的相互启动概率高,,,也就是说pushService能保持长久存活

5. 非典型交互流程

5.1 应用死掉

一直想找个机会,好好的分析下,这些很浅显的问题:

  • 什么叫Deaded?
    • 整个进程组被手动或第三方管理软件kill 了,这是死掉了么?(Yes)
    • 整个应用没有一个Activity在Task栈中,甚至没有Task栈,但是却有后台service在运行,这个应用是Deaded 么?(No)
    • 整个应用没有一个Service在启动,没有Task栈,但是有Receiver正在活动期间,那么这个应用是Deaded 么?(No)

如果,我们非要从一个外在形式上来评判一个应用是Alive还是Deaded,那么我们可以使用当前进程的Application是否存在作为评判,那么唤醒一个应用其实就是意味着做了初始化Application的行为:

  • 什么叫唤醒Alive一个应用?
    • 启动Application下的任意一个Activity,会导致Application的初始化;
    • 启动Application下的任意一个Service,会导致Application的初始化;
    • 启动Application下的任意一个Receiver,会导致Application的初始化;
    • 看上去四大组件都会导致Application的初始化;

事实上,我们经常说“有些应用会后台自启动”,“后台偷跑流量”,这里所谓的自启动,其实并没有启动任何一个Activity,也就是根本没有构建Task,而只是启动了某个Service或者Receiver,从而势必引发至少一个进程和对应Applicaiton,我们视作Alive状态

好了,现在我们得知Application可以作为应用是唤醒状态Alive 或者 死亡deaded状态 的标准了:

  1. 如果主进程的Application存在,那么我们就认为app是Alive,否则app是deaded
  2. 如果当前进程Application存在(针对Service或Receive可能跨进程),那么当前进程是Alive,和主进程app的存活无关系

那么,应用被唤醒了,就意味着 应用可能维持的通信长连接已经建立了么?

  • 唤醒一个应用,只意味着启动某个service或者receiver,和调用 Application.OnCreate函数里的操作
    • 如果你的长连接通道的建立是在以上模块中构建的,那么长连接会被建立
    • 如果不是,那么就不会被建立。譬如,你是在启动页Activity中建立长连接,并把相关信息存储到application,那么你用启动service方式,就不会导致长连接的建立

相信看到这里你基本也清楚了,应用被唤醒 与 应用具体的业务逻辑没有半毛钱关系(长连接的建立也属于业务逻辑分部),如果你想实现某个业务部分,那就必须在被唤醒的模块中 调用或者初始化

5.2 设置中的“允许 应用 自启动”对于推送到底是影响了什么?

如何唤醒被杀死的android app

上篇文章里其实也有提及,唤醒应用的方式

  1. 广播,而且是静态注册的广播
  2. 服务,启动一个service

在本处,我们主要关注下,广播这一块。我们知道,第三方推送SDK维护的PushService其实往往是运行在一个单独的进程中的(但是很大可能和app主进程同组,这点注意下,一会会提及) ,这个进程和我们应用app进程是分割开的

我们注册推送SDK的时候,所有的官方文档都会让我静态一个Receiver类,作为监听器。回顾下上文提到的“推送流程 3-4”,PushService维持一个长连接,如果收到了来自PushServer的信息,则跨进程的发送广播给我们的应用,试图唤醒我们的的应用

5.2.1 第三方PushService唤醒App Receiver的方式

我们可以大胆猜测,这种inten方式应该如《如何唤醒被杀死的android app》一文中所讲:

我们一般发广播都是局限在app内部,所以通常都是这么发的:

Intent intent = new Intent();
intent.setAction("my.broadcast.test");
sendBroadcast(intent);

或者这么发:

Intent intent = new Intent(context, TestBroadcastReceiver.class);
sendBroadcast(intent);

静态的系统广播,例如:开机广播,用户开屏广播,USB插入和拔出广播等,在app运行期间可以用静态注册的接收器正常接收,但是在app被杀死后就无法收到了,Android系统做了屏蔽,把被杀死的app的系统静态广播都过滤了,所以想让app被杀死后仍然通过静态注册的接收器接收系统广播是做不到的

静态注册的自定义广播也会遇到类似问题,尤其是在定制版的系统中

采用下面这种方式发送广播即使app被杀死后,静态广播也能正常收到:

Intent intent = new Intent();
            Context c = null;
            try {
                c = createPackageContext("com.example.broadcasttest", Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
//            intent.setPackage(getPackageName());
//            intent.setComponent(pkgName, className);
//            intent.setComponent(pkgNameContext, className);
            intent.setClassName(c, "com.example.broadcasttest.TestBroadcastReceiver");
//            intent.setClassName("com.example.broadcasttest", "com.example.broadcasttest.TestBroadcastReceiver");
            intent.setAction("my.broadcast.test");
            intent.setFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
            sendBroadcast(intent);

接收放广播的配置(app内的自定义推送Receiver)要把exported设置成true,否则就无法收到app以外的广播发送,只能收到app内部的广播发送

5.2.2 广播唤醒的缺陷

正如文中所说:

以上通过广播唤醒在一些手机上可以正常唤醒app,例如小米3;但是在魅族手机上就没办法唤醒了,需要到安全中心把app的自启动权限开启后才能正常唤醒,由此可见,一些手机厂商可能对于静态广播的接收做了一些优化导致静态广播还是没办法被接收,所以会唤醒失败

即便 intent.setFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)

相信到这里大家也有所明悟了,为什么推送有时候收不到,我们让客户开启自启动权限,就可以收到了,原因如下:

  • App的自启动权限,会限制 PushService的自启动权限;

    • App被kill的时候,PushService作为同组进程,会被同时kill了,如果自启动被限制,则PushService都无法重启,从而导致 整个“推送流程”在环节3就被停止,app自然无法收到推送
  • App的自启动权限,会影响自身被唤醒的限制,也就是receiver的正常接收

    • App被kill了,如果自启动被限制,则其静态注册的Receiver可能被系统屏蔽掉,以防止其自动唤醒,从而导致在 “推送流程”在环节4上被停止

这也是为什么有时开启app的自启动权限,就能导致第三方推送恢复正常的原因了。 至于app被唤醒后,触发通知栏,但是通知栏信息未显示的原因,我们在后续文章中会讲解。

6. 推送的“信息”

信息推送是一个动词,我们上边主要关注了“推送”这个动作的实现方式,我们知道了实现这个动作所需要的 四大天王,缺一不可

那么作为这个动作的受体,到底是什么呢?

我们可以简单地认为是一串文本信息,譬如一句话: “你好”

到这里,我们回归下最开始的意图,我们使用第三方推送,是期望能在手机的通知栏弹出一栏信息,根据 执行弹出动作的主体可以分为两种类型:

  1. 通知类型 弹出通知栏的动作由PushService完成,由服务端直接拟定
  2. 透传信息类型 弹出通知栏的动作由应用自身完成,先解析数据,再弹窗

相对而言,透传信息的自由度更高,我们可以自行处理收到信息后的状态,譬如弹窗,开启引用,或者不弹窗

我们现在在百度和华为平台上使用了透传信息,而小米则使用了通知

6.1 为什么还要使用通知呢?

其实这和我们之前说的“非典型交互流程”有关

我们知道, pushservice 在收到信息后,会发送广播,试图唤醒我们的应用,然后将信息传递过去

那么唤醒不成功呢? 自然无法将信息传递过去,应用本身不知道信息,处于dead状态,自然就不会弹窗提醒了

然而通知类型则不需要唤醒了,因为弹窗动作直接由pushservice完成啦,管你app是死是活,都能弹出来提示

而在小米手机上,pushservice是一种系统支持的服务,你杀都杀不死,所以我们在一般在小米手机上使用通知方式

6.2 那通知有这个好处,为什么我们在三个平台上,都使用通知呢?

很简单呀,一旦使用通知,相当于把全部责任给 pushservice, 如果pushservice越坚挺,我们就都使用通知

然而实际上,只有小米和华为这种手机厂商自拟的pushservice,在自己手机上 pushservice 才有这么牛逼

因为,是小米手机我们就用小米推送

6.3 华为为什么不用通知?

华为的透传信息也是系统级别的,能保持 pushservice 坚挺就足够了,,,我们还是期望多些自由度

6.4 透传或通知到了,就一定会弹窗么?

肯定不是了,弹窗只是一个动作,很有可能被“通知中心”之类的限制,你弹,它限制,你弹,它限制,自然出不来了

7. Android推送遇到的尴尬

目前作为移动设备的两大巨头 Android 和 iOS 平台, 也分别在系统级别各自集成相应的推送模块(GCM, APNs), 然而由于 Google 服务器在国内所存在的流量限制和Android 系统存在的定制多样性, 使得信息推送在 Android 设备上成为一件不得不大费周折的事情。

保证信息推送的即时性( 信息发送后能在 Minutes 级别被响应收到) 和高到达率( 信息发送后能稳定的到达对应设备) 成为信息推送所需要解决的首要问题。

目前市面上已经有很多关于 Android、 iOS、 Windows Phone 的信息推送平台, 基本上我们可以分为三种类型, 不同的推送平台都拥有着不同的优势, 然而也存在着不同层面上的问题, 无法有效保证信息推送的即时性和高到达率:

  • 官方推送 Google Cloud Messaging, 在 Android 系统级别进行支持的推送服务, 需要 Google 服务器支持。

由于是从原生 Android 的系统层次进行推送支持, 从而保证了移动端推送运转的畅通和稳定性, 可以有效且即时的响应信息, 但是由于国内对 Google 服务器的流量限制, 导致了服务端的 GCM推送服务的不确定性, 这也导致了国内的终端厂商大部分移除了 GCM 模块。 综合来说, GCM 对于主要面向国外市场的移动设备依然是一种优先考虑的选择, 对于国内设备只能作为信息推送的辅助手段, 无法承担实际信息推送的任务。

  • 手机厂商推送, 例如小米系统的 MIPush, 华为系统HuaWeiPush, 魅族系统的 FlyMePush 等.作为定制系统的手机厂商, 从自定制系统级别进行了推送支持。

国内的终端厂商往往在原生 Android系统的基础上进行了进一步定制, 实现了各自不同的终端系统, 并在各自的系统级别实现了推送服务,但是各自的推送平台仅能在各自相应的系统上作为系统级别的推送服务,例如 MiPush 仅能在小米系统上作为系统级服务存在, 在其他终端系统上退化为普通推送服务。

  • 第三方推送, 诸如极光、 个推、 信鸽、 友盟、 阿里、 百度、 小米、 华为等, 不具备系统级别的服务支持。

作为应用级的信息推送平台, 支持跨系统平台的推送服务, 且往往依赖于集成同款应用之间的相互唤醒, 具有一定的稳定性, 例如集成百度推送的移动应用可以被另一款集成该推送的移动应用所唤醒, 但是移动端推送服务被系统回收和 kill 的概率相对较高, 往往需要赋予自启权限,且很难保证推送的稳定性。

如何合理且高效的运用这些推送平台, 以保证信息推送的即时性和高到达率?

8. 多平台通道的构因

为方便理解,我举一个例子,让大家感受下

场景预设:农民伯伯种地,同一土地的酸碱度和水分程度会随时间变化,同时不同的土地含矿物质不同。农民伯伯只能种下唯一一颗种子,种子一分钟就会开会结果,而后落地成种子继续开花结果。不同的土地适合不同的种子,种子在不适合的土地上生长会减产或者死亡。

场景预设的等价描述:不同的系统平台上启动推送服务,同一系统平台适用的推送服务可能会不同,不同的系统平台适用的推送服务也会不同。系统平台上同时运行的有且只能有一个推送服务。 推送服务的选择与开启在每次应用开启时都会触发。

涉及元素:土地 = 终端设备的系统平台、种子 = 推送平台

问题: 如何保证产量 = 如何提供信息的到达率?

  • 环节1:为什么要集成多个平台? = 为什么要集成多个品种的种子成一个种子库;

土地会随着时间导致酸碱度和水分程度不同—-不同的酸碱度和水分程度适宜不同的种子—–>时间会导致 土地适用于不同的种子 —–> 农民伯伯只能种一个种子—->那么当然期望一个超级种子:它内部包含多个种子;能体现不同性质,而长成苹果 or 梨子等

  • 环节2: 在不同的土地上第一次播种,能自动变成对应最优种子 因地制宜 == 【核心】智能初始化

不同的土地含矿物质不同 — 不同矿物质含量适宜不同的种子 —> 不同的土地 导致 适用于不同的种子 —–>农民伯伯只能种一个种子—->那么当然期望一个超级种子:它内部包含多个种子;第一次在某个土地上落地时,能先抽取下土地的土壤并分析,而后根据矿物质的不同,选择出包含的最优适配的种子作为自身的性质;记录该种子标识,并在下一分钟的再次 落地开花结果中能够直接转化为对应种子,而不是再次抽取土地土壤分析;
不同的手机系统 有 不同的最佳适用推送服务,小米系统适用小米推送,华为系统适用华为推送,vivo适用百度推送 —> 应用一旦被安装在手机上,只能同时运行一款推送服务——>那么当然期望一个超级推送:它内部包含多个推送平台;第一次在某个终端安装并运行应用时,智能的在不同手机平台上从“推送库”中选择不同的推送服务,作为首要选择并缓存记录;同时下次启动应用时,直接按照此时记录的推送平台进行启动;

  • 环节3:同一土地的酸碱度和水分变化时,自动调整种子 = 【核心】智能切换

土地会随着时间导致酸碱度和水分程度不同—-不同的酸碱度和水分程度适宜不同的种子—–>时间会导致 土地适用于不同的种子 —–> 农民伯伯只能种一个种子—->那么当然期望一个超级种子:它内部包含多个种子;能够及时响应自身 减产或死亡的变化而选择出来对应的最佳种子,如果在当前土地上减产或死亡多次,能自动切换为下一个种子类型;
应用在运行的过程中,根据相关运行时状态(例如无响应和失败响应),动态的对推送服务进行切换,同时缓存记录该切换后的平台标识,下次启动应用时,直接按照此时记录的推送平台进行启动;

  • 环节4:农民伯伯觉得自动调整有些问题,他就想种苹果种子 == 命令行控制方式作为自动化切换的人工补偿。

  • 其他:所有土地都适合中土豆,但是土豆在国内不值钱,国外值钱 = 所有android的GCM是最原生的,但是GCM在国内被限制,国外可用

土地在国外则直接中土豆,否则土豆作为催化剂
手机设备支持GCM且不受限制则使用GCM,否则GCM作为辅助通道

  • 其他:不同的酸碱度与最优种子计算算法 == 优先级因子计算算法

思考下“既然各个推送平台各个有缺,为什么我不把全部推送都集成进来”

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值