其他

周江华:网易云信IM推送保障及网络优化实践

2016-09-27 周江华 网易MC



周江华,网易资深Android开发工程师,先后主导参与了易信、网易云信、网易七鱼的客户端开发工作,拥有非常丰富的功能开发以及代码优化经验。


2016年9月23日,MDCC 2016中国移动开发者大会在北京国家会议中心如期开幕。会上,网易云信Android端高级技术架构师周江华,发布了题为“云信IM推送保障及网络优化实践”的主题演讲,与所有开发者共同分享了网易云信IM在应对弱网环境、移动端硬件限制以及Android复杂的生态现状时的探索与心得。


演讲主题: 《云信 IM 推送保障及网络优化实践》


主题介绍: 在复杂的 Android 生态环境下,多种因素都会造成消息推送不能及时达到客户端。另外,不稳定的移动网络也给数据传输的速率和可靠性增加了障碍。


本文从这两个方面出发,讲述了网易云信 IM SDK 如何实现不影响用户体验的后台保活,改善的长连接加推送组合方案,以及在弱网环境大数据传输的优化实践。


演讲笔记:云小信  深度好文:7980 字 | 20 分钟阅读

阅读笔记先看问题:


1.什么是IM

2.云信 IM SDK如何实现不影响用户体验的后台保活
3.如何做长连接加推送组合方案
4.如何在弱网环境下优化大数据传输


完整笔记



对于移动APP来说,IM功能正变得越来越重要,它能够创建起人与人之间的连接。社交类产品中,用户与用户之间的沟通可以产生出更好的用户粘性。



IM是什么,有些什么要求呢?IM由两个字组成:Instant,Messaging。


即时性要求有新消息时能够立即收到,如果程序在后台,则要能立即收到推送通知。


通信则要求稳定可靠,系统不宕机,程序不崩溃,安全,传递消息时不会被拦截监听,消息不丢,顺序不乱,不重复,如果包含音视频聊天,则要求延迟低,流畅不卡顿。


要真正做出一套稳定可靠的商用级IM系统,挑战非常之多。下面,我会挑选影响IM即时性和消息稳定性最核心的两个问题,来看看都有哪些障碍,以及云信是如何去实践克服这些障碍的。




第一个问题是消息推送。iOS有 APNS做推送,相当稳定。Android本身也有GCM可以用,但是在国内有“墙”,直接就把GCM等等google的服务全部挡在外面。为了实现即时稳定的消息推送,从易信时代开始,我们就开始研究,随着时间的推移,困难和方法也在不停的变化。



对于IM,当APP退到后台,是必须还能够收到新消息提醒的,没有GCM,怎么办?在最初,唯一能做的,就是后台运行了。这几乎是接收推送的唯一途径,就算是到现在,也是最主要的途径。Android从设计上,就是支持真后台运行的,后台运行的特性也是Android现在能如此成功的原因之一,但另一面,Android长久以来一直摆脱不了的卡顿,耗电等坏名声,后台运行也拖不了干系。因此,系统对于后台运行也不会放任自流。APP想要在后台运行,需要面对不少的障碍。




第一个障碍是Android的Low Memory Killer机制。手机的内存有限,当后台运行的进程越来越多,内存剩余量也就随之减少。当有一个新的APP想要启动,如果内存不够,LMK机制就会启动,从正在运行的进程中挑选一个清理掉,释放出空间,然后新的APP就可以运行了。


LMK有两个尺度去评判。一个是进程优先级,优先级越低,被清理的可能性越大,另一个是内存占用,占的内存越多,被清理的权重自然也越大。




因为LMK机制的存在,虽然APP允许在后台运行,但同样也面临随时被清理的风险。因此,我们需要在被清理后及时的重新启动。常规的,有4种方式能够做到。


第一个是sticky service,就是在Service的onStartCommand中返回sticky flag,这样当service被kill掉后,系统会将它加入重启的pending列表,在后面合适的时机再把service重启。


第二个是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的行为,也不想打广告,所以这里对icon和名字做了模糊处理。我们看最上面,这一组是“暂不自动休眠”,因为这里列出的两个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软件来说,消息推送是一定要得到保障的,否则不明正确的吃瓜群众们会觉得是我们软件不行,连消息推送都做不好。因此,这时候我们还是应该想办法继续维持后台运行。


那么,我们又有哪些办法呢?



 

第一个是通过两次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生态环境中,无论采用什么方式,想要一直在后台运行时越来越难了,我们需要重新想另外的办法来保障消息推送。另一方面,我们作为开发者,也有义务为用户提供更好体验的软件,而不是无休止的在后台浪费用户的资源。



其实,对于IM来说,及时的消息推送和较低的电量消耗也并非不可兼得。在传统上,每个IM客户端都会各自维护一条与服务器的长连接,自己的消息和信令都在这条长连接上传递,每个APP也独自去心跳,断线重连等事情。


这种模式比较简单,不同的APP也是完全隔离的,不会互相影响。但他的缺点也非常明显,首先是做了很多重复的事情,造成了流量和电量的无谓消耗;第二是要保证所有的进程都能在后台运行很难。优化的方向也就非常明显了,那就是共享连接,现在绝大部分推送SDK也是这么做的。从这些APP里面选出一个当前正在运行的,或者是被杀概率最低的APP作为总代理,只由这个代理和服务器建立连接,一个手机上的所有其他APP都通过这个代理中转与服务器通信。但是,IM有一个很基本的要求在这种模式下无法得到满足:安全。所有APP的消息都经过代理中转,代理到服务器的连接是加密的,安全的,但到了代理这里,消息都被解开了,因此代理理论上可以看到其他所有APP的来往消息。因此,这种共享长连接的方式并不适用于IM。


虽然共享长连接方式不合适,但仍然给我们提供了一个优化的思路。在此基础上,我们想到了另外一个可以脱敏共享连接的方式:安全长连接加推送连接模式。


每个APP在使用和真正传递数据时,仍然独立使用自己的安全长连接。而当APP退到后台一段时间之后,则断开长连接,然后每个APP开启一个推送代理,并选择其中一个和云信的推送服务器建立连接,之后当APP有新消息时,就通过这个推送连接传递。APP可以自己控制发出的推送消息的安全级别,可以是包含说话人和消息内容,可以只包含说话人,或者只是一条简单的有新消息到达的提醒文案。推送到达后,如果是代理APP自己的消息,直接传递给代理APP即可。如果是其他APP消息,前面说到过,直接唤醒可能会失败,而且会导致无谓的电量消耗,所以这里并不直接将提醒传递给目标APP,而是由带来发出一条通知栏提醒。等用户去点击通知栏提醒后,才会把目标APP唤醒。


现在国内的ROM中,华为和小米的系统本来是带有推送系统,且开放给了第三方APP的。在这两个系统上,使用系统的推送通道明显会更加稳定,也更加节省资源。因此在MIUI上,从长连接到推送通道的切换流程仍然和前面的一样,只是不再使用自己的推送连接,而是将消息转发到MIUI的推送服务器,然后转给MIUI系统的推送代理,然后传递给云信的APP。华为的推送系统流程也是一样。不过现在华为和MIUI在推送实现上有一些区别,例如MIUI的通知栏提醒是在自己的推送代理里完成的,而华为却是将提醒通知交给APP自己去完成的,另外,他们的通知栏提醒的管理接口也有很多区别。在APP没有被禁用的情况下,两者都可以收到推送,而如果APP已经被禁用了,MIUI的通知栏提醒方式还可以将推送送达,而其他的推送方式则不能送达了。


以上就是在保障消息推送方面我们所能够做的所有事情了。如果以后有更多的系统开放自己的推送系统,我们也可以选择逐步接入,以提高推送到达即时性,减少资源消耗。不过相应的,我们也要承受不断加入各种系统的推送SDK,增大发布包体积的缺点。期望Android拥有统一推送平台的那一天早点到来吧。




相对于PC的网络环境,我总结的手机网络有3个特点:


第一个是慢,尤其是2G,3G网络,慢的令人发指。当我们收发图片视频这类比较大的文件时,就会看到蛋疼的菊花一圈一圈不停的转。


第二个是断,手机跟着人不停的移动,网络也不停的在切换,从wifi到移动网络,从一个基站到另一个基站,从有信号到没信号,都可能导致网络中断。有些制式的网络,接打电话也会导致数据网络断开。另外,移动基站还有NAT超时,到一个连接上长时间空闲后,基站就会默默的将连接断开,没有任何通知。


第三个是贵,这个就不用多说,看中国移动每天净赚一个亿就知道了。



在云信整个通信系统中,我们有3种类型的连接:TCP,UDP,HTTP。虽说这三个并不是同一层的协议,不过毕竟都在我们的应用的更下层,因此这么划分也无妨。3种类型的协议对应了不同的业务应用。TCP主要是用户长连接,也就是普通IM消息和信令的传输,UDP用于传输实时音视频数据流,而HTTP则主要用在音频,图片等文件的上传下载上。对于不同的业务,我们的优化的关注点会有一些不相同。


长连接是云信所有业务的基础,使用量也是最大的,因此优化也是从基础开始。 在这里我们举两个例子。



第一个是协议的选择。前面说,长连接的使用量是最大,选择一个合适的协议至关重要。如果是刚开始接触IM开发,一般会选择一些开源的协议,比如XMPP,SIP等。这是XMPP协议的一个请求样例,可以看到是一段XML格式的文本数据。这是基于SIP的SIMPLE协议的一个请求样例,可以看到是一段类似HTTP协议的文本数据。这些协议的优势在于开源,有成熟的解决方案可以使用,扩展性好,甚至还可以和其他系统互联互通,协议的可读性也非常好。但是在普遍比较臃肿,冗余字段很多,在昂贵的移动网络里面用起来会让人觉肉疼。云信采用的是私有的二进制协议,这是一个请求的数据样例,这里是把二进制数据转为了16进制显示出来,每个字节这里显示为两个字符。可以看到二进制协议的特点在于完全失去了可读性,但是,却带来极高的表达效率,相对于文本协议,可以节省非常多的数据流量。




另一个例子是登录的优化。由于移动网络经常断开,所以登录常常是心跳之外交互最多的协议了。使用量越大,优化就越有意义。一般而言,登录会经过这么几步。


第一步是LBS。这里的LBS不是经常说的基于地址位置的服务,在不同的厂商可能也有不同的叫法,反正作用都是获取服务器的IP地址。像云信这种需要提供全球服务的系统,在世界各地都要部署服务器,用户登录时,肯定要选择一台最优的服务器接入服务。通过lbs,客户端可以获取离自己最近,连通性最好的服务器连接机IP地址,服务器也可以据此做负载均衡。


拿到服务器连接机IP后,客户端就去连接该服务器。


连接成功,需要有一次握手。这个握手不是TCP的三次握手,而是为了建立安全连接,同服务器协商加密算法和加密密钥。


然后就发送登录请求,这里会带上用户认证信息,本机设备信息等数据。


登录成功之后,就是同步数据,包括离线消息,用户信息,群组信息等。一般而言,这里不会去做全量同步,而是采用基于时间戳的增量同步。


在移动网络上,每一次交互都需要比较长的时间,同时,每一次网络请求电量消耗也是很大的。所以,优化的方向就是尽量减少交互次数,而方法则是合并请求,并行操作以及省略请求。


LBS和连接这两个步骤是可以并行完成的。如果前面已经获取过LBS,这里可以有之前的缓存地址,如果没有,可以先连一个默认地址。


其次是握手和登录也可以并行操作。在握手包中,就可以把加密后的登录包直接带上去了。如果是断线重连,我们还可以简化登录,直接带上上一次登录的会话ID,一来减少服务器鉴权压力,二则可以直接带回在断线期间是否有未读消息等数据,如果没有,则能直接将同步这一步省略掉。如果有,同步也可以只做部分同步,只去拉去离线消息即可。等到APP切换到前台,才去同步其他的信息。


通过这些优化,登录时间可以降为原来的1/2到1/3,登录的流量消耗也可以节省30%左右。




实时音视频对实时性要求很高,但可以容忍一定的丢包,所以我们选择UDP私有协议来做为底层的传输协议。如果只是普通的IM消息,对网络情况其实不是太敏感,最多也就是慢一点,菊花转的久一点。但对于这种视频电话,如果网络差了,发生了经常性卡顿,或者是延迟很高,图像出现花屏,音视频不同步了,这个功能其实也就相当于废弃了。而且,音视频数据量本身也比较大,在弱网环境下发生问题的概率就更大了。


 

UDP协议是不可靠,为了提高弱网下的实时音视频的通话效果,需要使用相关方案来做QoS保障:主要包括了基于udp协议的拥塞控制、前向纠错FEC技术及相关的重传技术。同时网络层需要能够实时的探测到网络状态,作为底层调整QoS策略的依据,同时需要回调上层,来动态调整音视频的码率,做到音视频码率自适应。通过上面的QoS保障,我们实际测试在20%的随机丢包弱网环境下,音视频通话还能够正常进行。


第二是音频,我们的音频编解码主要以Opus为主,它具备高音质,高压缩率,高抗丢包等特性,非常适合移动网络。我们使用智能的jitterbuffer算法来平滑由于网络抖动引起的声音卡顿和延迟累计问题。配合PLC丢包补偿算法,来降低音频丢包后的爆音。同时,我们使用自研的高性能降噪算法,配合回声消除、自动增益和舒适噪音等音频处理算法来进一步保证音频部分的质量。


对于视频,我们使用时域分层的H264视频编码器,来降低丢包对视频流畅性的影响,同时支持动态帧率和动态分辨率,方便上层根据业务需求进行切换。现在用户对于视频的清晰度要求越来越高,我们的实时通话系统当前能够支持720p。720p下纯软件编解码对CPU开销过大,因此在可以开启硬件编解码的机器上,对于需要720p清晰度的都尽量使用硬件编解码。


由于音视频的网络优化如果全部细说,恐怕再加1个小时也讲不完,所以这里我只提了一些优化的方向供大家参考,就不一一展开了。




下面再来看看对于HTTP的优化。图片语音是IM的必需元素,而且本身数据比较大。在弱网环境下,快速的上传下载,更少的等待时间可以带来更好的用户体验。


断点续传可以减少因网络原因导致的重复传输,减少传输时间,节省流量。


图片预加载技术可以根据不用网络情况,在收到消息后,就加载不同素质的预览图片,甚至直接将原图预加载,做到用户点开即看。


上面两个是比较基础的优化措施,下面两个则比较高级一点。



图片和语音这种文件我们并没有通过长连接收发,而是通过HTTP去做上传下载。传统上通过HTTP上传时,文件会分为一片一片,传完一片,收到回包,才会穿下一个分片,一直到最终传输完成。可以看到,服务器返回ack这段时间,上传通道其实是空闲的,如果把这段时间利用起来,可以节约不少上传时间。Pipeline就是为此而来。通过重叠利用http请求的响应等待时间,加快传输速度。使用pipeline,需要修改HttpClient,同时还需要服务器提供支持。视网络具体情况,使用pipeline后,一次上传可以减少20%至30%的时间。




常规发送语音消息需要这几步,先录音,然后计算hash值,然后上传,上传完毕后,服务器计算一下校验和,通过后语音消息发送成功。在前面录制语音时,网络其实也是空闲的。把这段时间利用起来,则可以减少后面上传步骤的时间。优化后,流程就变成这样。在录制的过程中,每录完一段,就作为一个分片直接上传。直到最后录完,计算好hash,再把最后一个分片带上hash信息上传。这里除了客户端的改动,也是需要服务器支持。服务器在开始接收时,很多信息都不明确,需要开辟缓存来记录整次上传过程。对于比较差的网络,边录边传的效果会更好,毕竟纯语音的比特率并不高,基本都能做到录完就传完。



以上就是分享的全部内容。提升消息推送达到率和到达速度,优化网络利用效率,节省系统资源一直都是Android开发的核心和基础,新技术,新方法都在不停的涌现,也欢迎大家一起讨论,进步。



【推荐阅读】

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存