聊天室项目,也被称为即时通讯(IM)。

其原理是服务器是一直在启动状态的线程,不断的从客户端(App)获取消息,收到消息后,进行类型和发送目标判断,以发送到群组或者单聊的方式,客户端收到消息后进行界面的展示。

如果要自己开发即时通讯类的 App,那么必须得要后台,但是现在很多第三方工具已经给我们集成好了所有需要调用的接口工具,比如极光、个推

长连接

运作方式

客户端在与服务器建立了TCP连接之后,客户端定时向服务器发送心跳包确认,有消息的时候,服务器直接通过这个已经建立好的TCP连接通知客户端。
而正所谓长连接,就是大家建立连接之后不主动断开,双方互相发送数据,发完了也不主动断开连接,之后有需要发送的数据就继续通过这个连接发送。

连接中断的情况

由于现在我们使用的是手机,因此我们的网络状态在很多时候都不是稳定的,就很有可能会导致连接被切断(切断后就需要再次建立连接),比如说NAT设备超时(路由器就是一个NAT设备),网络状态切换,DHCP的租期等等。

心跳包

心跳包,顾名思义,就是用于判断当前的客户端是否还有心跳(也就是是否还活着),如果没有心跳了,服务器就会关闭掉这个连接,用于节省带宽。

在一个正常的情况下,服务器会面对很多个连接。而一旦这个连接建立之后,是不会主动断开的,就会一直占据到服务器的资源,而很有可能过了一段时间之后有的连接就已经失效了,就可以释放掉该连接,而之后如果客户端需要再连接的时候重新建立连接就好了。

而这里有个点是很重要的,如果客户端心跳间隔是固定的,那么服务器就会在连接闲置超过这个时间还没收到心跳时,可以认为对方掉线,关闭连接。如果客户端心跳会动态改变,例如在微信的心跳方案中的话,就应该在服务器设置一个最大值,超过最大值如果还没收到就认为对方掉线。

同时服务器通过TCP连接主动给客户端发消息出现写超时的时候,也可以直接认为对方掉线了。

而之所以在这一小段中提到Android微信智能心跳方案,是因为对于整个心跳包来说,理论是比较简单的,而重要的是这个心跳时间间隔的设定。
发送心跳包势必要先唤醒设备, 然后才能发送, 如果唤醒设备过于频繁, 或者直接导致设备无法休眠, 会大量消耗电量, 而且移动网络下进行网络通信, 比在wifi下耗电得多. 所以这个心跳包的时间间隔应该尽量的长, 最理想的情况就是根本没有NAT超时, 这也就是网上常说的长连接, 慢心跳.

而超时的这个情况肯定是不能避免的了,这个时候就可以参看一下上面的那篇心跳方案,它是关于如何让心跳间隔逼近NAT超时的间隔,同时自动适应NAT超时间隔的变化。

设备休眠

首先Android手机有两个处理器,一个叫Application Processor(AP),一个叫Baseband Processor(BP)。AP是ARM架构的处理器,用于运行Linux+Android系统;BP用于运行实时操作系统(RTOS),通讯协议栈运行于BP的RTOS之上。非通话时间,BP的能耗基本上在5mA左右,而AP只要处于非休眠状态,能耗至少在50mA以上,执行图形运算时会更高。另外LCD工作时功耗在100mA左右,WIFI也在100mA左右。一般手机待机时,AP、LCD、WIFI均进入休眠状态,这时Android中应用程序的代码也会停止执行。

Android为了确保应用程序中关键代码的正确执行,提供了Wake Lock的API,使得应用程序有权限通过代码阻止AP进入休眠状态。但如果不领会Android设计者的意图而滥用Wake Lock API,为了自身程序在后台的正常工作而长时间阻止AP进入休眠状态,就会成为待机电池杀手。比如前段时间的某应用,比如现在仍然干着这事的某应用。

网上有说使用AlarmManager,因为AlarmManager 是Android 系统封装的用于管理 RTC 的模块,RTC (Real Time Clock) 是一个独立的硬件时钟,可以在 CPU 休眠时正常运行,在预设的时间到达时,通过中断唤醒 CPU。

尤其需要注意的一点就是:如果不进行特别的设置,Android会在一定时间后屏幕变暗,在屏幕变暗后约几分钟,CPU也会休眠,大多数的程序都会停止运行,从而节省电量。而CPU休眠的话就存在一个很严重的问题了,很有可能你设置的一些程序会由于CPU休眠而无法执行,例如Timer和TimerTask,这个时候就可以使用系统的AlarmService来执行轮询。因为虽然系统让机器休眠,节省电量,但并不是完全的关机,系统有一部分优先级很高的程序还是在执行的,比如闹钟,利用AlarmService可以定时启动自己的程序,让cpu启动,执行完毕再休眠。

而对于Socket来说,同样也存在一个很严重的问题,当屏幕关了几分钟之后连接会被断开,但实际上网络又是连通的,这是因为CPU休眠了,此时就可以设置一个PARTIAL_WAKE_LOCK来保持CPU不休眠。(这是从网上找的一种解决方案,可能会有更优)

保活方式

保活的思路一般有3种:

  • 被别人拉活: 自己进程完全死亡的时候,让其他应用拉活
  • 主动拉活: 自己进程被系统杀死的时候,挣扎一下,看能不能自己活过来
  • 降低进程杀死概率: 一般通过提高进程优先级来达到降低系统在杀死进程的时候,杀死自己 App的概率

目前常见的保活共有下面几种:

系统广播保活
前台 Service 保活
双进程保活
Native 进程保活
JobSchedule 保活
一像素Activity 保活
联盟保活
WaterMelon

被别人拉活

不是我们自己拉活自己,是其他应用拉活我们,这种一般不好控制频次

系统广播保活(<21)

通过监控系统广播的方式,达到保活效果

低版本 Android 可以注册静态广播,注册后,当系统发出相应广播的时候,会拉活已经监听了该广播的进程,达到保活效果。这种方式多存在于5.0以及5.0 以下 和一些有 bug 的系统中(华为8.x)

IM即时通讯架构图_保活

JobSchedule 保活 (>= 21)

5.x 以后,Android 提供了JobSchedule,PushJobService会定时被拉活,但是在某些系统上还是有问题

各种联盟保活

本着互利共赢的原则,联盟保活产生了,相互拉活,A应用活跃的时候,拉 B 应用;B应用活跃的时候,拉 A 应用。然后组成一个大联盟,要活大家一起活,要死大家一起死。

粘性 Service

onStartCommand返回 START_STICKY ,系统会在 service 杀死后,重启该 service。屡试不爽,有时候8.x 手机也能生效( 可以试试 Process.killProcess,这种情况还能被系统拉活)

IM即时通讯架构图_客户端_02

主动拉活

双进程保活(21 <= os < 25)

原理为A,B个进程均锁住一个文件a,b,然后A进程尝试去读取 b 文件的锁,同理 B 进程尝试去读取 a 文件的锁。如果 A 进程拿到了 b 文件的锁,说明 B 进程已经释放 b 文件的锁,同时也说明 B进程凉凉了,这时候 A 进程就应该尝试去拉活 B 进程。如果 B 进程拿到了 a 文件的锁,同理。

系统杀多个进程是有时间差的,在这个时间差内,如果能监听到另一个进程被杀死,并且能拉活该进程,相当于保活成功

Native 进程保活 ( < 21)

同样是监听其他(push)进程 是否存活,如果不存活就拉活该进程

WaterMelon(>=25)

watermelon实际上是双进程保活的升级版本,双进程在高版本上效率比较低,常常在系统杀死A的时候,A 进程还没有监听到 B 进程已经死了或者还没来得及拉拉活 B 进程,导致保活失败

watermelon 的所有操作均在 native 完成,包括所以跨进程操作,都在 native 完成,这样能提高了很多效率。

提高进程优先级

前台 Service 保活( < 24)

系统杀死进程时,会低优先级杀死有前台 Service 的进程。这种保活是以提高进程优先级的方式来达到保活目的。并且这种方式起前台 Service 后,并不会弹出横幅.

原理:

对于 API level < 18 :调用startForeground(ID, new Notification()),发送空的Notification ,图标则不会显示。

对于 API level >= 18:在需要提优先级的service A启动一个InnerService,两个服务同时startForeground,且绑定同样的 ID。Stop 掉InnerService ,这样通知栏图标即被移除。

PushSdk 里面,通过启动NotifyIntentService并且设置为前台,然后把 NotifyService 也设置为前台,2个 service 绑定了同一个 id 的 Notification,然后 stop 掉NotifyIntentService,然后这时,这个 Notification会被隐藏,这时候 NotifyService还是前台 Service。

一像素 Activity ( <26)

同样是通过提高进程优先级的方式达到保活效果

在用户锁屏的时候,启动一个 Activity,只有一像素大小,影响不了用户操作,当用户开屏的时候,finish 这个 Activity