安卓推送这件小事

安卓推送这件小事

今天来讲讲推送这件小事,事虽小,要做好却不容易。

推送难,难于上青天。

我们在讨论 Android 手机上的推送时,大多数情况是在说集成第三方推送,因为即使是像微信这样的大厂,也需要厂商加到启动白名单里才能保持在线。

iOS 手机使用 APNs(Apple Push Notification service)进行推送,而 Android 手机,也是有 GCM(Google Cloud Messaging)作为 Google 官方的推送支持的,但是在国内需要翻墙才能使用,并且需要手机安装了 Google Service ,条件比较苛刻。

这样一来,国产手机的推送就成了个问题,也带了机会。

微信由于有国际版,将 GCM 作为辅助公共通道,但仅用于激活微信自己的 Push 通道,并没有通过 GCM 来传递数据,这点也是为了复用心跳的优化策略和数据处理逻辑。

GCM 最新版本叫 FCM(Firebase Cloud Messaging)

推送的实现方式

总结一下几种推送实现方式,其中有些我们只要了解即可,因为属于历史解决方案,现在已经被废弃掉了。

1. 轮询

客户端定期询问服务器有没有新的消息,这种方式最大的缺点就是性能实时性的矛盾,轮询时间过长和过短都不好。

2. 短信

这种方式还没出生就不被允许了,首先运营商不会配合,其次拦截手机短信本身就是一个高危权限,大多数用户不会买单。

3. 长连

目前最通用的方案,客户端与服务端建立 TCP 长连,定时发送心跳包保活,有新消息时服务端通过该长连通道进行推送。

这里再简单说明一下长连和心跳包。

长连接就是建立连接之后,双方互相发送数据,,发完了也不主动断开连接。

但是在某些情况下长连会断开,问题就在断开这件事上,而且这件事必须是由客户端知道,因为客户端是可以重连服务器的,服务器却没法再联系上客户端。这样才确定了心跳包必须由客户端发给服务器。所以心跳包的作用就是告诉服务端客户端还活着,如果服务端挂了,客户端能知道,所以保活说的是保持两边都活着。

上面说的某些情况中, NAT 超时算是一个典型的例子。

因为 IPv4 的 IP 量有限,运营商分配给手机终端的 IP 是运营商内网的 IP,手机要连接 Internet ,就需要通过运营商的网关做一个网络地址转换(Network Address Translation,NAT)。简单的说运营商的网关需要维护一个外网 IP、端口到内网 IP、端口的对应关系,以确保内网的手机可以跟 Internet 的服务器通讯。大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断。所以长连接心跳间隔必须要小于 NAT 超时时间(aging-time),如果超过老化时间不做心跳, TCP 长连接链路就会中断,服务器就无法发消息给客户端,只能等到客户端下次心跳失败后,重建连接才能取到消息。

有了长连,即使在休眠模式下,有推送消息过来,也能唤醒 Android 系统,这是由系统机制决定的,我们这里只要知道结论就好。

想要了解更多的可以去看文末的参考资料

推送的指标

在接入推送服务时有几个核心指标需要考量:

1. 在线率

在线率 = 在线用户数 / 总用户数

推送服务后台保持在线的方法

  • Push 进程常驻后台,需要用户手动让应用常驻
  • 共享连接通道的方式,比如极光或者个推,通过共享连接,当应用有推送到达时,唤起该应用

显然,后者在体验上更加接近 GCM 。

2. 到达率

到达率 = 实际到达数 / 目标用户数

在线数 -> 目标用户数 -> 成功下发数,如果后端的计算或调用出现问题这两个数据就会不准确

在线数 -> 实际到达数 -> 展示数,数据收到后,是否展示要看用户有没有打开该应用的允许通知的开关,可以通过如下方法判断

notificationManagerCompat.areNotificationsEnabled();

3. 耗电量

耗电量受到很多方面的影响,如果收到推送比较多,打开应用比较频繁,耗电量自然也会上去不少,但这个用户是可以接受的。以下几个耗电量的因素用户是比较反感的:

  • 应用间互相唤醒产生的耗电,因为这个耗电是别的应用的,用户本来没有意图要去打开
  • 错误重试造成的耗电,重试策略的优化包括重试时间的累加和重置

推送选型

上面提到,我们这里聊的推送,是第三方推送,那有开发者要问了,为什么不自己做推送?自己做不是不行,但需要考虑几个问题:

  • 开发成本问题, ROI 是否可以接受
  • 如果不加入白名单,那么一旦应用被彻底杀掉,是没人给你吟唱复活魔法(互相唤起)的

第三方推送主要有厂商推送和非厂商推送

  • 华为、小米、魅族推送
  • 个推、极光、友盟
  • 阿里、腾讯、百度

其中,选型的几个因素

  • 厂商推送通知是否系统通道(所有厂商支持)
  • 厂商推送透传是否系统通道(仅魅族)
  • 非厂商推送的市场占有率(影响共享连接互相唤起的概率)

如何在 Android 手机中查看某个应用使用的是什么推送?

adb shell dumpsys activity services | grep igexin

可以看到,这几个应用都在使用个推

大众点评、宝宝树、饿了么、滴滴、简书、领英、 WPS Office 、格瓦拉。

推送接入

如何解耦

由于各个推送 sdk 接口定义不同,为了减少耦合,我们采用多 module 的形式进行接入。

  • app
  • jikepush
    • PushServiceImpl
  • push_厂商1
    • PushPlatformImpl
  • push_厂商2
    • PushPlatformImpl
  • push_非厂商
    • PushPlatformImpl
  • jikecore
    • PushService
    • PushPlatform

在 app 层通过注册的方式添加需要的 PushPlatform ,之后通过 PushService 接口来进行调用,具体的启动和切换的实现放在 PushServiceImpl 。

其实大部分推送平台的接口标准都差不多,无非是命名上有差异,所以我们用 PushPlatform 这个接口来屏蔽这种差异。

对于推送启动和切换的操作,放在 PushService 中。

当启动某一个推送服务的时候,就关掉其他的推送服务,后台始终只保持一个推送服务。

registeration id

通常我们启动一个推送服务后,会收到一个 registeration id(以下简称 reg id),用这个 reg id 和我们自己的服务器进行绑定。

这里有个需要注意的地方,绑定的操作我们需要调用2次。

  • after PushService.start() 因为已经接收到 reg id 后有些推送不再触发 receive reg id
  • after receive reg id 首次启动推送是异步收到 reg id 的

收到 reg id 之后,推送 sdk 会自己保存下来以便下次使用,但有时候我们使用 sdk 的方法获取的时候无法获取到,究其原因是 reg id 的保存不是我们自己做的,因此, reg id 这么重要的东西我们自己也要存。

推送需要的系统权限

推送 sdk 需要获取手机的 imei 号作为合成 reg id 的必要元素,需要以下两个权限

  • Manifest.permission.READ_PHONE_STATE
  • Manifest.permission.WRITE_EXTERNAL_STORAGE

启动推送服务的时候需要判断权限。

推送的切换策略

建议选择手机 rom 而非手机型号作为切换条件,这样可以解决部分用户刷机的问题,比如 Nexus 手机刷了个 MIUI 的情况。

获取到 rom 信息后,有3种推送切换策略

策略一

客户端根据 rom 信息自动选择使用哪个推送

  • 优点:无后端工作量,不需要切换
  • 缺点:一旦某个推送挂了一天,无法临时切换到其他推送

策略二

客户端上报 rom 信息,后端选择使用哪个推送

  • 优点:灵活切换推送
  • 缺点:切换推送重新绑定 reg id 有个时间差

策略三

使用推送 sdk 自己的集成方案,当 sdk 自己的推送服务离线时,切换到厂商推送

  • 优点:最大程度保证推送的稳定性
  • 缺点:集成方案本身的不稳定性影响了推送的稳定性

推送类型

推送类型分为通知和透传

通知

  • 厂商到达率有优势
  • 开发成本较高,需要适配不同厂商的接口标准

透传

  • 可以自己解析、展示、跳转,灵活性高
  • 开发成本较低,适配一次就够(通常通过 json )
  • 一些厂商的透传到达率没有厂商优势

集成方式

一般我们集成推送 sdk 有两种集成方式

  • maven 集成
  • 手动集成

从维护的角度来说, maven 集成的维护成本要小于手动集成,但是我这里还是推荐手动集成,主要有以下几个原因。

maven 集成的方式通常 sdk 会要求使用 manifest placeholder 的方式进行 appid appkey appsecret 的注入,但是这种方式需要在 app 层的 build.gradle 去注入,比较耦合。

maven 集成还有一个缺点,准确来说这是 sdk 开发商的问题,总会打包一些我们不需要的资源文件,其实我只需要一个 jar 包而已,并不是整个 aar 啊。

手动集成第一次写 manifest 比较麻烦,但是后面只要替换 jar 和 so 文件就好了,因为 manifest 一般不经常变化,除非有重大版本更新。

推送的展示

接下来就要说说通知 Notification 了,这个东西经过厂商的各种定制,适配起来也有不少麻烦。

厂商的推送样式差异

部分厂商

  • 不支持 NotificationCompat.BigTextStyle (这都能不支持)
  • 不支持 NotificationCompat.MediaStyle (这个还可以理解)
  • 不支持 Action Button
  • 不支持 Ticker
  • 自定义样式高度限制

原生系统 Android 版本的推送样式差异

  • 4.x large icon 需要是方的
  • 5.x 6.xsetColor 设置底色和 small icon 配合形成 large icon
  • 7.x setColor 影响标题颜色

通知的跳转

  • 透传:采用 Url 方式进行跳转
  • 通知:采用 Intent 参数的方式进行跳转

推送这件小事就说到这里了。

展望未来

最近,由泰尔实验室牵头的安卓统一推送研讨会正在进行中,即刻上有一个安卓推送服务统一进展的提醒,我们一起关注安卓推送生态的变化吧。

用一句广告语描述厂商和 app 的相爱相杀:你好,我也好。

参考资料

Android推送技术研究

Android微信智能心跳方案


文章被以下专栏收录
31 条评论
推荐阅读