iOS IM消息设计:如何提升弱网的体验;保证消息不重复、不丢失、有序到达;实现消息的快速查找

2,055 阅读4分钟

一、如何保证弱网情况下消息的响应速度

弱网

弱网关乎的用户的体验的问题:进一个聊天窗口,一直在转圈,体验是非常差的。

场景

  1. 无网 无网的状态是可以判断的,网络不正常只有DB有什么展示什么,也许消息断层。但可以理解。

  2. 网络良好 这种情况消息完全可以正常展示,哪怕你每次进会话请求都ok。 但要考虑到为节流、节省电量。

  3. 弱网 这个时候可能是客户端问题:如2g网络、3g网络、明明是4g却只有一格、两格信号;wifi明明连上,网络却不好。

解决方案

有网的情况下:必须确保数据的有序、不丢失、不重复。我们能做到请求过的消息不再请求或者减少请求。

本文的方案思想基于算法:合并区间

消息模型

消息模型

例如: 我们当前聊天展示的消息是[100-200],过了很长时间现在消息是300,那我们进会话回去拉取20条,即[280-300],那我们本地块就有两个[100-200],[280-300]。向上滑动加载20条历史数据,第二个块变成了[260-300]。如果继续加载历史数据,第二个会变成[200-300]。这个时候把第二个与第一个合并,本地块只有一个,[100-300]的消息。

当下次看历史消息,消息在[100-300]这段不需要请求网络了。

不断与原来区间合并,让本地可信区间越来越大,为我们所用。

实际场景

  1. 推送,启动app或断网重连时服务端会推送10条消息,这个时候也需要与本地最大的块进行合并。
  2. 进聊天窗口,如果是合并过的,直接展示。如果没有展示过,则需要拉取网络进行展示,合并块。
  3. 拉取历史消息:需要与内存列表进行合并,同时需要与前一个DB块进行合并。

效果

  1. 看过的消息,不再需要依靠网络。减少网络交互。
  2. 弱网或有网时,在本地可信区间内,体验是非常好的。

二、如何确保界面消息不丢失、不重复、有序展示

场景

  1. 进入会话先展示DB消息(可能不足一屏),再拉取服务端消息(有可能与DB展示的消息重合,也有可能是最新的20条),正常展示后可以加载历史消息
  2. 搜索进入的消息是中间的消息,可以向下从顶部加载历史消息,也可以向上从底部加载新消息。
  3. 断网重连后,服务端会推送10条消息。

之前的做法

有个变量判断是否从顶部拉取,还是从底部拉取。拉取回来的消息需要与界面上展示的消息进行合并。如果是顶部拉取的,则插入到原数组第一个位置;如果是底部拉取,则追加到数组的末尾。

实际情况

消息是可能存在重复的。例如我拉取了DB的10条,这个时候网络回来20条,网络回来的20条可能包含DB的10条,也可能不包含与DB的10条重合的,也可能没有交集。靠插入到第一个位置或追加到末尾的位置,已经靠不住了。 追加末尾,只有插入的消息大于缓存中的最大一条才追加到末尾。导致消息丢失。

出现偶现的bug

  1. 重新安装登录时,打开消息,发现有部分消息没有拉取到,退出重新进入后好了
  2. A呼叫B(B再桌面,在线状态),然后A挂断,B点击通知消息,进入会话框,发现消息未拉取,页面空白
  3. 杀死进程,进入APP,快速点击某个会话,界面上会丢失消息,且无法拉取到。返回列表,重新进入后,显示正常。
  4. 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];
}

总结

  1. 有序数组的合并应用上了,代码清晰、易读。
  2. 再也不会有消息重复、重叠的问题,也不需要靠字典来去重。
  3. 没有变量来判断是拼接到第一个位置、还是追加到末尾。
  4. 时间复杂度:新的消息一般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;
}

小结

在复杂的场景下,少维护一个全局变量,代码的复杂度会小很多。