我所在的项目,最近正在打造直播功能,并且也已经上了两期直播了,总体来说坑还是比较多的,因为你要定制播放器、聊天室、点赞、弹幕等等的,这些种种功能,组成一个直播间,其中的性能优化是非常重要的。这篇文章主要聊聊我解决聊天室相关问题的填坑之路。
第一次直播(无限刷新)
我们的名厨APP,是在2.1.0版本第一次加入的直播,实际上整体的开发时间是比较充足的,也经过了几轮测试,但在进行了第一场线上直播后,确实发现了很多影响用户体验的bug。这其中非常严重的一个问题就是,在接收到大量聊天消息时,处理聊天列表刷新耗费了太多内存和性能,以至于iPhone6s都会因为内存不足而杀掉APP进程。
为了方便快速的完成直播中的聊天功能,我的建议是在开发团队没那么强能力和时间的前提下,尽量选择一些三方服务商,我们APP中直播所用到的聊天室,选择了融云的服务。
具体集成起来是很方便的:1、在AppDelegate中初始化融云SDK并配置自定义消息;2、当前APP使用用户获取融云Token,并用Token连接融云服务;3、加入聊天室;4、在指定Controller中设置融云代理,接收消息。
接收消息的方法为:
// 监听消息接收的方法
- (void)onReceived:(RCMessage *)message left:(int)nLeft object:(id)object {
}
第一个参数message,为接收到的聊天消息;第二个参数nLeft,为剩余的历史消息数量;第三个参数object,是接收消息的监听key值。按融云文档的说法是,同一时间会收到大量消息,nLeft代表你接收到的消息的剩余数量,当并发接收消息数量巨大的情况下,可以在nLeft等于0的时候,再刷新聊天列表,似的,我是这么做的,if (nLeft == 0) { self.chatTableView reloadData }。
在经过了几轮的测试后,APP安心的上线了,虽然会觉得肯定还有测试不到的地方,会有些突发的bug出现,但聊天这块肯定不会有问题。结果万万没想到,我们的直播太火爆了,不仅服务器负载过高,聊天这里也因为消息接收过多,导致聊天列表刷新过于频繁而引发APP占用内存暴增、卡顿,甚至闪退。
第一次改进(每秒刷新)
既然问题出现了,就赶紧修复吧,我们直播是每周一期的,所以要赶在下次直播前,快速找出新的解决方案。
既然刷新过于频繁,那就改为定期的刷新吧,所以新的方案为,每次接收到消息,就存储到数据源中,但每1s只刷新一次,这种方案还是比较靠谱的,毕竟不管你接收到多少消息,都只会每1s刷新一次,从之前的狂刷到现在的定期刷,效果还是比较明显的。但伴随而来的问题还是比较突出的,采用UITableView reloadData的方式进行刷新,当数据源数据量巨大的情况下,就算每1s刷新一次,但这1s的这次刷新,也会有一定的线程堵塞。
如图是我们名厨APP的直播页面,当数据源中的数据到达500条时(iPhone6s),在刷新列表的时候,就会有明显的卡顿,例如图中的红色标记的功能,在持续使用这些功能的时候,你会发现每隔1s都会卡顿一下。显然这样的体验是不好的。
第二次改进(每秒在适当的时候刷新)
因为数据源中数据过多的时候,只要刷新就会卡顿,所以尽量控制刷新的次数,既然固定了每秒刷新,这是不可改变的,因为加长这个时间,则消息的实时性就会大打折扣。所以要针对这1s的刷新做一定的条件限制。限制条件如下:
<li>当聊天列表在最底部的时候才刷新,因为聊天列表展示新消息的策略是,聊天列表在最底部的时候,接收到新消息并滚动到底部,不再最底部的时候,不用滚动到最底部。
<li>当当前直播页面展示在用户眼前的时候,才刷新,也就是说,在用户全屏的时候、点击右下角弹出提问列表的时候、当前页面需要登录权限弹出登录页面的时候不会刷新。
加入以上的限制,就能保证当前页面展示在眼前、并且聊天列表滚动到最底部的时候才进行刷新。
第三次改进(缓存数据源的引入)
但通过以上的改进后,还存在一种卡顿,就是只要你停留在聊天列表最底部的时候,每秒都会刷新一次,用户看直播、聊天的目的就在于消息的实时性,大部分用户,都会停留在聊天的最底部以接收最新的消息。但每秒的刷新,还是过于频繁,如何降低这个频率,是很重要的。所以,为什么要每秒都进行刷新呢?为什么不回归到最古老的方案,在接收到消息的时候再刷新呢?所以缓存数据源诞生了。
现在直播页面,有两个数据源,一个咱们叫它正式数据源,用于存放当前聊天列表展示的数据,另一个数据源为缓存数据源,缓存数据源用于存放新接收到的聊天消息。
则整个聊天的功能循环就变成了这样:
1、接收到新的消息,并将新消息存储在缓存数据源中;
2、每1s,都将缓存数据源中的数据放置到正式数据源中;
3、刷新聊天列表并清空缓存数据源;
4、重复上面的流程以反复接收新的消息;
引入了缓存数据源,就可以针对第二次改进,多加入一个限制,就是缓存数据源中有数据的时候,才进行上面的循环。
第一次完善(insertRow的引入)
做了上面这些修改,就能彻底摆脱卡顿吗?答案是NO,因为所有的根源都在UITableView reloadData这个方法上。而且既然引入了缓存数据源,则数据的插入,就更好控制了,每次将缓存数据源加入到正式数据源的时候,直接实用UITableView的insertRowsAtIndexPaths方法,将这些新数据追加到聊天列表后面就好了。
第二次完善(聊天数量的限制,缓存机制的完善)
当然以上的所有方案综合在一起,当数据量巨大的时候,还是会有堵塞主线程的现象存在,主要表现在iPhone6s以下的机型上。所以,历史消息的数量,是需要进行限制的。
在观察了斗鱼、映客这些成熟的直播软件的处理方案后发现,在历史消息达到一定数量后,就会将最老的消息删除掉,这也就说明了,历史消息数量不是无限的增大下去的。
所以历史消息应该有一个最大上限,综合测试后,在5s上也能流畅运行的最大上限为500左右。但具体策略就是超过500就删除到500吗?显然不是,如果这样处理,当历史消息到达500后,则之后手机的处理量就一直维持为500这个数量上了,而且在做删除消息的操作时,你要向正式数据源结尾追加缓存数据源数据,并从最前面删除最老的历史消息,这个时候,再更新聊天列表,就要用到tableview的reloadData方法,所以如果历史消息始终维持在500这个级别,就会有很多次的reloadData方法被调用,卡顿还是不可避免的。
所以针对历史消息的数量的限制,我引入了两个关键数字,一个是maxNum(假设为500),一个是minNum(假设为300),代表正式数据源和缓存数据源汇总后,消息数量的两个阶段,并针对不同阶段有不同的处理。
1、当汇总后的正式数据源中的消息数量不超过300时,会将新加入的缓存数据源消息追加到聊天列表中,而不是刷新聊天列表。
2、当汇总后的正式数据源中的消息数量超过500时,会从正式数据源的第一条消息开始删除,删除到只剩300条消息后,刷新聊天列表。
3、当汇总后的正式数据源中的消息数量介于300到500之间时,按少于300处理,每次总数到达500后,循环这个处理,保证聊天消息数量控制在minNum和maxNum之间,也就保证了性能。
以上处理方法,保证了始终有maxNum-minNum(也就是500-300=200)这么多的空间可供使用缓存数据源去追加消息(insertRow的方式)。
maxNum的调整:针对手机的性能进行调整,尽量保证iPhone5s也能承受的数量。
minNum的调整:minNum越小,则insertRow处理的聊天列表刷新越多,性能越好,minNum越大,则reloadData出现的频率更快,则会有更多的不定期卡顿出现。当然minNum也不是可以无限的小下去,比如定为0,这样的后果就是,一旦聊天消息总数到达500,则所有消息都被删除了,则用户看不到任何历史消息了。
我们公司最后定下来的比例是200、500。
最终综合上面的方案,上线了一个新的版本。
第二次直播
在进行了第二场直播后,我的心终于能放下了,因为聊天不会再卡顿了,并发收到大量消息的时候,也处理的游刃有余。
类似场景
一、以生活中的场景为例,就好比一个壮汉搬运货物,他的力气是很大的,比如搬运可乐,要搬到100m以外的柜台上,运用第一次直播的方案就是,可乐是源源不断出货的,但壮汉每次只能拿一瓶,并运到100m以外,虽然壮汉能力是有的,但他的能力都浪费在了运输的途中,有点大材小用了。手机也是,手机接收消息是很简单的,但很多性能和内存都浪费在了刷新聊天列表上。现在换到使用第二场直播的方案,壮汉一直在等,每收到一瓶可乐,就将可乐放到箱子中,等箱子装满了,或者到了壮汉可以承受的重量的时候,再将可乐运到100m以外,这样的壮汉只要平时歇着,偶尔出下力就可以了。手机也一样,大部分时间都在接收消息,但实际并没有刷新聊天列表,只在特定场景,刷新很多数据。跟饿么吗的外卖送餐员一样(为什么要做这个比喻!因为午餐时间到,正在订餐...),他都是取到很多份再统一配送的,而不是取到一份就送一份。
二、另外,说到缓存,大家想到的肯定就是在线视频了,视频的处理也是缓存形式的,有缓存和没缓存绝对是两种体验。如果不进行缓存的处理,则播放器每时每刻都在请求视频流,这无疑很耗费性能,还很容易导致卡顿。加入了缓存,就可以一劳永逸,在视频播放到缓存快没有的时候,再请求一段视频流,缓存下来就好了。
总结
整个事件,还是比较提心吊胆的,因为毕竟直播是个大模块,而且并发量比较巨大,自己在参考融云的Demo的时候,没有验证并发量巨大的情况下是否适用,同时也没有考虑到直播的真实应用场景,而且第二版虽然顺利修复了聊天室中的问题,但赶上要圣诞节了,苹果马上就要放假了,能否顺利上线新版本也是比较担心的。所谓吃一堑长一智,知耻而后勇,以后再碰到类似问题,还是多长点心吧。
总得来说,以上聊天的处理,我是没在网上找到类似的处理方案,有做直播聊天的,可以稍微参考下,有什么疑问或是更好的解决方案,可以给我留言。而解决问题,主要的目的还是找到问题出在哪,是哪些原因导致的,只有找对了原因,对症下药,才能更高效的解决问题。