Android推送原理

这里将会介绍Android上的推送原理

Android推送原理

这里将会介绍Android上的推送原理

定义

在用户未打开或打开的情况下,客户端主动的向用户推送服务器最新消息

系统级别的推送

系统级别的推送就代表着,在任何时候都可以进行推送,也不用担心会被系统杀死

Google

C2DM

全称:Cloud to Device Messaging 云端推送

这个是基于Push方法进行获取消息的,也就是说C2DM服务负责处理例如消息队列等事务,并向运行设备的应用发送这些消息

这张图网上全都是😓

  • 优点

    C2DM提供了一个简单的、轻量级的机制,允许服务器可以通知移动应用程序直接与服务器进行通信,以便于从服务器获取应用程序更新和用户数据

  • 缺点

    缺点很明显,这些都是需要依赖于Google服务,但是国内大部分的机型都阉割了Google服务,这就导致无法使用,并且连接上Google还需要进行网络代理

推送的几种方式

Push

从字面意思就可以知道,这是一种服务端主动向客户端进行发送消息的模式,也就是客户端跟服务端之间会维护一个长连接

Pull

这是一种由客户端主动向服务端请求消息的模式

  • 缺点
    1. 成本大,需要自己实现与服务器之间的通信,例如消息排队等;
    2. 客户端中需要一直保持着请求服务的存活,并且还要间隔一定时间就要向服务端发起请求消息,这样也会导致消息不能及时到达

SMS

这种模式是基于Push的方式,如果服务端上有新的消息,那么服务端会发送一个类似于短信的信令到客户端,客户端接受到信令之后,就会主动向服务端请求消息

  • 优点
    1. 客户端消耗小
    2. 可实现完全的实时操作
  • 缺点
    1. 成本较高,因为目前来说,很难找到免费的短消息发送网关来实现这种方案,只能通过向运营商缴纳相应的短信费用

推送的设计

一般来说,进行推送最好的方式就是:移动网络长连接(Push)。因为手机平台本身的特点,电量、网络、流量的限制移动互联网应用在设计上跟传统PC 上的应用很大不一样。所以需要根据手机平台的特点,尽量节省流量、电量,同时还要保证数据能及时的到达客户端。在手机平台上,常用的方法有如下2种

定时查询

这种就是上面推送方法所讲的Pull,在性能、电量等各方面来说都是使用实时推送(Push)会更好一点,所以这里就不着重讲解

实时推送

这种就是手机和服务端之间维护一个TCP长连接,当服务端有新数据的时候,就实时推送到客户端。但是Push的实现和维护成本都会比较高,在移动网络下长连接的维护,相对也有一些技术上的难度。下面就来讲讲移动网络下长连接的维护

移动网络长连接

这里就先要讲一下移动网络的特点

移动网络

现在绝大多数的IP地址应该都还是IPV4,但是IPV4的IP数量有限,运营商分配给手机的的IP是运营商内网的IP,手机想要访问互联网,就必须得先通过运营商的网关,去做一个网络地址转换。简单来说就是运营商的网关需要维护一个外网IP、端口内网IP、端口对应的关系,以确保内网中的手机可以随时的与互联网的服务器通信

但是大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰NAT 表中的对应项,造成链路中断。所以说要想维护好长连接就得避免链路中断

NAT表:NAT映射表存在在路由器里面,实现的是私有IP与公有IP之间的转换;实际上可能是你家一台路由器拥有一个公网IP,而所有连在这台路由器上的所有设备都共享一个公网IP

客户端长连接的维护

为了不让NAT链路中断,所以我们需要定时发送心跳包,来刷新NAT表项,防止淘汰。在Android上定时运行任务的常用方法有如下两种

Timter/其他计时器的实现

Timeter类可以用来执行需要循环执行的任务,但是Timeter有个问题,就是它是需要使用WakeLock来保持CPU的唤醒,这样会大量消耗手机电量,所以这种方法不太可行。其他计时器的实现也是一样,比如说是通过While和Thread.sleep来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class PushService : Service() {

var url = "你请求的网络地址"

private val pushThread: PushThread by lazy { PushThread() }

override fun onCreate() {
pushThread.start()
super.onCreate()
}

override fun onBind(intent: Intent?): IBinder? {
return null
}

class PushThread : Thread() {

override fun run() {
while (true) {
Thread.sleep(10000)
// TODO
}
}
}
}

上述代码就是通过开启了个Service,在onCreate中开启了一个线程,线程中每隔10秒向服务器发送一次请求

AlarmManager

而AlarmManager是Android系统封装的用于管理RTC模块,RTC(Real Time Clock)是一个独立的时钟,在CPU处于休眠状态下也可以正常运行,在预定时间到达时,会中断CPU休眠,唤醒CPU。这不是完美符合我们的预期吗,没达到预定时间的时候CPU正常休眠,到达时间则唤醒工作,这样一来手机的耗电量问题也得到了改善

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class PushManager(private val context: Context) {

private val mAlarmMgr: AlarmManager by lazy { context.getSystemService(Context.ALARM_SERVICE) as AlarmManager }

fun startRequest() {
cancelRequestAlarm()
mAlarmMgr.setRepeating(
AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + 1000,
10000,
getOperationIntent()
)
}

private fun cancelRequest() {
mAlarmMgr.cancel(getOperationIntent())
}

// 发送广播的意图
private fun getOperationIntent(): PendingIntent {
val intent = Intent()
intent.action = "push"
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}

AlarmManager是系统服务,所以不能用一般的构造方法去构造,可以使用getSystemService()来获取系统级别的服务

接下来我写了一个发起请求的方法,内部是通过PendingIentent去通知广播,具体的请求应该在广播中处理

1
2
3
4
5
6
7
8
9
fun startRequest() {
cancelRequestAlarm()
mAlarmMgr.setRepeating(
AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + 1000,
10000,
getOperationIntent()
)
}

里面调用了AlarmManager设置重复执行定时任务setRepeating(),setRepeating()接收4个参数

参数 属性
第一个参数标识闹钟类型 AlarmManager.ELAPSED_REALTIME_WAKEUP(从手机开机后的时间,包含了手机睡眠时间)/AlarmManager.RTC_WAKEUP(使用系统设置中的时间)
第二个参数表示任务的首次执行时间 这里就与第一个参数相关了,如果第一个参数设置为AlarmManager.ELAPSED_REALTIME_WAKEUP,那么当前时间就为SystemClock.elapsedRealtime()。如果第一个参数设置为AlarmManager.RTC_WAKEUP,那么当前时间就为System.currentTimeMillis()
第三个参数为两次任务执行的间隔 传入你想要的间隔时间
第四个参数为对应的相应操作 一般这里都是去发送广播,然后在广播中执行相应的操作。当然也可以getActivity()、getService()

这样就可以最大程度上可以解决电量消耗问题和定时器被系统回收问题

至于电量消耗问题还可以进一步的解决,可以执行一些策略

  • 在移动网络下每隔5分钟去请求一次信息
  • 在Wifi情况下每隔2分钟去请求一次信息
  • 在熄屏情况下停止请求
  • 熄屏一分钟则杀死推送进程

推送服务持久化

比如说我们的推送相关的服务是在Service上,那么客户端如何防止用户在使用第三方或者系统本身的清理后台杀死进程呢?

这里就可以用到Android中的ActivityManager内部类RunningAppProcessInfo

RunningAppProcessInfo

RunningAppProcessInfo是用来记录当前系统中进程状态的类,下面讲介绍一些常用值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* Constant for {@link #importance}: this is a persistent process.
* Only used when reporting to process observers.
* @hide
*/
public static final int IMPORTANCE_PERSISTENT = 50;

/**
* Constant for {@link #importance}: this process is running the
* foreground UI.
*/
public static final int IMPORTANCE_FOREGROUND = 100;

/**
* Constant for {@link #importance}: this process is running something
* that is actively visible to the user, though not in the immediate
* foreground.
*/
public static final int IMPORTANCE_VISIBLE = 200;

/**
* Constant for {@link #importance}: this process is running something
* that is considered to be actively perceptible to the user. An
* example would be an application performing background music playback.
*/
public static final int IMPORTANCE_PERCEPTIBLE = 130;

/**
* Constant for {@link #importance}: this process is running an
* application that can not save its state, and thus can't be killed
* while in the background.
* @hide
*/
public static final int IMPORTANCE_CANT_SAVE_STATE = 170;

/**
* Constant for {@link #importance}: this process is contains services
* that should remain running.
*/
public static final int IMPORTANCE_SERVICE = 300;

/**
* Constant for {@link #importance}: this process process contains
* background code that is expendable.
*/
public static final int IMPORTANCE_BACKGROUND = 400;

/**
* Constant for {@link #importance}: this process is empty of any
* actively running code.
*/
public static final int IMPORTANCE_EMPTY = 500;
  • 一般数值大于IMPORTANCE_SERVICE(300)的进程都表示着,已经长时间没用,或者为空了

  • 一般数值大于IMPORTANCE_VISIBLE(200)的进程都是非可见进程,也就是在后台运行了

大多数第三方的清理软件都是清理大于IMPORTANCE_VISIBLE的值,也就是说只要我们进程的值小于IMPORTANCE_VISIBLE(200),就不会被清理软件清理掉。在Serivce中,有一个方法叫startForeground()这个方法的只用是以可见进程的模式启动Serivce(也就是进程值会小于200)。但是这样会在设备通知栏持续显示一个通知,别急这也有解决办法,只需要讲id传为0即可避免显示通知。相应的想要关闭Service也得用对应的stopForgeround()

1
2
3
4
5
6
7
8
9
#Service
//如果你的服务已经启动(通过{@link Context#startService(Intent)}运行),那么
//还可以让这个服务在前台运行,提供正在进行的
//在此状态下显示给用户的通知。
//默认情况下,启动的服务是后台的,这意味着它们的进程不会被给出
//前台CPU调度(除非该进程中的其他东西是前台的),
//如果系统需要杀死它们来回收更多的内存(例如在
//网络浏览器),它们可以被杀死而不会造成太大的伤害。
startForeground()