58即时通讯移动端架构的实践总结
1.背景
在58集团的各业务线中,存在大量需要交易双方沟通交流的场景,比如,客户咨询商家产品信息,售前售后简单的答疑和维权等。此时需要较为完善的即时通信(IM)解决方案,但是由于58集团针对不同的商户和使用场景有多个APP,APP自行实现IM功能代价较大,且维护起来人力分散,于是,IM SDK项目便应运而生,APP通过接入SDK,可以快速实现IM基本功能。
2.整体设计
在最初的设计中,我们总结了如下的几个设计目标:
1)IM 主流程稳定可用:消息传输具有高可靠性。
2)UI 组件直接集成进入SDK,并支持可定制化。
3)富媒体发送集成进入SDK,并可按需定制需要的富媒体类型。
4)实现消息传输层SDK,与带有UI的SDK的功能分离,业务调用方既可以使用消息传输SDK,处理消息,然后自行处理UI,也可以使用带有UI组件的SDK,一步实现较为完备的IM功能。
依据以上几个诉求,我们为IM SDK设计了如下架构:
名称 |
描述 |
C++ Net |
负责SDK长链接相关的逻辑。包括登录、登出、互踢、推消息、心跳包等。 |
C++ Biz |
负责SDK各个模块的数据逻辑,如会话列表、消息列表、用户资料、好有关系、群等。还同时支持持久化存储等功能。更重要的是该层是跨平台的,不仅能够支持移动端平台接入,还能满足Windows、Mac等PC端平台接入使用。 |
Lib层 |
平台中间层,为上层提供平台形式的交互接口。 |
UIKit层 |
Kit层不仅负责UI渲染、事件订阅、用户事件处理等常规内容,同时还负责拉取会话列表、消息列表上下翻页、消息补空洞、离线消息跳转、同步用户信息等即时通讯的业务逻辑。 |
总体来说,这个架构采用标准的MVC模式,使得在项目初期,产品得以快速上线发布。并且借助底层核心逻辑的跨平台优势,得以在各个开发平台快速部署,实现功能完善,平台覆盖全面的战略目标。
3.设计要点
此章节中主要描述了,IM SDK设计中一些重要流程。
3.1 消息发送流程
IM SDK目前支持但不限于文字,富文本,音视频、图片、附件、位置、交互卡片等多种形态的消息类型。并提供统一的发送接口,其发送流程主要如下图所示:
3.2 收消息流程
收消息在客户端存在二种场景,一种是在线收消息,一种是离线收消息。以下分别阐述具体过程。
3.2.1 客户端在线收消息
收消息的时候,需要将数据派发到单独的dispatch线程上处理解析、I/O等环节。然后回调业务方的消息监听接口,业务方需要针对UI的具体场景,做不同的处理。除了刷新消息列表,IM SDK还会回调业务方会话列表变更的监听接口,同时刷新会话列表。
3.2.2 客户端离线收消息
客户端退到后台60s以上,或者app处于未启动状态时候,客户端以push的方式收消息。push消息的处理流程如下图所示:
SDK通过同步会话列表的方式,将每个会话的最后一条push消息拉取到本地,当打开某一个聊天页的时候,才会将中间的其他消息补全。
3.3 可定制化的UI
随着公司业务规模的扩大与业务线的快速迭代,新的业务也需要IM即时通讯功能,众所周知,哪怕是IM即时通讯的UI功能,也会占据大量的开发与调试时间, 为了解决这个痛点,我们提供完整的IM即时通讯解决方案,包括Lib层(IM接口层解决方案)、UIKit层(UI层解决方案)、Logic层(可定制化的UI抽象层解决方案)等。
我们改造了原有UIKit层方案,将其拆分为Logic+UIKit的架构方式的初衷是:
1)IM即时通讯的UI逻辑有其共通性,具备以SDK的方式提供IM一揽子解决方案的前提条件。
2)我们希望把IM SDK的持续迭代收敛在SDK内部,不对业务方造成干扰。实现和业务方自身UI逻辑解藕的目标。
基于以上的诉求,我们设计了如下的整体架构。以回话列表和消息列表为例,具体的架构设计如下图所示:
描述 |
|
ChatVirtualView |
聊天页的虚拟view,主要角色是处理聊天页列表的交互(包括但不限于上拉,下拉,消息底部判断,消息在屏幕的位置的定位等),该虚拟view会维护一个业务方的tableview实例对象,托管的tableview对象需要业务方在该类初始化的时候进行配置。配置之后tableview的代理实现和滚动回调会在这个虚拟view中进行处理,并根据情况回调给业务方。 |
ChatViewModel |
处理和聊天页UI相关的数据逻辑,比如上UI的数据源、消息收敛策略、拉取历史消息的任务之间的同步、删除消息、撤回消息、已读未读状态等内容 |
TalkVirtualView |
会话列表页的虚拟view,主要角色是处理会话列表页的交互,该虚拟view会维护一个业务方的tableview实例对象,托管的tableview对象需要业务方在该类初始化的时候进行配置。配置之后tableview的代理实现和滚动回调会在这个虚拟view中进行处理,并根据情况回调给业务方。 |
TalkViewModel |
处理和会话列表页UI相关的数据逻辑,比如上UI的数据源、会话的增量更新策略、业务方插入非IM SDK会话、删除指定会话,获取指定会话等内容 |
我们还是以会话列表和消息列表为例,具体阐述Logic+UIKit这种架构方案的优势:
3.3.1 会话列表的UI定制化
3.3.1.1 会话列表的类结构介绍
会话列表在IM SDK中的入口是WChatConversationListController类。其具体的类结构如下所示:
类图解释:
(1) WChatConversationListController中拥有一个WChatLogic层WIMTalkVirtualView类的实例对象vv,并实现接口类WIMTalkVirtualViewDelegate,如图中左部分所示。
(2) vv以WChatConversationListController中的conversationListTableView为入参,并赋值给其属性conversationListTableView,并将TableView的delegate和dataSource设置为vv,相关处理逻辑封装在vv中。其中所需数据源为:
[WIMLogicTalkViewModelsharedInstance].conversationListDataSource
(3) WChatConversationListController创建vv示例代码如下:
self.vv = [[WIMTalkVirtualView alloc]initWithTableView:self.conversationListTableView];self.vv.delegate = self;
3.3.1.2 WIMTalkVirtualViewDelegate回调实现
由于TableView的回调在vv上,所以接口类WIMTalkVirtualViewDelegate透传了绝大多数TableView的回调方法给WChatConversationListController(如果不满足需求可以通过继承的方式自己扩展)。比如如下几个重要的代理方法:
(1) 绑定会话类型与Cell:
-(UITableViewCell*)tableViewCellForConversationVM:(WIMTalkVirtualView *)vvtableView:(UITableView *)tableview model:(id)model atIndexPath:(NSIndexPath*)indexPath;
(2)TableView Cell 点击回调:
-(void)conversationVM:(WIMTalkVirtualView*)vv didSelectConversation:(id)conversation;
(3)计算会话列表单元行高度:
-(CGFloat)calculateModelHeight:(GmacsConversationModel*)model;
3.3.2 消息列表的UI定制化
3.3.2.1 消息列表的类结构介绍
消息列表页面在IM SDK中的入口是WChatConversationController类。其类结构如下图所示:
类图解释:
(1) WChatConversationController中拥有一个WChatLogic层WIMChatVirtualView类的实例对象vv,并实现接口类WIMChatVirtualViewTableViewDelegate(透传TableView回调的协议)和WIMChatVirtualViewCommonDelegate(其他回调,如上拉开始/结束,下拉开始/结束,消息跳转,发/收消息前,发/收消息后,列表滚动事件等),如图中左部分所示。
(2) vv以WChatConversationController中的messageTableView为入参,并赋值给vv的属性messageTableView,并将TableView的delegate和dataSource设置为vv,相关处理逻辑封装在vv中。其中所需数据源为:
[[WIMLogicChatViewModelsharedInstance].messageList
(3) 如果SDK的消息模型不满足业务方的UI渲染需求,我们支持业务方定义自己的消息模型,方式是继承WIMChatVirtualView类,并在子类中实现-(Class)getMessageModelClass方法,返回业务方自己的消息模型,SDK将为业务方实例化该类作为消息模型对象来使用。
(4) WChatConversationController创建vv示例代码如下:
self.vv = [[WIMChatVirtualView alloc]initWithTableView:self.messageTableView startMessageLocalId:self.startMessageLocalIdtargetId:targetId targetSource:targetSource conversationType:conversationType];
self.vv.tableViewDelegate = self;self.vv.commonDelegate = self;
3.3.2.2 实现回调接口
(1)实现WIMChatVirtualViewTableViewDelegate协议。以下实现是IM SDK的demo部分接口示范代码,业务方根据自己业务逻辑酌情处理。
– (UITableViewCell*)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath
messageModel:(id)model {
// do something
}
– (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath*)indexPath
messageModel:(id)model {
// do something
}
– (void)tableView:(UITableView*)tableView
didSelectRowAtIndexPath:(NSIndexPath*)indexPath
messageModel:(id)model {
// do something
}
(2)实现WIMChatVirtualViewCommonDelegate协议。以下实现是IM SDK的demo部分接口示范代码,业务方根据自己业务逻辑酌情处理。
/**
* 开始加载上一页动画
*/
– (void)startLoadBackwardsAnimation {
// do something
}
/**
* 开始加载下一页动画
*/
– (void)startLoadForwardsAnimation {
// do something
}
/**
* 加载上一页动画结束
*
* @param errorCode 错误码
* @param errorMsg 错误描述
*/
-(void)onLoadBackwardsFinished:(NSInteger)errorCode errorMsg:(NSString*)errorMsg {
// do something
}
/**
* 加载下一页动画结束
*
* @param errorCode 错误码
* @param errorMsg 错误描述
*/
-(void)onLoadForwardsFinished:(NSInteger)errorCode errorMsg:(NSString *)errorMsg{
// do something
}
4.总结
以上就是58集团IM SDK架构设计的实践。我们认为好的架构应该是随着业务的拓展而不断的自我进化,脱离于具体业务的架构设计并不是最优设计。58集团的业务特性是业务线多,每个业务线拥有独立账号体系,且不同账号体系之间需要互通。其次是商家分类多样,导致不同身份场景下的交互方式差异性大,与此同时IM SDK需要深度服务于各大业务线,满足业务线的各类即时通讯需求,所以IM SDK移动端对架构的易用性、扩展性、兼容性有十分苛刻的要求。同时我们还注意到,优秀的架构设计能极大程度的降低迭代成本,随着业务线版本的迭代,IM SDK移动端目前也在这个方向持续的优化和努力。