iOS 客户端 IM 以及列表 UI 框架
一、背景及简介
1.1 背景
已经好多年都没有写过什么东西了,近期换了新工作,换工作面试期间有面试官问,看你在知乎工作时间挺长,怎么也没写什么博客呢?仔细想想,工作期间做的一些项目,也有不少可以拿出来写写,不写的原因一个是工作忙,最主要的原因还是因为太懒了。最近换了新工作一段时间之后,由于原有设计对于新增聊天场景的支持性不佳、拓展困难等一系列原因,所以在一次新的聊天业务中准备设计一套新的设计方案,后期再逐步替代现有方案。趁着这次机会也逐渐改一改自己的懒毛病,把这次工作的内容整理整理,给大家分享一下,多有疏漏,敬请指正。
1.2 简介
本文主要介绍以下几个部分:
- 当前聊天业务基本工作原理;
- 使用场景以及新设计方案需要实现的目标;
- 新方案功能模块划分、模块架构设计和关键类接口的设计;
- 后续迭代替换现有业务的思路。
二、基本工作原理
下面会介绍当前项目中聊天部分的一些基本工作原理
3.1 消息发送过程
消息的发送是基于 HTTP 请求,主要包含两种类型:一种是简单文本类型、一种是多媒体类型的消息。
- 简单文本类型,是组装好参数后,直接通过 POST 请求发送到服务器;
- 多媒体类型消息,在调用发送消息的接口之前,需要先将多媒体数据上传到 CDN 服务器,然后把返回的地址链接拼到发送消息的请求参数中,再请求发送消息接口,实现消息的发送;
- 还有一些特殊的多媒体信息,比如第三方服务提供的大表情消息,在调用发送消息接口之前,就需要把第三方获数据另保存一份到自己的 CDN 服务器,然后把返回的数据拼接到发送消息参数中,请求发送消息接口,实现消息的发送。
3.2 消息接收过程
消息的接收是基于 WebSocket 长连接和 HTTP 请求相互配合来实现的。WebSocket 是 App 内用于服务端向客户端发送通知消息的单向通讯服务工具
- 有了新的消息,服务端会通过 WebSocket 向接收方主动发送通知消息;
- 做为主动通知的补充,客户端端上还有轮询任务,以固定时间间隔来通过 HTTP 请求服务端询问是否有新的消息;
- 客户端收到通知消息之后,再通过 HTTP 请求新消息数据,来展示到对应的 UI;
3.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 渲染页面,与其他模块没有直接关联。
- 对于易用性方面的设计:
方向 | 介绍 |
---|---|
方便导入 | 对外提供统一头文件 |
功能完备 | 包含通用业务功能的完整实现,初始化后,无需任何其他配置,聊天相关功能不需要业务额外实现,即可实现消息列表相关独立的基础功能 |
基础功能可配 | 固定配置可通过传入初始化参数,动态配置可通过代理实时问询业务是否支持 |
配置信息创建简便 | 通过 builder 生成,支持链式语法,增减字段不需要修改接口,所有配置信息只读,避免误改 |
代理事件信息完整 | 代理方法命名通用化,且需要包含时机以及必要数据 |
主题生成方便 | 传入符合要求配置文件的路径即可生成主题模型 |
- 对于基础功能拓展性方面的设计
功能点 | 介绍 |
---|---|
长按消息菜单 | 通过代理来禁止某默认功能、添加自定义功能项,也可以改变顺序 |
自定义列表数据 | 用户可以在消息列表中插入自定义 UI 卡片(例如个人介绍、配对提示等等) |
自定义消息卡片 UI 样式 | 通过代理方式,传入任意消息类型的自定义消息卡片的样式 |
默认消息卡片 UI 元素配置 | 对只改动消息卡片中部分元素提供支持,比如昵称后增加 Tag、模糊头像、改变字体颜色、消息气泡背景颜色等 |
埋点 | 可根据不同消息,传入埋点参数 |
主题 | 可根据需要传入不同颜色的主题,不传入主题的情况下,支持日夜间模式 |
- 易于维护性方面的设计
设计方式 | 介绍 |
---|---|
数据驱动 | 列表中消息的展示、删除、更新等均由数据驱动,列表只根据实际数据变更来渲染页面 |
单向数据流 | 驱动数据流要保持单向,列表内部不会反向修改任何数据源,保持数据逻辑清晰 |
数据源明确 | 数据源包括消息数据与自定义数据,分别由 ChatContext 和业务提供,列表正确展示数据源内容即可 |
内聚呈现逻辑 | 数据组装和驱动逻辑由 CoreMessageListPresenter 呈现器结合 IGListKit 实现,集中将数据呈现到列表 |
简化层级结构 | CoreMessageList 内部代理、列表、工具类等之间交互采用中介者模式,由网状结构改为星型结构,简化事件传递和交互层级 |
隐藏细节 | 通过提供 Adapter 适配器向业务提供标准操作函数,不暴露内部结构,减少滥用风险 |
- 代理回调方面的设计
要点 | 介绍 |
---|---|
代理事件的时机 | 包含 will、did 描述 |
代理事件的控制 | 包含 should,且有 return Bool 类型 |
命名 | 要描述事件而不能描述业务,比如长按头像就应该命名为 longPressAvatar,而不是 atUser |
类聚 | 可分类型的事件不能单独作为一个方法,比如点击了提示消息的「重新编辑」,这属于一种提示类型的事件,需要用提示事件做为枚举,通过一个统一的方法回调处理,防止出现每增加一个提示类型就要增加一个方法 |
内部操作业务可控制 | 消息操作部分:重新发送、撤回、删除,等事件需要通过代理方法由业务控制,事件完成需要回调给业务 |
4.2.2 聊天消息管理工具(ChatContext)
该负责维护管理消息发送、接收、撤回、删除等等操作,以及消息数据维护,数据变更的事件回调,CoreMessageList 部分会根据该数据变更来驱动列表的 UI 变更。给 ChatKeyboard 模块提供发送接口,给 CoreMessageList 内部的功能菜单提供撤回、删除、重试等功能提供对应的功能接口。
该模块主要分为四个子模块,消息数据检查模块、消息发送模块、消息接收模块、数据管理模块。其中消息发送和接收部分,根据文中前半部分介绍的基本工作原理进行设计开发。数据管理模块,可以根据消息数据变更的特性对算法进行针对性优化,该数据模块与消息 UI 列表中的 Item 数据模型算法保持一致:
- ChatContext 具有以下这几个特点
特点 | 描述 |
---|---|
功能完备 | 包含聊天功能的环境变量、以及数据逻辑部分的完整实现 |
与 UI 无关 | 只包含数据以及逻辑,与 UI 完全无关,UI 部分可根据数据变更回调来更新页面 |
一对多 | 存在形式是一对多,支持同一聊天对应的 UI 页面存在多个,通过弱引用等方式自管理生命周期 |
接口通用 | 比如,不同的聊天场景请求发送接口会携带业务参数,接口设计需要在增删业务参数时,接口不需要变更 |
简单易用 | 接口参数定义明确,配置信息生成简便可拓展,与 CoreMessageList 配置信息参数设计思路一致 |
- 数据管理模块部分分析以及算法优化
针对消息数据的顺序确定性,以及增量新消息数量一般数量比较少等特性。
需要提供的功能主要有添加新的消息、加载历史消息、删除和更新。
在添加新消息前,数据要提前排序,拼接到数据列表中时采用如下图的方式:
4.2.3 键盘输入模块(ChatKeyboard)
负责输入框、键盘以及内部表情菜单等 UI 元素,维护通用功能的管理和配置功能,提供用户事件以及键盘 UI 变动的回调,可支持主题配置。提供 UI 更新事件回调给 CoreMessageList 用以修改消息列表底部的空间、滚动到底部等。点击发送按钮、选择完图片、视频、表情后会调用 ChatContext 对应的发送消息接口。简述如下表:
组成部分 | 输入框、系统键盘、 emoji 表情、大表情、以及视频、语音等工具栏功能 |
依赖模块 | 图片视频选择器、emoji 以及表情包管理工具、音视频录制工具等 |
职责原则 | 数据采集与传递,不做加工与处理 |
提供配置功能 | 主题、支持的表情类型、支持输入的类型 |
4.3 模块详细介绍
对于详细介绍的部分,由于篇幅的原因,这里会详细介绍 CoreMessageList 模块部分,会通过 UML 类图、接口设计以及设计思路来进行详细介绍
4.3.1 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>*)coreMessageListshouldTryShowMenu:(XXXMessageActionType)menuTypemessage:(XXXMessage *)message;/// 将要弹窗消息菜单
/// 可根据业务需要添加自己的 Action,改变顺序等
/// @param actions 默认的 Action,可通过 type 区分类型
/// return 符合自己业务需求的 Action 数组
- (NSArray<XXXMessageAction *>*)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageListwillShowActions:(NSArray<XXXMessageAction *>*)actions forMessage:(XXXMessage *)message;/// 已经展示消息菜单
- (void)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList didShowedCellMenuWithActions:(NSArray<XXXMessageAction *>*)actions;@end
4.3.4 XXXCoreMessagesListDelegate 接口设计
XXXCoreMessagesListDelegate 列表相关的总代理,主要包含三个子协议:
- XXXCoreMessagesListCustomDataDelegate 接口设计,这个代理主要是负责业务传入自定义数据
/// 列表自定义数据数代理
@protocol XXXCoreMessagesListCustomDataDelegate <NSObject>@optional/// 消息列表顶部添加自定义的 CellModel
- (NSArray<XXXCoreMessageListBaseCellModel *> *)customPrependCellModelsForCoreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList;/// 根据自己在列表顶部添加的自定义 CellModel,返回对应的卡片
/// @param cellModel 通过代理传入的自定义 CellModel
- (IGListSectionController *)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageListsectionControllerForCustomPrependCellModel:(XXXCoreMessageListBaseCellModel *)cellModel;/// 列表对于不支持的消息类型,允许业务方自定义文案,待定
- (NSString *)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageListcustomTipsForUndefineMessage:(XXXMessage *)message;/// 对于不同消息增加的自定义配置信息
- (XXXCoreMessageListCellConfig *)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageListcellConfigForMessage:(XXXMessage *)message;@end
- XXXCoreMessagesListCellDelegate 负责处理所有 cell 相关元素的事件,比如点击、长按头像或者内容;
聚合一个提示类事件代理,封装提示模型,回调事件类型。比如点击「重新编辑」、「实名认证」等;点击富文本链接等事件回调;
/// Cell 相关事件代理
@protocol PUGCoreMessagesListCellDelegate <NSObject>@optional/// 点击头像,return NO 或者不实现默认跳转 profile 页
- (BOOL)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageListtackleTapAvatar:(PUGUser *)avatarUsermessage:(PUGMessage *)message;/// 长按头像
- (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageListdidLongPressAvatar:(XXXUser *)avatarUsermessage:(XXXMessage *)message;/// 点击内容
- (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList didTapContentWithMessage:(XXXMessage *)message;/// 点击富文本链接, 如果可以处理,return YES,否则 return NO
- (BOOL)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageListhandleTapLink:(NSString *)linkmesssage:(XXXMessage *)message;/// 点击了提示消息事件
- (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageListdidTapTipAction:(XXXCoreMessageListCellModelTipAction)actionmessage:(XXXMessage *)message;
@end
- XXXCoreMessagesListScrolledDeletgate,主要负责列表滚动相关的代理事件,如果有需要滚动加速、减速之类的代理可以在此处添加
// 列表滚动事件代理
@protocol PUGCoreMessagesListScrolledDeletgate <NSObject>@optional/// 列表滚动
- (void)coreMessageList:(UIViewController<PUGCoreMessageListController> *)coreMessageList scrollViewWillBeginDragging:(nonnull UIScrollView *)scrollView;- (void)coreMessageList:(UIViewController<PUGCoreMessageListController> *)coreMessageList scrollViewDidScroll:(nonnull UIScrollView *)scrollView;@end
- XXXCoreMessagesListDelegate 负责消息相关事件处理以及回调列表整体的代理事件,包括上述三个子代理
/// 消息列表相关功能代理事件
@protocol XXXCoreMessagesListDelegate <XXXCoreMessagesListScrolledDeletgate,XXXCoreMessagesListCustomDataDelegate,XXXCoreMessagesListCellDelegate>@optional/// 是否重试发送失败的消息
- (BOOL)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList shouldRetrySendMessage:(XXXMessage *)message;/// 成功操作消息
- (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageListsuccessOperate:(XXXCoreMessagesListMessageOperate)operatemessage:(XXXMessage *)messageresponseObject:(id)responseObject;/// 操作消息失败
- (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageListfailToOperate:(XXXCoreMessagesListMessageOperate)operatemessage:(XXXMessage *)messageerror:(XXXError *)error;/// 收到新的消息
- (void)coreMessageListDidReceivedNewMessages:(UIViewController <XXXCoreMessageListController>*)coreMessageList isFirstPage:(BOOL)isFirstPage;/// 点击了消息列表页面
- (void)didTapCoreMessageListEmptyArea:(UIViewController<XXXCoreMessageListController> *)coreMessageList;@end
五、总结
对于这个全新的设计方案,其基础功能目前还不能完全满足现有的业务,基础功能也需要继续完善,为了保证新设计方案不阻塞业务,会优先在新的聊天场景中使用新的聊天框架,在接入新聊天场景的过程中来逐步完善基础功能。后期如果对单聊或者老群聊有较大的改版时,也可以考虑直接接入新的框架去重新实现。
以上所述,是问目前从事的项目聊天模块架构的一个新的设计方案,希望能够给同行提供一定的思路,也希望能够得到大佬们的指导建议。最后祝愿同行的朋友能够身体健康、工作顺心。
iOS 客户端 IM 以及列表 UI 框架相关推荐
- ios客户端学习笔记(六):iOS客户端的工作流程
iOS客户端的工作流程可以简单地概括为: 用户操作 -> 视图显示 -> 交互处理 -> 数据获取 -> 数据处理 -> 数据展示. 结合代码对每个工作流程进行更为具体. ...
- ios客户端学习笔记(七):iOS客户端的UI设计
iOS客户端的UI设计是指在iOS操作系统上开发应用程序时所涉及的用户界面设计,包括应用程序的布局.颜色.字体.图标等元素的设计.良好的UI设计应该能够提高用户体验,使用户能够轻松地使用应用程序. 在 ...
- 移动周刊第 177 期:Android 新特性介绍、iOS 客户端框架演进
写在前面 本期移动周刊第 177 期如约而至,聚焦 Android.iOS.VR/AR/MR.直播等前沿移动开发技术,收录一周最热点,解读开发技巧,我们希望从中能够让你有一些收获,如果你有好的文章以及 ...
- bootstrap 新闻列表_kuapingUI 2.2 版本发布,跨屏 UI-bootstrap 大组件 UI 框架
kuapingUI 2.2 版本发布,增加了一个比较实用的工具栏web组件,是由分享按钮组 + 联系按钮组构成,分享按钮组包含了 QQ空间.朋友圈.QQ.微信.微博等分享:联系按钮组包含了 微信.QQ ...
- Socket在iOS客户端上的简单实现 - 利用GCAsyncSocket框架
Socket在iOS客户端上的简单实现 - 利用GCAsyncSocket框架 GCAsyncSocket 这是一个2003的开发出来的一个开源框架 首先把GCDAsyncSocket的.h和.m文件 ...
- 58同城iOS客户端Hybrid框架探索
作者:杜艳新,刘文军.58同城iOS高级研发工程师,专注于App Hybrid框架的架构研发,主导了58同城App的Hybird混合研发的系统架构以及研发. 责编:唐小引,欢迎技术投稿.约稿.给文章纠 ...
- 58 同城 iOS 客户端 Hybrid 框架探索
[CSDN 编者按]58 同城 iOS 客户端的 Hybrid 框架在最初设计和演进的过程中,遇到了许多问题.为此,整个 Hybrid 框架产生了很大的变化.本文作者将遇到的典型问题进行了总结,并重点 ...
- iOS之UI--主流框架的搭建-- 仿制QQ的UI框架
iOS之UI--主流框架的搭建-- 仿制QQ的UI框架 使用XCode搭建多个控制器界面,一般在实际开发中建议超过四个控制器界面使用纯代码. 下面的实例其实已经超过了四个,总结详细步骤的目的,主要是更 ...
- .NET Core/.NET5/.NET6 开源项目汇总9:客户端跨平台UI框架
.NET Core 实现了跨平台,支持在 Windwos.Linux.macOS上开发与部署,但是也仅限于Web应用程序.对于Windows桌面端应用程序,WinForm 与 WPF 技术是当前最成熟 ...
最新文章
- redis主从复制、高可用和集群
- Doctype? 严格模式与混杂模式-如何触发这两种模式,区分它们有何意义?
- CTF-RSA-tool 安装全过程
- TF之CNN:利用sklearn(自带手写数字图片识别数据集)使用dropout解决学习中overfitting的问题+Tensorboard显示变化曲线
- mysql 白皮书_mysql企业版 《 MySQL企业版中文白皮书 》.cn.doc
- Python文件与目录操作
- linux运行python乱码_linux下python中文乱码解决方案详解
- 前端学习(2036)vue之电商管理系统电商系统之将本地的文件合并
- Python基础(九)--异常
- 各种数据库的连接字符串
- 六、面向对象编程——类和对象
- windows本地script脚本恶意代码分析(带注释)
- 福利福利!20行代码教大家抓取斗鱼美女主播封面
- 电脑用什么软件可以测试网速吗,在电脑上怎么测试网速_两种快速测试网速的方法介绍 - 驱动管家...
- Python常用小技巧(三)——批量修改图片尺寸
- 观察者模式Observer -- 深入理解
- codeforces1492 D. Genius‘s Gambit python
- 计算机游戏动漫制作自我鉴定,动画制作专业毕业生的自我鉴定范文(精选5篇)...
- 多云定义:什么是多云,多云究竟是什么?
- args = parser.parse_args()报错
热门文章
- Dice Loss,balanced cross entropy,Focal Loss
- table表格做一个表头斜线样式
- 幼麟棋牌游戏进程分析
- 微量小程序联盟,如何实现微信小程序换量和微信小程序推广?
- lua 调用c++ dll, 提示:%1 不是有效的 Win32 应用程序。
- PS自用(抠图、调色)
- Spine的默认材质Spine/Skeleton有毛边问题
- 删除自定义reg注册表
- 【OpenCV】- 分水岭算法
- python unrar问题_Python-使用unrar库时Couldn't find path to unrar library的解决办法