推送定义: 在任何时间地点服务端向客户端推送一条消息, 如果客户端在线或者下次上线, 就能接收到该消息
通常想到的实现方式是: 轮询 tcp 长连, 其目的都是让服务端和客户端之间时刻保持在线状态
对于客户端而言,
轮询: 无非是写个线程按某种配置的时间间隔无限循环去请求服务端是否有新的消息, 当有新的消息, 就提醒给用户
tcp 长连: 与服务端建立 tcp 长连, 这样服务端就可以直接给客户端发送消息了
上面两种方式都是比较消耗资源的, 而这里我们使用的是另外的一种方式来实现的
udp 无连接: 其实现基本原理为, 客户端创建 socket 并向服务端发送 udp 包, 服务端接收到请求连接的 udp 包之后, 将客户端 id 与 ip 和端口号进行绑定, 当要向某个客户端 id 发送消息时, 找到其对应的 ip 和端口号, 然后将消息组装成 udp 包发送即可, 其大致流程如下:
而对于客户端需要解决的如下几个问题:
1. 如何维护客户端 id 与路由之间的绑定关系;
2. 如何延长客户端的在线状态 (app 保活)
3. 客户端性能考虑
下面将针对这几点进行逐步介绍
维护客户端与路由的绑定关系
这里我们需要了解一下 NAT, 所谓 NAT 就是, 在局域网内部网络中使用内部地址, 而当内部节点要与外部网络进行通讯时, 就在网关处, 将内部地址替换成公用地址, 从而在外部公网上正常使用
所以当发送 udp 包到服务器时, 服务器拿到的 ip 和端口其实是客户端在路由上映射的 ip 和端口, 所以我们需要维护路由上的映射表, 这时就需要定期发送心跳包, 以保证路由上的映射关系不会被清除掉
1. 维护心跳包
主要作用是防止 NAT 超时, 和探测连接是否断开, 并根据实际情况进行重连操作, 其流程如下:
2. 网络监测
当网络切换和变化时, 会导致映射关系失效, 所以我们需要做相应的监测和重连
1. 监听网络变化, 当网络类型变化或者断开后重新连接上时, 进行重连
2. 定期监测 ip 地址变化, 如果监测到 ip 地址有变化时, 则进行重连
APP 保活
app 保活是一个老生常谈的话题, 经过广大开发者多年累积与筛选, 互联网上相关文章层出不穷, 目前看来不算什么硬梗, 大多都按套路出行, 这里也套路套路
当应用退到后台时, 为了确保推送通道能够正常使用而不被系统回收, 通常会做一些进程保活和拉活的策略, 大体分为以下几类:
1. 利用系统 Service 机制拉活
将 Service 设置为 START_STICKY, 利用系统机制在 Service 挂掉后自动拉活
如下两种情况无法拉活:
1.Service 第一次被异常杀死后会在 5 秒内重启, 第二次被杀死会在 10 秒内重启, 第三次会在 20 秒内重启, 一旦在短时间内 Service 被杀死达到 5 次, 则系统不再拉起
2. 进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉, 无法重启
经测试, 在绝大多数手机任务进程中, 手动杀掉进程后, 是不会自动重启的 (符合情况 2)
2. 设置进程优先级
当进程退到后台后, 系统在回收资源时, 会根据进程优先级, 进行资源回收, 优先级越高越晚被回收, 所以尽可能地提高 service 进程的优先级, 可以在一定程度上保障其在后台时不被系统回收
进程按照重要性分为如下 5 类:
1. 前台进程 (Foreground process)
2. 可见进程 (Visible process)
3. 服务进程 (Service process)
4. 后台进程 (Background process)
5. 空进程 (Empty process)
一般的后台进程进程属于第 4 类, 我们可以通过 setForeground 将 service 提升到 2, 但是这种方案必须与一条可见的通知绑定在一起, 而这种体验显然不能被用户接受
当然我们可以通过 new Notification() 的方式设置一个空的通知, 与之绑定, 但只在 4.3 以下版本才有效, 如下:
- ```
- if (Build.VERSION.SDK_INT < 18) {
- service.startForeground(1001, new Notification());//API < 18 , 此方法能有效隐藏 Notification 上的图标
- }
- ```
神奇的开发者们发现了一种通过多实现一个 TmpService, 在 MainService 和 TmpService 两个 service 中同时发送具有相同 ID 的 Notification, 然后干掉 TmpService, 这时 Notification 会自动消失, 而 MainService 的优先级已经被提升到 2 了, 从而达到了目的缺陷是: 需要开发者在 manifest 多配置一个 service, 可能在不久的将来也会被官方更新掉, 不建议采纳
3. 利用系统广播拉活
该方式是通过 AndroidManifest.xml 注册一些特定的系统广播方式, 来拉活进程, 但是这种方式在高版本中已经被官方封掉了, 所以也成了一堆没必要的配置
当然我们可以在代码中添加了相关的系统广播注册同时在监听进程死掉时发送的广播, 测试在某些低版本的手机上有效
- ```
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(Intent.ACTION_USER_PRESENT);
- intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
- intentFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
- intentFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
- intentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
- intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
- ```
4. 使用 AlarmManager
使用 AlarmManager 定时发送心跳定时检查 ip 变化
但是经测试当系统休眠时, AlarmManager 也停止了工作, 且在不同 sdk 版本上需要采用不同的 set 方式, 如下:
- ```
- AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
- if (pingPendingIntent != null) {
- am.cancel(pingPendingIntent);
- }
- pingPendingIntent = PendingIntent.getBroadcast(MobSDK.getContext(), 0, new Intent(ALARM_ACTION_PING),
- PendingIntent.FLAG_UPDATE_CURRENT);
- final long nextTime = SystemClock.elapsedRealtime() + interval * 1000L;
- if (Build.VERSION.SDK_INT >= 23) {
- am.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTime, pingPendingIntent);
- } else if (Build.VERSION.SDK_INT >= 19) {
- am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTime, pingPendingIntent);
- } else {
- am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTime, pingPendingIntent);
- }
- ```
5. 进程间相互拉活
当某台手机上有多个应用都在使用 sdk 时, 可根据用户在后台配置授权后, 选择性的进行相互之间的拉活
6. 利用 native 进程拉活
网络中流传的一种利用 Linux 中的 fork 机制创建 Native 进程, 在 Native 进程中监控主进程的存活, 当主进程挂掉后, 在 Native 进程中立即对主进程进行拉活
在 Android 中所有进程和系统组件的生命周期受 ActivityManagerService 的统一管理而且, 通过 Linux 的 fork 机制创建的进程为纯 Linux 进程, 其生命周期不受 Android 的管理
这种方案在网上流传已久, 听说在 5.0 以上版本也不成立, 且需要额外添加本地代码编译 so, 无形的添加了 app 体积, 不采纳
7. JobScheduler 和账号同步机制拉活
这种两种方式同样需要在 AndroidManifest.xml 中注册相关配置和权限, 版本限制, 效果一般
8. 将应用加入厂商或管理软件白名单
9. 第三方 push 通道接入:
GSM: 国内不支持
小米推送华为推送
性能考虑
APP 性能也是老生常谈的话题, 总结其出发点和最终的目的都是为了减少用户流量内存占用电量消耗等等方面的优化, 以达到省电省流量且界面流畅的终极目标
在开发时, 大致可从如下几个方面思考和稍加注意:
1. 减少网络请求次数, 缩小网络中传输数据的体积, 像推送这种主动的操作, 可通过自定义数据传输协议来控制流量的消耗
2. 控制唤醒屏幕, 避免开启没必要的线程, 合理释放资源, 减少 IO 操作, 避免使用广播机制, 减少 cup 占用时间等等方面来控制内存和电量的消耗
来源: https://juejin.im/post/5a8fc7dbf265da4e7071d9b6