对于移动APP来说,IM功能正变得越来越重要,它能够创建起人与人之间的连接。社交类产品中,用户与用户之间的沟通可以产生出更好的用户粘性。
在复杂的 Android 生态环境下,多种因素都会造成消息推送不能及时达到客户端。另外,不稳定的移动网络也给数据传输的速率和可靠性增加了障碍。
本文详解了 网易云信IM SDK在应对弱网环境、移动端硬件限制以及Android复杂的生态现状时的探索与心得.如何实现不影响用户体验的后台保活,改善的长连接加推送组合方案,以及在弱网环境大数据传输的优化实践。
带着思考阅读
- 什么是IM
- IM SDK如何实现不影响用户体验的后台保活
- 如何做长连接加推送组合方案
- 如何在弱网环境下优化大数据传输
IM的定义
IM由两个字组成:Instant,Messaging。
即时性要求有新消息时能够立即收到,如果程序在后台,则要能立即收到推送通知。
通信则要求稳定可靠,系统不宕机,程序不崩溃,安全,传递消息时不会被拦截监听,消息不丢,顺序不乱,不重复,如果包含音视频聊天,则要求延迟低,流畅不卡顿。
要真正做出一套稳定可靠的商用级IM系统,挑战非常大。
第一个问题是消息推送。iOS有 APNS做推送,相当稳定。Android本身也有GCM可以用,但是在国内有“墙”,直接就把GCM等等google的服务全部挡在外面。为了实现即时稳定的消息推送,从易信时代开始,网易就开始研究,随着时间的推移,困难和方法也在不停的变化。
对于IM,当APP退到后台,是必须还能够收到新消息提醒的,没有GCM,怎么办?在最初,唯一能做的,就是后台运行了。这几乎是接收推送的唯一途径,就算是到现在,也是最主要的途径。Android从设计上,就是支持真后台运行的,后台运行的特性也是Android现在能如此成功的原因之一,但另一面,Android长久以来一直摆脱不了的卡顿,耗电等坏名声,后台运行也拖不了干系。因此,系统对于后台运行也不会放任自流。
APP在后台运行所面对的四大障碍
第一个障碍是Android的Low Memory Killer机制。手机的内存有限,当后台运行的进程越来越多,内存剩余量也就随之减少。当有一个新的APP想要启动,如果内存不够,LMK机制就会启动,从正在运行的进程中挑选一个清理掉,释放出空间,然后新的APP就可以运行了。
LMK有两个尺度去评判。一个是进程优先级,优先级越低,被清理的可能性越大,另一个是内存占用,占的内存越多,被清理的权重自然也越大。
因为LMK机制的存在,虽然APP允许在后台运行,但同样也面临随时被清理的风险。因此,网易需要在被清理后及时的重新启动。
第二个障碍是alarm,闹钟,有循环闹钟和一次性闹钟两种,在闹钟触发后启动对应的组件。
第三个障碍是在Manifest文件中静态注册的Receiver,通过监听各种系统事件,比如开机,网络变化,mount/unmounts等,在这些事件发生时启动组件,因为这种方式会造成在这些事件发生时系统容易卡顿,在7.0里面,Android增加了限制。
第四个障碍是JobScheduler,这是在5.0里面新增的,允许APP在特定事件发生时做一些动作,比如充电,切换到wifi等。
虽说无论怎么做,APP终究免不了一死,但通过对照LMK的评判准则,还是可以降低APP被清理的概率的。第一个就是降低进程的内存占用。如果采用单进程的模式,由于进程中包含了UI,Webview,各种图片缓存等内容,内存必然会居高不下,降不下来。IM软件一般都会采用双进程甚至多进程的策略,将push进程独立出来,在push进程里只处理网络连接和push业务,不参与任何其他业务逻辑,更不包含任何UI。
以下是网易云信Android SDK的架构,按照分层的结构模式设计。最底下青色的一层是push层,他就是作为一个独立进程运行的。他只负责处理网络长连接的相关工作,比如安全加密,心跳,鉴权,封包解包等工作,所有业务逻辑都交给UI进程的服务模块去做。来看一下云信demo的进程内存占用情况。上面一个是主进程,看第四列PSS的数据,内存占用是50M左右,下面一个是push进程,内存占用只有10M左右。当处于后台时,push进程被清理概率比UI主进程低很多。
降低被清理概率的第二个手段是提升进程优先级。先看这个例子,这是绿色守护的一个截图,最上面是“暂不自动休眠”,因为这里列出的两个APP的状态都是工作中,对应的进程优先级是“可视进程”。但这两个APP并没有提供桌面小部门在运行,也没有指示前台服务的常驻通知栏提醒,事实上,他们就只是在后台运而已。通常进程退到后台后,其进程优先级类型就变成了较低的后台进程,而不是这样的“可视进程”,他们是通过什么方法来提升优先级,降低被清理概率呢?
Android在设计前台服务上有一个漏洞,通过两个服务配合就能创建一个隐形的前台服务。这里有两个已经启动的service: A和B。先在A中调用startForeground,提供一个NOTIFY_ID, 然后A就变成前台服务了,同时有了一个ID为NOTIFY_ID的常驻通知栏提醒,然后网易在B中也调用startForeground,提供相同的NOTIFY_ID, B也变成了前台服务,因为两个通知ID相同,因此这一次就不会创建新的通知栏提醒了。然后再在A中调用stopForeground,A的前台属性被取消,同时,常驻通知栏提醒也会被移除,但是,service B并不会受到任何影响,还是前台服务,这是再把A停掉,进程就只剩下前台服务B了,进程也变成了前台进程,但用户不会有任何感知。
正常来说,做了上面3步之后,进程就能够比较稳定的在后台运行了。
但在有些情况下,推送进程却永远起不来。跟踪之后发现,除了系统能够杀掉后台运行的进程外,用户也一样是可以杀死进程的。用户杀掉进程的方式有两种,一种是在最近任务列表中将app划掉,这种方式和系统杀掉进程效果相同。另外一种就是通过这里的force stop,这种方式比系统清理更加彻底。不但app正在运行的进程会被清理,app当前在重启列表中的待重启服务,注册的各种闹钟,事件监听组件等都会被移除,除非用户在主动点击或者系统重启等外力,app没法再自己重新爬起来了。
在有些国内的像MIUI一类的ROM上,用户从最近任务列表中将app移除,效果竟然也是force stop。正常来说,如果是用户主动操作,app本身也不应该再重启了。但有些时候这个并不是用户本意,况且,对于IM软件来说,消息推送是一定要得到保障的,否则不明正确的吃瓜群众们会觉得是软件不行,连消息推送都做不好。
APP安卓进程保活的好办法
第一个是通过两次fork加上exec的方式。两个fork后,第一次fork的进程退出,第二次fork出来的进程就会被init进程领养。用户此时再force stop,因为这个进程复进程是init,而不是Zygote,因此不会被清理。由于这个进程还是从android进程fork出来的,带有android运行时环境以及复进程的资源,所以内存会比较大,这里可以再通过exec命令,打开一个纯linux的可执行文件,开启一个daemon进程,其内存占用大概只有100K+,对用户也就完全无感了。利用这个后台进程,可以定时的将push进程拉起来。此种方式只在5.0以下的系统中有效,在4.4及以上系统中,SELinux特性是强制开启的,exec没有权限执行,同时在5.0之后,ActivityManager在做force stop以及移除任务时,只要是具有相同的uid的进程,就会全部清理掉,不再漏掉没有虚拟机环境的进程。
最后一个后台保活的手段是一个大杀器。因为前面所列的所有保活手段都不是那么保险,因此想出来这么一个互相保活的方式。当一个APP进程起来后,他就去扫描已安装的应用列表,看看有没有自己的兄弟姐妹,比如说同一个长的APP,或者是集成了同一个SDK的APP,如果有,就把这些APP都拉起来。这也就是现在比较出名的“全家桶”方案。虽说这种方法确实能够带来较高的后台存活率,特别是那些大厂和应用广泛的sdk,但是这种方式对于用户的伤害也非常大,如果有后台推送的必要性,且不会对用户体验造成太大伤害时,此方式还可以使用,但如果只是为了推广告,则会对用户造成伤害,反过来,也可能会导致用户直接卸载APP。
现在各种手机管理软件都会对这种全家桶唤醒方式做限制,特别是在root过的机器上,可以做到完全切断这些唤醒路径。同时,很多ROM也会自带管理软件,限制后台运行和后台唤醒,以便给设备换取更长的续航。在目前国内的Android生态环境中,无论采用什么方式,想要一直在后台运行时越来越难了,需要重新想另外的办法来保障消息推送。另一方面,作为开发者,也有义务为用户提供更好体验的软件,而不是无休止的在后台浪费用户的资源。