调研背景
Android在国内没有办法利用系统级的推送服务,再加上第三方rom对于原生的各种定制(这里的定制并无褒贬之意)所以国内的开发者就需要选择一些成熟的推送平台做推送服务了。目前的推送平台种类繁多,本文将给出推送平台的具体对比和调研结果,方便后来人进行选择。
注意:本报告写于2016年04月25日,本文内容可能随着时间和技术的发展而产生错误和偏差。
调研信息一览
推送平台
参与测试的机型:
- MI2 (android 5.1.1)
- N5 (android 6.0.1)
- MI4 (miui 7.2.4)
- OPPO (colorOS 2.1)
- MX4 (flymeOS 4.2.2.1c)
- Samsung (android4.1.2)
因为测试的结果在相同的rom上表现一致,所以下文的测试结果做了简化,屏蔽了机型的细节。
调研结果
个推
我先排除的是个推,排出它并非是因为他不好,而是因为个推本身就是收费平台,其优点就是服务全,数据颗粒度细,对开发者的响应也比较及时,但因为其收费的属性让我无法将其作为选择之一。
阿里推送
阿里推送采用的是阿里云的一整套服务,但因为大公司的原因,所以会强制植入one sdk,这个东西真的是全家桶级别的东西,而且联系技术的方式是通过阿里旺旺让人心累。大而全的东西一向不是我的选择,而且未来阿里推送可能会有付费的趋势,因此我对它不做考虑。
友盟推送
友盟是做数据的老牌厂商,但其给我的感觉总是不舒服,之前做过友盟反馈,接入和使用总觉得不够优雅。但就推送来说,它的后台界面和提供的小工具、文档都是最美观实用的。
值得一提的是友盟的sdk引入了okhttp、okio等第三方库,我是认为完全没有必要的。我认为一个独立的sdk应该尽可能的保持轻量,过多的自带库会引起很多问题。

推送后台
友盟采用了共享连接的技术方案,即多个采用友盟的App可以相互共用长连接,保证你的app被杀死后只要用户打开了采用友盟推送的app,你的app就可以利用友盟兄弟来接收推送。这个技术也被很多推送平台所采用,是一个很不错的方案。

共享长连接(将长连接挂在高德上)
测试结果:
| 安卓原生 | MIUI | ColorOS | flymeOS | Samsung | |
|---|---|---|---|---|---|
| app处于前台 | √ | √ | √ | √ | √ |
| 返回键退出 | √ | √ | √ | √ | √ |
| 杀掉app | √ | × | × | √(有延迟) | √ |
| 手机重启后 | √ | × | × | √(有延迟) | √(有延迟) |
| 打开兄弟App | - | × | × | × | - |
(√:能收到推送,×:收不到推送,-:无意义的测试)
测试结果表明友盟对于原生的支持较好,可以做到在原生rom上不死,在flyme上面能有一定的几率复活,只是复活后有一定几率丢失之前的消息。

友盟进程

内存使用情况
sdk配置文件如下:

通过代码分析,友盟的一些服务是新开进程的(push),所以会有两个进程,这也是用来提升service存活率的通用方法。

广播接收器
我们可以明确的知道,友盟推送在监听到(可能在不同的rom有监听不到的情况)开机、网络改变、应用卸载这样的广播后就会自动复活,以便再应用被杀死后仍旧可以实时接收远端的推送信息。

兄弟应用
在测试中我发现在MIUI上即使打开了兄弟app(高德地图),我也收不到推送。后台工具告诉我长连接的service确实挂到了兄弟app上,至于为什么没办法接收到推送就不得而知了。
最后需要说下友盟的缺点,在service被杀死后有一定概率丢失消息,刚注册的设备是无法收到消息的,需要过一段时间才能收到测试消息,再过一段时间才能收到正式消息,总觉得不稳定。
小米推送
小米的推送有一个很大的优势就是在MIUI上会直接使用系统的长连接,只要系统不死,那么你的app就能接收到推送。当你的MIUI用户达到了一定的量级,可以说你必须使用小米的推送。在小米推送的更新日志中,我们可以发现它在非MIUI的rom上也采用了兄弟app共享长连接的方案。

更新日志
需要吐槽的是小米的推送后台经常出现打不开的情况,复现频率极高。
| 安卓原生 | MIUI | ColorOS | flymeOS | Samsung | |
|---|---|---|---|---|---|
| app处于前台 | √ | √ | √ | √ | √ |
| 返回键退出 | √ | √ | √ | √ | √ |
| 杀掉app | √ | √ | × | × | √ |
| 手机重启后 | √ | √ | × | × | √(有延迟) |
| 打开兄弟App | - | - | × | × | - |
(√:能收到推送,×:收不到推送,-:无意义的测试)
从结果来看小米推送是符合其文档描述的,能够在原生和MIUI上有良好的表现,所以小米推送可以被列入选择列表。

原生rom上的内存信息

MIUI上的内存信息
我发现小米在MIUI上会自动采用系统的服务,也就是说不用自身建立一个service,所以理论上要更加轻量省电。

小米推送配置文件
从代码来分析,小米会监听网络切换的广播来重启service,对于MIUI层面可能会有一个特殊的方法,不启动额外service,却可以handleMessage。

兄弟应用
对于能否通过兄弟应用来接收推送信息,我的测试结果是无法收到,不知道是不是bug。我只能说通过这样的方式来提升消息的接收率是不靠谱的。
极光推送
极光早期专注于推送服务,可以说沉淀很深。极光对开发者的响应可以说是做的最好的。Android文档里说到了各个功能的目的,不会像oneSDK那样偷偷加入一些权限。文档也是走AS的风格,较为现代。不足之处就是后台比较古老,样子不好看,发送操作有一个延迟的过程,没有友盟做的好。
极光也支持兄弟应用相互拉起的功能,从代码上看是1.8.0之后的版本开始支持的。
第三方系统收不到推送的消息的原因(摘自极光文档)
由于第三方 ROM 的管理软件需要用户手动操作
小米【MIUI】
自启动管理:需要把应用加到【自启动管理】列表,否则杀进程或重新开机后进程无法开启
通知栏设置:应用默认都是显示通知栏通知,如果关闭,则收到通知也不会提示
网络助手:可以手动禁止已安装的第三方程序访问2G/3G和WIFI的网络和设置以后新安装程序是否允许访问2G/3G和WIFI的网络
MIUI 7 神隐模式: 允许应用进行自定义配置模式,应用在后台保持联网可用,否则应用进入后台时,应用无法正常接收消息。【设置】下电量和性能中【神隐模式】
华为【Emotion】
自启动管理:需要把应用加到【自启动管理】列表,否则杀进程或重新开机后进程不会开启,只能手动开启应用
后台应用保护:需要手动把应用加到此列表,否则设备进入睡眠后会自动杀掉应用进程,只有手动开启应用才能恢复运行
通知管理:应用状态有三种:提示、允许、禁止。禁止应用则通知栏不会有任何提醒
魅族【Flyme】
自启动管理:需要把应用加到【自启动管理】列表,否则杀进程或重新开机后进程无法开启
通知栏推送:关闭应用通知则收到消息不会有任何展示
省电管理: 安全中心里设置省电模式,在【待机耗电管理】中允许应用待机时,保持允许,否则手机休眠或者应用闲置一段时间,无法正常接收消息。
VIVO【Funtouch OS】
内存一键清理:需要将应用加入【白名单】列表,否则系统自带的“一键加速”,会杀掉进程
自启动管理:需要将应用加入“i管家”中的【自启动管理】列表,否则重启手机后进程不会自启。但强制手动杀进程,即使加了这个列表中,后续进程也无法自启动。
OPPO【ColorOS】
冻结应用管理:需要将应用加入纯净后台,否则锁屏状态下无法及时收到消息
自启动管理:将应用加入【自启动管理】列表的同时,还需要到设置-应用程序-正在运行里锁定应用进程,否则杀进程或者开机后进程不会开启,只能手动开启应用
总之就是一个很实在的东西,对于android的权限和设计思路它也无能为力,它只能尽可能做到高的推送送达率。
| 安卓原生 | MIUI | ColorOS | flymeOS | Samsung | |
|---|---|---|---|---|---|
| app处于前台 | √ | √ | √ | √ | √ |
| 返回键退出 | √ | √ | √ | √ | √ |
| 杀掉app | √(有延迟) | × | × | × | √(有延迟) |
| 手机重启后 | √ | × | × | × | √(有延迟) |
| 打开兄弟App | - | × | × | × | - |
(√:能收到推送,×:收不到推送,-:无意义的测试)
关于延迟:
我认为是极光的服务被杀死,无法进行实时消息的接收,直到复活后才会接收到消息,所以产生了消息延迟。这点在android原生和三星的rom上有明显的规律。在三星的手机上杀掉app->关屏->点亮屏幕后服务就会自动重启,我认为这个就是服务重启的方式之一。

内存信息

配置文件
极光的配置文件中详细说明了什么是用来测试的,什么是必选的,什么是可选的,而且是否将服务放入独立进程都是一个建议选项而非强制。整体的demo和文档给人的感觉是不流氓,却无奈。

兄弟应用
我打开了极光官方提供的兄弟app,但并不能接收到我测试app的推送消息。
结论
- 在原生和MIUI上小米推送有很强的优势
- 在三星的ROM上小米和友盟难分伯仲
- 在魅族的Flyme中友盟有一定概率复活
- 在OPPO的ColorOs中三家均无突出表现
整体结论是国外的rom推送成功率高,国内rom推送成功率低。
编码实现
目前我采用的是集成多个第三方推送,然后根据机型来注册不同的推送服务。后端不控制推送的平台,由客户端保证一条推送仅展示一次。因为友盟的推送必须要一个module,所以我索性把第三方推送平台都整个成了一个module。
注意:友盟推送和友盟反馈会有冲突,现在友盟反馈已经不再维护了,所以强烈不建议使用友盟反馈。而且友盟推送和阿里的产品有冲突,所以如果产生了冲突,要下载去UTDID的SDK。

友盟和阿里冲突解决
友盟反馈和友盟推送冲突的解决方案(我才知道友盟反馈原来偷偷集成了推送服务):
compile('com.umeng:fb:5.4.0') {
transitive = true
exclude group: 'com.umeng', module: 'message'
}
module一览

module中的libs文件夹
libs中集成了友盟所需要的so包(引入一个友盟的推送,要引入一堆东西,还各种冲突)。此外还有小米、信鸽推送的jar包。

libs
一般我们不会把so放在lib中,都会采用新建一个main/jniLib文件夹来放,友盟采用了另一种方法来方,因此在build.gradle中出现了这样一段代码:

gradle
友盟文档中说,要引入我的推送,你就必须引入httpClient,因此我们必须在build.gradle中引入已经被谷歌废弃的httpClient:
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
// http://dev.umeng.com/push/android/integration
// 如果是android6.0以上的api编译,需要在PushSDK的build.gradle文件的android{}块内添加:
// useLibrary 'org.apache.http.legacy',并把compileSdkVersion的版本号改为23。
useLibrary 'org.apache.http.legacy'
defaultConfig {
minSdkVersion 8
targetSdkVersion 23
}
}
友盟还使用了square家的东西,所以必须要引入以下两个依赖:

Paste_Image.png
到此为止,我终于引入了友盟的sdk。小米和信鸽的sdk我就不说了,上面的图中也有,一个jar包搞定。
Manifest中的权限
因为各大推送平台都有自己的权限,但是如果混合到一起,以后想删除某个平台的代码都很难删,最终也不知道整个权限是谁加入的,有什么作用。因此我把各个平台的权限做了独立,这样做虽然在编译的时候会有重复定义权限的警告,但是并不影响最终的包,这点我通过反编译已经证实了,请放心。
Mainfest中的特殊key
在配置文件中因为用到了key和具体项目的包名,而我希望整个push的module保持独立性,所以我对于特殊的key做了留空处理,让引入整个lib的项目自己在gradle中填写。
以友盟为例,上面我把友盟需要的key做了留空,信鸽和小米也做了同样的处理。在使用中,我们会看到需要具体包名的情况,而android中applicationId是一个默认关键字,所以用到包名的地方可以用它来做占位。
接着在依赖这个lib的module中做配置:
defaultConfig {
// manifest中隐式提供的一个变量,第三方的库会用到这个,必填
// http://tools.android.com/tech-docs/new-build-system/user-guide/manifest-merger#TOC-Placeholder-support
applicationId "kale.push.test"
minSdkVersion 16
targetSdkVersion 23
versionCode 1
versionName '1.0.0'
manifestPlaceholders = [
tencentAuthId : "tencent1234",
umengAppKey : "11111111111111111111111",
umengMessageSecret : "22222222222222222222222",
xgAccessId : "33333333333333",
xgAccessKey : "44444444444444"
]
}
编写推送管理类
因为集成了多个平台,所以东西多,东西一多就需要管理类。管理类做的事情很简单,注册服务,接收推送消息,展示通知。
public class PushManager {
@StringDef({PushType.MI, PushType.UMENG, PushType.XG})
public @interface PushType {
String MI = "MI", UMENG = "UMENG", XG = "XG";
}
public static void register(Context context) {
registerXgPush(context);
registerMiPush(context);
registerUMengPush(context);
}
public static void handleRegisterResult(Context context, @PushType String type,
boolean isSuccess, @Nullable String resultMsg) {
if (isSuccess) {
} else {
P.e(TAG, type + "推送注册失败,参数:%s", !TextUtils.isEmpty(resultMsg) ? resultMsg : "无");
}
}
}
因为是由客户端来保证同一条推送消息仅展示一次,所以需要一个数据库来存放近期(存放近5天)的通知,在展示层面肯定要通过推送平台的透传来做。将透传的信息拿到,看下整个推送是否已经展示过,如果没展示过就解析出通知的信息,弹出一个notification。解析的工作无外乎就是json解析,各个公司有各个公司的标准,没啥可说的,下面就贴下弹出通知的代码。
public static void showNotification(Context context, @NonNull String message, @NonNull String target) {
Uri uri = Uri.parse(target);
Intent intent = new Intent(context, MainActivity.class)
.putExtra(IS_FROM_NOTIFICATION, true) // 标识是从通知跳过来的
.setData(uri)
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
int code = (int) SystemClock.uptimeMillis();
PendingIntent pIntent = PendingIntent.getActivity(context, code, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// 删除时
Intent deleteIntent = new Intent(context, NotificationDeleteService.class);
// 删除时开启一个服务来做统计
PendingIntent deletePIntent = PendingIntent.getService(context, code, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(context)
.setAutoCancel(true)
.setSmallIcon(R.drawable.app_icon)
.setContentTitle(context.getResources().getString(R.string.app_name))
.setTicker(message)
.setContentText(message)
.setDefaults(Notification.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentIntent(pIntent)
.setDeleteIntent(deletePIntent)
.build();
NotificationManagerCompat nm = NotificationManagerCompat.from(context);
nm.notify((int) SystemClock.currentThreadTimeMillis(), notification);
}