复杂业务下UIViewController的减负工作

2,400 阅读15分钟

前言

绝大部分的开发者一开始都是从MVC架构开始进行开发的,而后有了MVCS、MVP以及MVVM等等。

《接手一个不合格的业务线代码,我是如何去维护以及重构的》中我讲了一下我对于接收的一个项目优化的一些思路和具体办法,在实际的聊天页面优化过程中也需要具体的去操作TableView视图优化的技术细节,因此,我会在下面针对TableView来举几个可以为Controller减负的例子。

关于流畅度优化的一些具体方案和思路,我在《iOS 性能优化的探索》里面有过描述和具体实现的Case,大家可以去参考一下。本篇文章着重讲述关于代码结构优化的问题。

优化概述

最开始的时候,面对Controller巨大的代码量,我第一选择是通过 #pragma mark - XXX来做功能区分,这样的确是在一定程度上解决了Controller里面逻辑的划分问题,在代码易读性上有所改善,但是无法解决代码量巨大的问题。后面的我开始利用Category来进行功能性的抽离和划分,比如在AppDelegate中其他模块的初始化工作,将功能相同的代码逻辑进行抽离,化简Controller中复杂逻辑。以上操作最终也只是一定程度上减少了一个类当中的代码量,仍然不能够尽善尽美的化简和分层各部分逻辑。

复杂Cell类型的TableView,我相信只要有一定开发经验的人都会遇到很多。这种情况下,大多伴随着复杂的业务逻辑、繁多的字段,不仅仅会造成Model逻辑复杂,Cell中也会有大量的复用或者不复用的逻辑,Controller当中也会充斥着各种if的判断逻辑,所以以下主要是针对复杂Cell类型的TableView优化的一些方案。

可采用的方案

一、Factory模式

相对来说,这种方式是对于IM聊天界面这种类型众多、逻辑相对统一的页面是一种立竿见影的设计模式。对于IM聊天界面来说,最繁杂的代码在于处理聊天卡片样式众多的问题,因为卡片样式类型众多,左右展示方向以高度的不一致导致了会有大量的代码进行处理。

1)总体使用思路

Factory模式则精简了以上逻辑,把Cell的创建以及复用过程通过工厂化抽象出来,Controller只负责调用传参而不负责创建乃至复用逻辑。这样就可以把在处理最繁重业务逻辑的两个方法

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

中的各种if判断中抽离出来。

那么接下来就是要进一步将抽离出来的if判断进行简化操作,我们有几个工具:

  • 面向对象
  • 子类继承
  • 协议
  • map存储
  • cache

2)组件拆分思路

  1. 对于IM列表来说子类继承方式是再合理不过的,通过在父类中封装基础UI控件以及定义对外暴露方法,从而可以让子类卡片能够能集中的去实现各自的业务逻辑,而不需要再去重复的处理相同部分的业务逻辑以及UI布局。同时因为在父类中已经定义好了通用的传参入口,这样可以在工厂类中直接通过同一API来设置cell的参数。
@protocol IMBaseCellDelegate <NSObject>

@end

@interface IMBaseCell : UITableViewCell

@property (nonatomic, weak) id<IMBaseCellDelegate> delegate;
@property (nonatomic, strong, readonly) IMMessage *message;

- (void)setupMessage:(IMMessage *)message;
+ (CGFloat)heightForMessage:(IMMessage *)message;
@end
  1. 面向对象的意思则是View对象自己处理自己的业务逻辑,Cell通过自己类型的消息对象,可以计算出自己的MessageContainerView的高度数据等等。计算之后返回给Factory类Cache存储,这样可以减少重复对于消息高度的计算,节省时间以及资源,提高流畅性。类似技术《iOS 性能优化的探索》中有对应的介绍。

  2. 当然,Protocol的使用也是十分重要。因为子类卡片通常拥有自己独特的业务逻辑,比如说类似微信公众号的消息卡片,可点击区域存在多个,这个时候可以通过协议集成的方式,将当前卡片的delegate投射到落地controller上。

  3. map的作用是基础性的,因为IM消息中类型的固定,可以将消息类型和对应的cell class做抽象映射,完全规避掉了通过if或者switch的冗长的判断逻辑。

+ (void)registerTableViewCells:(UITableView *)tableView{
    @weakify(self)
    [[[self cellConfigurationMapper] allKeys] enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        @strongify(self)
        kHGIMContentType messageType = (kHGIMContentType)[obj integerValue];
        [tableView registerClass:[self cellClassForMessageType:messageType]
          forCellReuseIdentifier:[self cellIdentifierForMessageType:messageType]];
    }];
}

+ (HGIMBaseCell *)dequeueReusableCellWithMessage:(IMMessage *)message forTableView:(UITableView *)tableView{

    NSString *identifier = [self cellIdentifierForMessageType:(kHGIMContentType)message.type];
    HGIMBaseCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    [cell setupMessage:message];
    return cell;
}

+ (NSDictionary *)cellConfigurationMapper {
    static NSDictionary *mapper = nil;
    if (!mapper) {
        mapper = @{
                   @(kIMContentText) : @{@"class" : [IMTextMessageCell class]},
                   @(kIMContentPicture) : @{@"class" : [IMImageMessageCell class]},
                   @(kIMContentMutiRichText) : @{@"class" : [IMMutiRichMessageCell class]};
    }
    return mapper;
}

3)Factory模式的不足

Factory模式最大的优点有时候在并不适合的业务场景下也可能变得优势尽丧,Factory模式顾名思义需要标准化和集中化,如果遇到一些集中程度不够高的业务场景,虽然仍然可以通过工厂模式进行cell创建、继承以及复用等,但是复用程度没有那么高的Cell以及复杂的业务逻辑将会让Cell的Protocol刷新和继承逻辑失控,将本来可以集中管理的部分业务逻辑代码没有办法很好的集中处理。

二、胖/瘦Model 与 MVCS

1)胖/瘦Model

大概很多人不知道Model还会有区分,所以我在这里简单做个介绍。

  1. 胖Model

    因为业务逻辑复杂,以及前后端弱耦合的需要,大部分情况下服务端返回的原始数据都是无法直接拿来给View使用的数据,于是我们可以在对Model进行赋值的时候进行一系列操作,通过调用Get方法来直接调用处理后的原始数据。好处是减轻了Controller里面琐碎的数据处理逻辑,让Controller可以集中注意力处理事件,也可以让Model复用的时候节省很多时间,毕竟几乎所有的原始数据处理在Model里已经完成,不需要挨个Controller里面处理一遍。

    所以说,胖Model包含了部分弱业务逻辑。胖Model要达到的目的是,Controller从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上

    但是,这也不是完美的,遇到熟数据复用度不高、或者说Model和具体业务有绑定耦合关系的时候,胖Model就会产生问题,会随着复用的增多导致适应不同业务的代码不断增加,最后就很有可能控制。

Raw Data:
    timestamp:1234567

FatModel.h:
    @property (nonatomic, assign) CGFloat timestamp;
    - (NSString *)ymdDateString; // 2015-04-20 15:16
    - (NSString *)gapString; // 3分钟前、1小时前、一天前、2015-3-13 12:34
FatModel.m:
    - (NSString *)ymdDateString {
        // deal with timestamp 、、、、
     }
    - (NSString *)gapString {
        // deal with timestamp 、、、、
      }
  1. 瘦Model

    和上面胖Model相反,完全是原始数据赋值,不做任何处理,处理逻辑完全交给Controller或者数据中间层Adapter/Helper来处理。用Controller的话就很麻烦,处理代码很有可能是重复使用,但是使用中间层的话,相对来说比较省事,一次调整,处处可复用。即保证了Model尽可能的薄,也保证了对于业务使用足够灵活,同时也减少了Controller的压力,同一个Model可以针对不同的业务需要进行适应。

    但是使用起来也需要注意中间层数据转换耗时的问题,以及需要多维护一个工具类的问题。假如是TableView上Cell的数据,是否需要增加Cache以减少多次数据转换的耗时是需要考虑的点。

Raw Data:
    "name":"casa",
    "sex":"male",

SlimModel:
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, strong) NSString *sex;

Helper:
    #define Male 1;
    #define Female 0;
    + (BOOL)sexWithString:(NSString *)sex;

Controller:
    if ([Helper sexWithString:SlimModel.sex] == Male) {
        ...
    }

2)MVCS

MVCS中的S是Store的意思,大概的作用就是将获取数据和处理数据以及数据转换的操作放到单独的Store中,于是我们可以理解MVCS架构其实是瘦Model的一种扩展。不仅仅是原始数据的处理,连获取数据的操作也一并交给了Store来做。所以说MVCS使用的前提是,它假设了你是瘦Model,同时数据的存储和处理都在Controller去做,这在一定程度上降低了Controller的处理压力。

苹果自身就采用的是这种架构思路,从名字也能看出,也是基于MVC衍生出来的一套架构。从概念上来说,它拆分的部分是Model部分,拆出来一个Store。但从实际操作的角度上讲,这个Store专门负责数据存取,它拆开的实际上是Controller中的逻辑。因为不仅仅是数据存储部分,连带着复杂的数据处理部分,包括网络请求甚至数据库等等繁杂的逻辑都从Controller当中拆解出来,降低Controller的压力从而能让它更集中精力的去处理业务逻辑。

所以,相对来说这个架构适合Controller需要处理十分复杂的数据逻辑一类的情况,假如数据逻辑并没有那么复杂,实际上并不适合这种架构设计。

三、MVP

1)架构简单介绍

实际上 MVC 与 MVP 之间的区别其实并不明显,而且 MVP 也是从 MVC 中衍生出来的结构,两者之间最大的区别就是 MVP 中使用 Presenter 对视图和模型进行了解耦,它们彼此都对对方一无所知,沟通都通过 Presenter 进行。

MVP中的P代表Presenter,字面上和MVC相比C变成了P,那么细究的话,不只是Controller发生了变化,View也跟着发生了变化。

Presenter层的出现隔离了View和Model两层之间的直接沟通,View不直接和Model产生接触,都通过 Presenter 传递数据。View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。

image.png

如上图所示,和MVC本质的区别就在于 MVC 的Action都经过Controller来进行处理,并且更新Model和View。而出现了Presenter之后View-Action-Model就一一的绑定到了一起,因为iOS中ViewController也属于一个View类,所以也有自己的逻辑的所需要的Presenter,实际的更新逻辑如下:

11111.png

这张图片大家看到当View相应用户操作时候的一个大致流程,View、Presenter和Model通过Api来实现解耦合,从而降低了业务耦合度。

2)优缺点

  • 优点是显而易见的,主要就是将复杂的逻辑从Controller中抽取出来,实现了对于Controller的降压和代码量的减少,让业务逻辑更加直接,同事也便于替换View层,即使UI不断变化也不会和业务逻辑产生耦合。
  • 缺点也是显而易见的,比如横向几个View进行交互和更新的时候,处理起来就会出现麻烦,另外就是因为引入了Presenter,中间层就会变得很大导致代码量上涨,增加了简单页面的维护成本。

四、MVVM

关于 MVVM 在iOS中的应用我在之前的文章iOS开发中 MVVM 设计模式的探究中有过一定讨论,本篇文章就不再赘述了。

五、其他方案

1)Protocol的一些思路:

2)减小Controller体量

我采用的方案

我最后采用的方式实际上是Factory模式+MVCS的结构,原因有两个:符合对于业务初步抽离的需要,符合代码隔离的需要。

项目现状和期望

  1. 作为IM的一个功能组件来说,最复杂的位置主要集中在数据的处理以及UI逻辑这两部分上。其中数据处理因为属于核心逻辑,涉及到长链接、短连接以及数据库的处理,这部分是个繁杂的逻辑,需要将他们进行逻辑隔离和封装。对于UI层来说,只需要构建消息体发送或者收到消息体之后进行UI刷新,包括消息体的状态update、扩充完整的消息数据以及数据库处理等实际上都不需要UI层关心。

  2. 当前项目中长短链接以及数据库的处理和业务逻辑杂糅在一起,需要整体隔离封装,除了代码隔离之外,还要根据业务逻辑、数据处理逻辑、以及面相上层Api稳定以及向下组件可替换性等进行整体设计。

3.因为重构之后还有二期开发计划,计划封装成独立组件,这部分组件同时应用于C/B两端,所以需要针对组件化进行设计开发。

Factory模式大致结构

+ (void)registerTableViewCells:(UITableView *)tableView{
    @weakify(self)
    [[[self cellConfigurationMapper] allKeys] enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        @strongify(self)
        kHGIMContentType messageType = (kHGIMContentType)[obj integerValue];
        [tableView registerClass:[self cellClassForMessageType:messageType]
          forCellReuseIdentifier:[self cellIdentifierForMessageType:messageType]];
    }];
}

+ (HGIMBaseCell *)dequeueReusableCellWithMessage:(IMMessage *)message forTableView:(UITableView *)tableView{

    NSString *identifier = [self cellIdentifierForMessageType:(kHGIMContentType)message.type];
    HGIMBaseCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    [cell setupMessage:message];
    return cell;
}

+ (NSDictionary *)cellConfigurationMapper {
    static NSDictionary *mapper = nil;
    if (!mapper) {
        mapper = @{
                   @(kIMContentText) : @{@"class" : [IMTextMessageCell class]},
                   @(kIMContentPicture) : @{@"class" : [IMImageMessageCell class]},
                   @(kIMContentMutiRichText) : @{@"class" : [IMMutiRichMessageCell class]};
    }
    return mapper;
}

这部分上面有过一点介绍,主要的目的就是将初始化、复用、Cell高度计算等从Controller里面隔离出来,通过Cell的子类继承以及协议等方法将Cell调用标准化,将整个TableView逻辑不断地下放到Cell当中。

Store-数据处理设计结构

IM结构设计(简化版)

有简化版结构图可以看到,主要分层为4层:

  • Foundation层:主要是核心第三方框架层。
  • IMServer层:主要是针对已有的第三方框架进行了初步的接口封装,包括AFN的调用、FMDB的一些初始化逻辑、长链接的初始化等等。
  • IMAdapter层:这一层主要是负责根据业务逻辑和项目的数据结构的一些封装,包括接口调用、长短链接处理、数据存储处理和更新等。
  • 业务数据逻辑层:这一层相对来说比较简单,主要是针对群聊以及私聊的业务逻辑进行封装,通过各自遵循Protocol的方法将自己暴露给Client,UI层直接调用Client中的组件和属性来实现私聊和群聊的各自业务逻辑。

这样设计有2个目的:一是可以实现组件核心逻辑和UI层以及基础组件上下相互拆解耦合,二是经过IMServer层和IMAdapter层分层之后可以将向下的组件封装和向上的业务功能封装拆分,继续降低耦和

总结和思考

在我看来,无论架构如何改变、结构功能如何重新划分,一切都是对于MVC架构的改进和功能性切分,减轻Controller的压力,而后继续减轻Cell、View和Model的压力,都是不断优化逻辑的过程,本质上说,还是以各种方式调配View、Model和事件的处理调度。

那么,到底该选用哪一种设计模式使用呢? 我的个人看法是,刀在手中,需要怎么用,就怎么用!

不用的业务模块不能全部套用一套技术方案和架构,比如说简单的一个列表页面,套用MVVM的架构很有可能比使用MVC多出一半的代码量,性价比不高。所以说,需要根据具体业务逻辑分析,然后在特定模块或者若干个业务逻辑复杂的页面进行优化的时候进行减负操作,然后再选中一个最符合业务逻辑和未来技术规划的技术方案,没有必要整个项目都套用一个架构,否则就会让整个代码僵化。

上面刚说了,各种设计模式和减负方法,总结起来都是围绕着Model、View和Controller三者展开的,大多数情况下都是第一步先拆分Controller,继而对另外两这个再进行功能拆分和整合,一切都是严格跟踪数据走向和生命周期来进行操作。所以说,只要是理解这一点,剩下的就迎刃而解了。

Refrence