iOS 客户端 IM 以及列表 UI 框架

一、背景及简介

1.1 背景

由于工作中 IM 原有设计对于新增聊天场景的支持性不佳、拓展困难等一系列原因,团队计划发起一次 IM 重构,这次重构由我来主导,计划在新的聊天业务中采用新的设计方案来开发,后期再逐步替代现有方案。这次把新方案的工作内容整理了整理,分享给大家,多有疏漏,敬请指正。

1.2 简介

本文主要介绍以下几个部分:

  1. 当前聊天业务基本工作原理;
  2. 使用场景以及新设计方案需要实现的目标;
  3. 新方案功能模块划分、模块架构设计和关键类接口的设计;
  4. 后续迭代替换现有业务的思路。

二、基本工作原理

下面会介绍当前项目中聊天部分的一些基本工作原理

2.1 消息发送过程

消息的发送是基于 HTTP 请求,主要包含两种类型:一种是简单文本类型、一种是多媒体类型的消息。

  1. 简单文本类型,是组装好参数后,直接通过 POST 请求发送到服务器;
  2. 多媒体类型消息,在调用发送消息的接口之前,需要先将多媒体数据上传到 CDN 服务器,然后把返回的地址链接拼到发送消息的请求参数中,再请求发送消息接口,实现消息的发送;
  3. 还有一些特殊的多媒体信息,比如第三方服务提供的大表情消息,在调用发送消息接口之前,就需要把第三方获数据另保存一份到自己的 CDN 服务器,然后把返回的数据拼接到发送消息参数中,请求发送消息接口,实现消息的发送。
    消息发送流程图

2.2 消息接收过程

消息的接收是基于 WebSocket 长连接和 HTTP 请求相互配合来实现的。WebSocket 是 App 内用于服务端向客户端发送通知消息的单向通讯服务工具

  1. 有了新的消息,服务端会通过 WebSocket 向接收方主动发送通知消息;
  2. 做为主动通知的补充,客户端端上还有轮询任务,以固定时间间隔来通过 HTTP 请求服务端询问是否有新的消息;
  3. 客户端收到通知消息之后,再通过 HTTP 请求新消息数据,来展示到对应的 UI;
    消息接收流程图

2.3 消息的存储

消息的存储底层采用 sqlite 和第三方 FMDB 开源框架,再此基础之上开发了一套支持 ORM 以及 SQL 语句生成工具的基础库。每个支持数据库保存的 Model 类内部保存了具体表名、不同字段与表字段的映射,进行数据库操作的时候可以根据这些信息生成对应的 SQL 语句来保存到本地数据库。本地消息的保存就是直接通过框架保存到数据库即可,网络部分是请求到数据后,通过前后端约定的 envelop key 确定到具体数据 Model 的类,从而根据 ORM 信息来保存到数据库,大概就介绍到这里吧,不再做过多的介绍了。

三、应用场景以及设计目标

重构目标主要是为了减少因产品需求增加而带来维护成本的急剧上升。软件的设计不仅需要遵守软件SOLID 原则,还要结合实际的应用场景,下面会介绍项目中聊天部分的应用场景,以及对应的设计目标。

3.1 应用场景

当前 App 随着产品迭代,聊天的场景逐渐增多。最早只有单聊的场景,到后来新增了群聊,后来较短的时间内又多出了几种类型的临时聊天场景,后来两个月内又新增一种新的群聊场景,并且需要支持主题配置。

这些不同的聊天场景之间,消息发送、接收等逻辑一致,列表中相同类型的消息样式相同。但是,不同业务场景所支持的消息类型、主题样式、业务功能均有不同。

比如,某些聊天场景中根据不同的业务逻辑,发送消息达到一定数量后,就需要本地插入某种特殊类型的提示消息;而其他的业务可能会有在列表的头部展示某种特定样式的卡片;还有一些聊天详情在某些时间没有人发言则需要关闭聊天,并变更 UI。

3.2 设计目标

当前已有的设计起源与单聊,所支持的可扩展方向是支持新增各种消息卡片以及样式,但是对于新增聊天场景难以支持,比如后续的群聊是在单聊的基础上拷贝了大量代码,其通用的部分基本上是复制了一份份代码。随着新的聊天场景越来越多,会存在大量的冗余代码,维护成本几乎指数级别的上升。

所以,这次我们就需要把多种聊天的基础部分抽取出来,来供各种不同的聊条场景来使用。其中聊天列表和聊天通用逻辑,可以方便的迁移到不同的聊天业务场景,使消息列表部分以及发送逻辑与具体业务逻辑进行解耦,实现接入方只关注自身的业务逻辑,不需要关注聊天系统内部通用逻辑。

四、系统设计

4.1 整体架构设计图

架构设计

4.2 模块划分

系统中子模块主要包括四个部分,分别是基础消息列表子模块(CoreMessageList)、聊天消息管理工具(ChatContext)、键盘输入模块(ChatKeyboard),下面做分别介绍在聊天模块中扮演的职责,及其与其他模块的关系,下面做详细介绍:

4.2.1 基础消息列表子模块(CoreMessageList)

该模块主要提供消息 UI 的展示、支持配置自定义 UI 样式以及事件回调。该模块依赖于聊天消息管理工具(ChatContext),ChatContext 会通过回调通知 CoreMessageList 渲染页面,与其他模块没有直接关联。

  1. 对于易用性方面的设计:
方向介绍
方便导入对外提供统一头文件
功能完备包含通用业务功能的完整实现,初始化后,无需任何其他配置,聊天相关功能不需要业务额外实现,即可实现消息列表相关独立的基础功能
基础功能可配固定配置可通过传入初始化参数,动态配置可通过代理实时问询业务是否支持
配置信息创建简便通过 builder 生成,支持链式语法,增减字段不需要修改接口,所有配置信息只读,避免误改
代理事件信息完整代理方法命名通用化,且需要包含时机以及必要数据
主题生成方便传入符合要求配置文件的路径即可生成主题模型
  1. 对于基础功能拓展性方面的设计
功能点介绍
长按消息菜单通过代理来禁止某默认功能、添加自定义功能项,也可以改变顺序
自定义列表数据用户可以在消息列表中插入自定义 UI 卡片(例如个人介绍、配对提示等等)
自定义消息卡片 UI 样式通过代理方式,传入任意消息类型的自定义消息卡片的样式
默认消息卡片 UI 元素配置对只改动消息卡片中部分元素提供支持,比如昵称后增加 Tag、模糊头像、改变字体颜色、消息气泡背景颜色等
埋点可根据不同消息,传入埋点参数
主题可根据需要传入不同颜色的主题,不传入主题的情况下,支持日夜间模式
  1. 易于维护性方面的设计
设计方式介绍
数据驱动列表中消息的展示、删除、更新等均由数据驱动,列表只根据实际数据变更来渲染页面
单向数据流驱动数据流要保持单向,列表内部不会反向修改任何数据源,保持数据逻辑清晰
数据源明确数据源包括消息数据与自定义数据,分别由 ChatContext 和业务提供,列表正确展示数据源内容即可
内聚呈现逻辑数据组装和驱动逻辑由 CoreMessageListPresenter 呈现器结合 IGListKit 实现,集中将数据呈现到列表
简化层级结构CoreMessageList 内部代理、列表、工具类等之间交互采用中介者模式,由网状结构改为星型结构,简化事件传递和交互层级
隐藏细节通过提供 Adapter 适配器向业务提供标准操作函数,不暴露内部结构,减少滥用风险
  1. 代理回调方面的设计
要点介绍
代理事件的时机包含 will、did 描述
代理事件的控制包含 should,且有 return Bool 类型
命名要描述事件而不能描述业务,比如长按头像就应该命名为 longPressAvatar,而不是 atUser
类聚可分类型的事件不能单独作为一个方法,比如点击了提示消息的「重新编辑」,这属于一种提示类型的事件,需要用提示事件做为枚举,通过一个统一的方法回调处理,防止出现每增加一个提示类型就要增加一个方法
内部操作业务可控制消息操作部分:重新发送、撤回、删除,等事件需要通过代理方法由业务控制,事件完成需要回调给业务
4.2.2 聊天消息管理工具(ChatContext)

该负责维护管理消息发送、接收、撤回、删除等等操作,以及消息数据维护,数据变更的事件回调,CoreMessageList 部分会根据该数据变更来驱动列表的 UI 变更。给 ChatKeyboard 模块提供发送接口,给 CoreMessageList 内部的功能菜单提供撤回、删除、重试等功能提供对应的功能接口。

该模块主要分为四个子模块,消息数据检查模块、消息发送模块、消息接收模块、数据管理模块。其中消息发送和接收部分,根据文中前半部分介绍的基本工作原理进行设计开发。数据管理模块,可以根据消息数据变更的特性对算法进行针对性优化,该数据模块与消息 UI 列表中的 Item 数据模型算法保持一致:

  1. ChatContext 具有以下这几个特点
特点描述
功能完备包含聊天功能的环境变量、以及数据逻辑部分的完整实现
与 UI 无关只包含数据以及逻辑,与 UI 完全无关,UI 部分可根据数据变更回调来更新页面
一对多存在形式是一对多,支持同一聊天对应的 UI 页面存在多个,通过弱引用等方式自管理生命周期
接口通用比如,不同的聊天场景请求发送接口会携带业务参数,接口设计需要在增删业务参数时,接口不需要变更
简单易用接口参数定义明确,配置信息生成简便可拓展,与 CoreMessageList 配置信息参数设计思路一致
  1. 数据管理模块部分分析以及算法优化

针对消息数据的顺序确定性,以及增量新消息数量一般数量比较少等特性。
需要提供的功能主要有添加新的消息、加载历史消息、删除和更新。
在添加新消息前,数据要提前排序,拼接到数据列表中时采用如下图的方式:
新增消息

4.2.3 键盘输入模块(ChatKeyboard)

负责输入框、键盘以及内部表情菜单等 UI 元素,维护通用功能的管理和配置功能,提供用户事件以及键盘 UI 变动的回调,可支持主题配置。提供 UI 更新事件回调给 CoreMessageList 用以修改消息列表底部的空间、滚动到底部等。点击发送按钮、选择完图片、视频、表情后会调用 ChatContext 对应的发送消息接口。简述如下表:

组成部分输入框、系统键盘、 emoji 表情、大表情、以及视频、语音等工具栏功能
依赖模块图片视频选择器、emoji 以及表情包管理工具、音视频录制工具等
职责原则数据采集与传递,不做加工与处理
提供配置功能主题、支持的表情类型、支持输入的类型

4.3 模块详细介绍

对于详细介绍的部分,由于篇幅的原因,这里会详细介绍 CoreMessageList 模块部分,会通过 UML 类图、接口设计以及设计思路来进行详细介绍

4.3.1 CoreMessageList 架构设计 UML 类图

CoreMessageList UML 类图

4.3.2 CoreMessageListViewController 消息列表视图控制器接口设计

设计思路:该部分暴露了消息列表视图控制器的对外功能,保持保持接口功能简捷易用

/// 聊天消息列表视图控制器协议
@protocol XXXCoreMessageListController <NSObject>
 
/// 列表事件代理
@property (nonatomic, weak) id<XXXCoreMessagesListDelegate> listDelegate;
/// 长按菜单代理
@property (nonatomic, weak) id<XXXCoreMessageCellMenuDelegate> cellMenuDelegate;
/// 埋点信息代理
@property (nonatomic, weak) id<XXXCoreMessageListTrackDelegate> trackDelegate;
 
/// 列表相关功能适配器
@property (nonatomic, readonly) id<XXXCoreMessageListAdaptor> listAdaptor;
 
/// 把列表视图控制的 View 添加到 container,并设置为 container 的子视图控制器
- (void)addViewToContainer:(UIViewController *)container;
 
@end
4.3.3 XXXCoreMessageCellMenuDelegate 接口设计

该代理接口主要是处理长按消息的菜单处理
设计思路:根据展示时机包含 shouldShow、willShow、didShow 三个方法,willShow 时可以设置或者添加自定义数据

/// 长按消息弹窗功能
@protocol XXXCoreMessageCellMenuDelegate <NSObject>
 
@optional
/// 长按对应消息气泡,是否尝试展示对应的菜单类型
/// 如果想要禁止某种类型,则 return NO
/// @param menuType 菜单类型
/// @param message 长按气泡对应的消息
- (BOOL)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList
      shouldTryShowMenu:(XXXMessageActionType)menuType
                message:(XXXMessage *)message;
 
/// 将要弹窗消息菜单
/// 可根据业务需要添加自己的 Action,改变顺序等
/// @param actions 默认的 Action,可通过 type 区分类型
/// return 符合自己业务需求的 Action 数组
- (NSArray<XXXMessageAction *>*)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList
                         willShowActions:(NSArray<XXXMessageAction *>*)actions forMessage:(XXXMessage *)message;
 
/// 已经展示消息菜单
- (void)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList didShowedCellMenuWithActions:(NSArray<XXXMessageAction *>*)actions;
 
@end
4.3.4 XXXCoreMessagesListDelegate 接口设计

XXXCoreMessagesListDelegate 列表相关的总代理,主要包含三个子协议:

  1. XXXCoreMessagesListCustomDataDelegate 接口设计,这个代理主要是负责业务传入自定义数据
/// 列表自定义数据数代理
@protocol XXXCoreMessagesListCustomDataDelegate <NSObject>
 
@optional
 
/// 消息列表顶部添加自定义的 CellModel
- (NSArray<XXXCoreMessageListBaseCellModel *> *)customPrependCellModelsForCoreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList;
 
/// 根据自己在列表顶部添加的自定义 CellModel,返回对应的卡片
/// @param cellModel 通过代理传入的自定义 CellModel
- (IGListSectionController *)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList
       sectionControllerForCustomPrependCellModel:(XXXCoreMessageListBaseCellModel *)cellModel;
 
/// 列表对于不支持的消息类型,允许业务方自定义文案,待定
- (NSString *)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList
             customTipsForUndefineMessage:(XXXMessage *)message;
 
/// 对于不同消息增加的自定义配置信息
- (XXXCoreMessageListCellConfig *)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList
                             cellConfigForMessage:(XXXMessage *)message;
 
@end
  1. XXXCoreMessagesListCellDelegate 负责处理所有 cell 相关元素的事件,比如点击、长按头像或者内容;
    聚合一个提示类事件代理,封装提示模型,回调事件类型。比如点击「重新编辑」、「实名认证」等;点击富文本链接等事件回调;
/// Cell 相关事件代理
@protocol PUGCoreMessagesListCellDelegate <NSObject>
 
@optional
 
/// 点击头像,return NO 或者不实现默认跳转 profile 页
- (BOOL)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList
        tackleTapAvatar:(PUGUser *)avatarUser
                message:(PUGMessage *)message;
 
/// 长按头像
- (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList
     didLongPressAvatar:(XXXUser *)avatarUser
                message:(XXXMessage *)message;
 
/// 点击内容
- (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList didTapContentWithMessage:(XXXMessage *)message;
 
/// 点击富文本链接, 如果可以处理,return YES,否则 return NO
- (BOOL)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList
          handleTapLink:(NSString *)link
               messsage:(XXXMessage *)message;
 
/// 点击了提示消息事件
- (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList
        didTapTipAction:(XXXCoreMessageListCellModelTipAction)action
                message:(XXXMessage *)message;
@end
  1. XXXCoreMessagesListScrolledDeletgate,主要负责列表滚动相关的代理事件,如果有需要滚动加速、减速之类的代理可以在此处添加
// 列表滚动事件代理
@protocol PUGCoreMessagesListScrolledDeletgate <NSObject>
 
@optional
 
/// 列表滚动
- (void)coreMessageList:(UIViewController<PUGCoreMessageListController> *)coreMessageList scrollViewWillBeginDragging:(nonnull UIScrollView *)scrollView;
 
- (void)coreMessageList:(UIViewController<PUGCoreMessageListController> *)coreMessageList scrollViewDidScroll:(nonnull UIScrollView *)scrollView;
 
@end

  1. XXXCoreMessagesListDelegate 负责消息相关事件处理以及回调列表整体的代理事件,包括上述三个子代理
/// 消息列表相关功能代理事件
@protocol XXXCoreMessagesListDelegate <XXXCoreMessagesListScrolledDeletgate,
                                       XXXCoreMessagesListCustomDataDelegate,
                                       XXXCoreMessagesListCellDelegate>
 
@optional
 
/// 是否重试发送失败的消息
- (BOOL)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList shouldRetrySendMessage:(XXXMessage *)message;
 
/// 成功操作消息
- (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList
         successOperate:(XXXCoreMessagesListMessageOperate)operate
                message:(XXXMessage *)message
         responseObject:(id)responseObject;
 
/// 操作消息失败
- (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList
          failToOperate:(XXXCoreMessagesListMessageOperate)operate
                message:(XXXMessage *)message
                  error:(XXXError *)error;
 
/// 收到新的消息
- (void)coreMessageListDidReceivedNewMessages:(UIViewController <XXXCoreMessageListController>*)coreMessageList isFirstPage:(BOOL)isFirstPage;
 
/// 点击了消息列表页面
- (void)didTapCoreMessageListEmptyArea:(UIViewController<XXXCoreMessageListController> *)coreMessageList;
 
@end

五、总结

对于这个全新的设计方案,其基础功能目前还不能完全满足现有的业务,基础功能也需要继续完善,为了保证新设计方案不阻塞业务,会优先在新的聊天场景中使用新的聊天框架,在接入新聊天场景的过程中来逐步完善基础功能。后期如果对单聊或者老群聊有较大的改版时,也可以考虑直接接入新的框架去重新实现。
以上所述,是问目前从事的项目聊天模块架构的一个新的设计方案,希望能够给同行提供一定的思路,也希望能够得到大佬们的指导建议。最后祝愿同行的朋友能够身体健康、工作顺心。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值