项目背景
IM聊天功能作为整个电商功能的补充和重要支撑,相信很多的电商App都会集成这么一个功能,但是大多数公司的IM功能相信都是集成的融云或者环信的SDK。
但是相信作为电商的有力支撑,IM的消息对于各个公司来说都有不同的业务需求,也就是说普通的图片、文字、红包甚至语音这种常用的消息类型并不能有力支撑起一个IM的业务。我们公司的IM业务功能正式在这种背景下,完全从前端到后端都是完全由自己人设计、开发以及维护的。
我是在17年年中左右正式接手了我司的IM业务功能,刚开始是起因于解决线上的一个bug,于是开始梳理了一下代码逻辑,于是。。。懵逼了好久好久好久。
虽然IM的代码已经在线上跑了很久了,在我开始解决bug之前貌似有大概有大半年将近一年的的时间没有人来维护,但是从架构设计上、业务代码实现等点来看,质量十分堪忧。
梳理代码
我司IM的架构设计大概是在几年以前,长连接协议使用的是WebSocket,业务逻辑有一些复杂,目测从数据逻辑到业务逻辑再到UI逻辑,代码量可能会接近5w这个量级,所以说一开始就一行一行的看代码逻辑显然是不太理智的。
梳理第一步
第一步的主要目的是熟悉代码的主要脉络,于是我开始有序的梳理沿着数据流向
梳理主干,要点如下:
- 分析各个数据模型(model)。
- 整理各个HTTP请求的API。
- 整理并备注各个Notification的Key,并标记使用场景。
- 整理各个Delegate回调函数的使用场景。
- 整理并备注各个枚举值的含义以及使用场景。
因为代码历史久远且开发维护人员换了好几茬之后,代码逻辑比较混乱,刚开始梳理的时候大部分时间都花在了备注各种代码上。
梳理第二步
第一步之后,我其实已经对于IM的架构逻辑开始有了一个初步的比较宽泛的了解。第二步的的主要目的是整理在使用的主要的几个组件:
- 数据库的初始化创建以及使用逻辑。
- HTTP代码的初始化创建以及使用逻辑。
- 长连接代码的初始化创建以及使用逻辑,特别是与服务端沟通和保活的部分。
- 针对以上基础控件的二次封装控件的整理。
梳理了上面几个之后,我陆续整理了如下:
- 数据库的表结构设计、初始化创建以及销毁等逻辑。
- 了解HTTP的接口,进一步了解了基础的业务设计。
- 通过和服务端的同事沟通,明确了当前的长连接协议下,两端是如何保活、沟通数据以及沟通状态的。
- 各个API的使用场景以及使用逻辑。
梳理第三步
上面两步,基本上把最核心的工具整理完成,下面就开始将代码逻辑串起来,整理业务逻辑:
- 群聊的收发消息逻辑。
- 私聊的收发消息逻辑。
- 登录、退出登录、更换账号登录以及绑定账号之后的业务逻辑。
- 自后台唤醒之后的业务逻辑。
- 收到推送消息之后的业务逻辑。
整理好了以上之后,我利用流程图工具ProcessOn创建了大概有10张左右的流程表,数据流向和业务逻辑一清二楚。
特别是聊天的数据逻辑,从HTTP请求、长连接推送以及数据库操作,无所不包。但是图整理完之后,对于主要流程几乎是掌握的比较清楚了,即使回头有忘记的,回头查看表之后就会一清二楚,不仅便于我熟悉逻辑,对以后的维护也很有益处。
维护
遇到的困境
虽然代码质量堪忧,但是代码中的bug还算比较少,所以在解决线上bug这个问题上,没有遇到过多的麻烦。但是其中也有几个bug十分麻烦,找了好久才找到问题的原因。
其中有一个,找原因大概找了一周多的时间,最后定位问题在我们的账号系统上。因为历史原因,我们的IM业务和其他业务是两套账号体系,中间经历过一次账号变更,但是由于某些奇葩的原因(有部分业务经历了hack),导致部分用户的账号没有成功的过渡账号变更,导致了一些问题。
表象原因是代码逻辑混乱,bug原因复杂,无法复现并难以定位问题。
但更深的原因是时间久远,我们对当时的代码设计以及业务逻辑变更毫无记录,导致无法快速定位到问题的原因。
思考
对于一个逻辑复杂的业务,首先要考虑的是易拓展、易维护和模块组件间的高度解耦。
对于一个成熟的业务来说,我认为易于维护性更为重要,因为业务已经进入成熟阶段的话就意味着功能增加的几率比较低,那么对于线上bug修复和小修小补的优化就是主要工作。
我司的IM就是这样的,大家看梳理的代码的第一步应该能看出来,我耗费了大量的时间去做各种备注,然而,这写备注应该在开发阶段就应该写好了。包括对于之前两套账号系统存在的原因,包括变更一次账号的原因,是否需要有一个详细的记录?
那么对于这种这种有助于后期维护的记录,我认为我们需要对重要的业务变更以及业务设计要有一个详细的记录,无论是自己作为记录还是为后面接手的同学做个参考,我认为都是极为重要的。
重构
思索需求
经过最开始的了解和一段时间维护,我发现遇到的最大麻烦是数据逻辑、业务逻辑、和UI操作逻辑混到了一起,简直可以说是牵一发而动全身。特别是数据逻辑十分混乱,因为数据逻辑的混乱,导致我对于后面业务逻辑的变更十分费力,测试成本也是指数级上涨,另外UI逻辑杂乱,适配iPhone X的时候也遇到了一些小麻烦。
现在的架构设计的原因是什么?这样设计业务需求是否合理?是否有优化的空间?
分析现状与判断未来
无论是客户端还是服务端,亦或是两端数据交互上,IM业务的架构设计本身就存在很多问题。因为时间短暂,不太可能一次性解决所有的问题,特别是对于服务端来说,一次性的大规模重构可能性极低。
对于现在来说:
- UI重构是首当其冲的, 在保证现有逻辑的基础上,重新设计UI层的逻辑结构,保证代码的复用性和可扩展性,为了将来有可能的业务升级留足空间。
- 梳理基础组件,比如HTTP、长连接协议和数据库,还有其他的一些工具类,通过封装成组件和组件引用来是他们能从业务逻辑中独立出来。便于独立维护、升级甚至替换。
- 将原有的数据操作逻辑从UI逻辑中完整抽离出来,需要达到向下对基础组件要有封装和控制,向上对业务逻辑要有承接,而且依然要做到耦合度尽量的低。
- 因为IM部分,我司的两个App这部分代码有90%的代码重合,需要考虑两端通用的问题。
架构设计
- 工厂模式
- 瘦Model
- 去model化的Cell
- 项目优先,分离核心业务模块组成pod
- 考虑业务变更可能性,尽可能向上保持API稳定性
- KVO进行反向传值
着手动工
UI层重构
UI层的重构是最先开始的,无论架构怎么变,UI层都是直接面向用户的,直接承载了公司的业务功能实现,所以为了灵活适应业务的升级或变化,给用户一个好用流畅的入口,UI层设计上要尽可能的灵活。耦合尽可能的小,流畅性上要有保证。
重构思路相对简单,重写View和Controller,去除冗余复杂的UI逻辑代码,规范并统一第三方框架使用,封装公用组件,隔离胶水代码,设计灵活的UI结构。
因为IM系统的最主要UI仍然是TableView,所以针对TableView的各种优化就是重中之重,我着重说一下我对于复杂Cell类型的设计方案。
1、共有的组件有很多,比如时间、头像、背景气泡等等,所以说子类继承父类是最基础的方案。
2、弃置Autolayout的UI书写方式,完全用Frame来写UI布局。Cell高度以及内部UI组件的布局和位置,通过异步计算并缓存为LayoutModel,通过这种方式降低计算的重复耗时操作。
3、有的消息类型只是负责展示,但是有的确有相对复杂的业务逻辑,但是为了防止Cell代码的膨胀,采用了瘦Cell的方式分离逻辑,力求使Cell尽量只负责UI的承载和展示,增加helper层处理相关逻辑。
4、因为虽然是相同的一个数据,但是呈现的方式会存才差异化,所以采用瘦Model的形式,通过创建Helper对取到的原始数据进行相对应的加工,直接提供给业务逻辑处理好的数据。在AFNetworking给的Demo中,是一个典型的胖Model的例子,倒不是说他的例子不好,只是随着业务逻辑的复杂以及生数据和熟数据的差异越来越大的时候,胖Mode的代码量会几何级数般的膨胀,所以还是要因地制宜,具体情况具体分析。
5、利用Factory模式分离出关于复杂Cell类型的判断,包括初始化、赋值等。
6、使用KVO取代delegate进行反向传值,用以减少代码耦合。
关于如何保证UI性能,可参考我的另一篇Blog,iOS 性能优化的探索.
最终结果,第一个完整case的UI层Controller代码,从3000行直接缩减到了1200行,Controller中没有复杂的多方数据处理逻辑,复杂的逻辑判断。只作为UI展示以及接口调用,完全剥离了数据逻辑的处理,所有的处理逻辑由下面的数据逻辑层处理。
数据逻辑层重构
我再分析了业务需求并设计了架构之后,决定重构以自下而上的顺序来进行,于是第一部分就是对于数据逻辑层的重构。步骤如下:
此部分,分为以下三层结构:
1、业务数据逻辑层
2、适配器层(adapter)
3、基础组件服务层(server)
1、业务数据逻辑层
这部分的主要作用是直接受到UI层的调用,负责长连接以及短连接的建立,数据库的初始化操作等。
向上直接承接UI逻辑和业务逻辑,是高度面向业务封装的接口。比如在发送照片消息的时候,只需要将调用API传入Image对象,其他的流程比如说是上传资源以及组成message对象等,则不需要上一层调用和考虑。
所以这一层尽可能的会很薄,不会有特别多的逻辑代码。
2、适配器层(adapter)
这部分的主要工作是承上启下,承接上一层的面向业务的封装,调用下一层基础组件服务层的接口,可以说绝大多数的接口封装都集中在这一层。因为我们有些业务的长连接和短连接的使用上不是很合理,所以我将长短连接都封装到了一个网络服务的类中,此后假如长短连接的业务产生了变化,但仍可以保持向上的接口稳定性。
举个例子说,当推送来一条消息之后,是通过长连接,但是需要收到数据之后在进行AFN的操作完成消息体完整数据的获取,之后要存入数据库并且将是否读取状态设置为NO,当用户读取当前消息之后,将这部分消息还要更新为已读状态。
这部分操作涉及到了所有的组件的操作,但是反馈到最上面一层的时候,大概只是新的消息,并且是完整的消息,然后再刷新UI。所以说,这一层的业务量比较大,几乎是要按照各种标准操作,完整的处理好所有组件的接口。
3、基础组件服务层(server)
这部分基本可以说是基于IM本身的业务特点,对于基础组件的调用封装。
包括:
1、数据库部分,对于IM消息的数据结构,封装的对于数据库的创建,以及增删改查等接口。以及基于业务的一些接口,例如一次性设置当前聊天的所有消息为已读状态等接口。
2、AFN部分,这部分相对来说就很简单了,基本上是依赖于AFN封装的接口,比如获取当前User的详细信息等。
3、长连接部分,包括对于长连接协议的创建连接、断连以及心跳超时上报等操作,也包括了发送消息和收到消息回调等底层操作。
拆分之后的Manager层代码量所见到原来的40%左右,于是改名为Session层。
基础组件层的重构以及封装
这部分因为属于公用的基础组件,所以相对来说只是基础组件的比如说AFN以及数据库(FMDB)是整个App的组成,所以没什么其他的操作,只是单纯做了一层逻辑上分层。
但是对于长连接我们做了一些定制,比如:
1、增加了重连的逻辑机制。
2、增加创建连接以及断掉连接时候各种状态的判断等。
主要任务还是集中在对于协议库本身的逻辑补充和健壮性优化等。
其他操作
1、创建枚举文件,扩展标准化的枚举变量。
2、合并以及分割Model,随着业务的扩展,原来的Model设计已经不符合当下的业务发展,根据现在固定的业务,重新设计了Model的集成关系,对于分化严重的也做了重新分割。
3、分离并封装了胶水代码到一个大的工具类,便于调用和调试。
走过的弯路
1、过度思考代码解耦合而忽略了业务逻辑复杂性,错将组件化各组件的解耦合的逻辑应用在了本来就是高耦合的MVC架构上。尝试使用去model化的Cell,但是实际操作环节发现增加了大量的逻辑判断,无形中将Model本该处理的业务逻辑转接到Controller和View上,表面上看上去API简洁到家,但是上手代码量并不算小,不利于维护。
2、在一开始采用了MVCS的设计重构UI层,简化Controller中对于Model的处理,在Store中进行了主动和被动网络逻辑、本地数据库调用等。但事实上最后通过封装统一入口的方式将获取数据的逻辑全部从UI逻辑层剥离开,下沉到了数据逻辑层,对于UI层来说只需要考虑的是进行了调用获取数据的API操作或者是被动受到了新的数据,不需要考虑数据来自于服务端、Cache还是本地数据库,也不需要考虑后面的逻辑。
3、对于UI层和下一层的数据沟通,虽然采用了KVO的方式回调,降低了耦合性,但是仍然存在参数复杂的情况下,传递过多的Key的情况,导致解析稍显困难和复杂。
4、Cell的继承,看上去是一个很直观的设计,但是随着重构代码量的增加以及业务变化发现继承过程中会存在很多问题,通过面向协议等方式或许可以解决继承中的问题。
总结以及思考
架构设计的时候,一定要预判用户的使用习惯,判断未来的业务导向,尽可能的降低代码侵入性和耦合性。对于性能产生的影响的地方,通过以上几点来设计架构。
架构设计分层要清晰,API设计要尽可能简洁,避免暴露过多的接口和参数,避免模块之间的紧耦合,UI设计要尽可能灵活。
重构前,需要思考切入点,是从上值下、从下至上,还是模块化抽离。
另
已从这家公司离职,部分逻辑全凭记忆整理,如果有疏漏或错误,还请大家海涵。
Refrence
- iOS应用架构谈 开篇
- iOS应用架构谈 view层的组织和调用方案
- 去model化和数据对象