一、如何保证弱网情况下消息的响应速度
弱网
弱网关乎的用户的体验的问题:进一个聊天窗口,一直在转圈,体验是非常差的。
场景
- 无网
无网的状态是可以判断的,网络不正常只有DB有什么展示什么,也许消息断层。但可以理解。
- 网络良好
这种情况消息完全可以正常展示,哪怕你每次进会话请求都ok。 但要考虑到为节流、节省电量。
- 弱网
这个时候可能是客户端问题:如2g网络、3g网络、明明是4g却只有一格、两格信号;wifi明明连上,网络却不好。
解决方案
有网的情况下:必须确保数据的有序、不丢失、不重复。我们能做到请求过的消息不再请求或者减少请求。
本文的方案思想基于算法:合并区间
消息模型
例如: 我们当前聊天展示的消息是[100-200],过了很长时间现在消息是300,那我们进会话回去拉取20条,即[280-300],那我们本地块就有两个[100-200],[280-300]。向上滑动加载20条历史数据,第二个块变成了[260-300]。如果继续加载历史数据,第二个会变成[200-300]。这个时候把第二个与第一个合并,本地块只有一个,[100-300]的消息。
当下次看历史消息,消息在[100-300]这段不需要请求网络了。
不断与原来区间合并,让本地可信区间越来越大,为我们所用。
实际场景
- 推送,启动app或断网重连时服务端会推送10条消息,这个时候也需要与本地最大的块进行合并。
- 进聊天窗口,如果是合并过的,直接展示。如果没有展示过,则需要拉取网络进行展示,合并块。
- 拉取历史消息:需要与内存列表进行合并,同时需要与前一个DB块进行合并。
效果
- 看过的消息,不再需要依靠网络。减少网络交互。
- 弱网或有网时,在本地可信区间内,体验是非常好的。
二、如何确保界面消息不丢失、不重复、有序展示
场景
- 进入会话先展示DB消息(可能不足一屏),再拉取服务端消息(有可能与DB展示的消息重合,也有可能是最新的20条),正常展示后可以加载历史消息
- 搜索进入的消息是中间的消息,可以向下从顶部加载历史消息,也可以向上从底部加载新消息。
- 断网重连后,服务端会推送10条消息。
之前的做法
有个变量判断是否从顶部拉取,还是从底部拉取。拉取回来的消息需要与界面上展示的消息进行合并。如果是顶部拉取的,则插入到原数组第一个位置;如果是底部拉取,则追加到数组的末尾。
实际情况
消息是可能存在重复的。例如我拉取了DB的10条,这个时候网络回来20条,网络回来的20条可能包含DB的10条,也可能不包含与DB的10条重合的,也可能没有交集。靠插入到第一个位置或追加到末尾的位置,已经靠不住了。 追加末尾,只有插入的消息大于缓存中的最大一条才追加到末尾。导致消息丢失。
出现偶现的bug
- 重新安装登录时,打开消息,发现有部分消息没有拉取到,退出重新进入后好了
- A呼叫B(B再桌面,在线状态),然后A挂断,B点击通知消息,进入会话框,发现消息未拉取,页面空白
- 杀死进程,进入APP,快速点击某个会话,界面上会丢失消息,且无法拉取到。返回列表,重新进入后,显示正常。
- iOS 14.3系统,点击锁屏中的通知消息,输入安全密码,进入会话框,消息没有拉取到(偶现)
思考
无论是原有的消息,还是拉取的消息,都是有序的。 利用双指针算法来实现两个有序数组的合并。不用考虑消息重复,重叠,拼接是否有问题。
/*
合并两个有序的消息:如果存在重复,取其中一个,丢弃另一个
*/
- (void)mergeCacheMsgs:(BLConversation *)conversation newMsgs:(NSArray *)newMsgs{
NSMutableArray * cacheMsgs = conversation.messages;
NSMutableArray * resultMsgs = [[NSMutableArray alloc] initWithCapacity:cacheMsgs.count+newMsgs.count];
NSInteger i = 0;
NSInteger j = 0;
NSInteger k = 0;
while (i<cacheMsgs.count && j<newMsgs.count) {
NSInteger cacheMsgId = [[cacheMsgs[i] messageId] integerValue];
NSInteger newMsgId = [[newMsgs[j] messageId] integerValue];
if (cacheMsgId<newMsgId) {
resultMsgs[k] = cacheMsgs[i++];
}else if (cacheMsgId>newMsgId){
resultMsgs[k] = newMsgs[j++];
}else{
resultMsgs[k] = cacheMsgs[i];
i ++;
j ++;
}
k ++;
}
while (i<cacheMsgs.count) {
resultMsgs[k++] = cacheMsgs[i++];
}
while (j<newMsgs.count) {
resultMsgs[k++] = newMsgs[j++];
}
[conversation.messages removeAllObjects];
[conversation.messages addObjectsFromArray:resultMsgs];
[self reloadConversationMessagesTimeline:conversation];
}
复制代码
总结
- 有序数组的合并应用上了,代码清晰、易读。
- 再也不会有消息重复、重叠的问题,也不需要靠字典来去重。
- 没有变量来判断是拼接到第一个位置、还是追加到末尾。
- 时间复杂度:新的消息一般20条左右,时间复杂度为O(n+20),不会有任何性能问题。
三、使用二分算法实现消息的快速查找
场景
消息已读、已听、撤回,需要找到内存中的消息对象,并修改属性;
使用字典来实现消息查找
数组结合字典,能够实现消息的快速查找,但是需要多维护一个字典。 消息返回,发送消息,接收到推送消息,同步消息时需要先塞进字典; 退出当前会话需要移除字典中的消息。
思考
仔细分析发现,数组展示的消息都是有序的,采用二分查找,效率很高,不需要维护额外的字典。
- (BLMessage *)binSearchMsgWithMsgId:(NSString *)messageId{
NSInteger mid = 0;
NSInteger l = 0;
NSInteger r = self.messages.count-1;
while (l<=r) {
mid = (l+r)>>1;
if ([messageId integerValue]>[[self.messages[mid] messageId] integerValue]) {
l = mid+1;
}else if ([messageId integerValue]<[[self.messages[mid] messageId] integerValue]){
r = mid-1;
}else{
return self.messages[mid];
}
}
return nil;
}
复制代码
小结
在复杂的场景下,少维护一个全局变量,代码的复杂度会小很多。