您的当前位置:华乐网 > 头条 > 社会 >

    iOS的MVC框架之控制层的构建(下)

    来源:微信公众号 作者: 编辑:华乐网 时间:2018-10-10 23:03
    导读: 程序员大咖点击右侧关注,免费进阶高级!

    iOS的MVC框架之控制层的构建(下)-爱尖刀

    iOS的MVC框架之控制层的构建(下)-爱尖刀

    程序员大咖
    点击右侧关注,免费进阶高级!
    iOS的MVC框架之控制层的构建(下)-爱尖刀

    作者:欧阳大哥2013
    链接:https://www.jianshu.com/p/4f7b3c9801f5
    任何形式的转载都请联系作者获得授权并注明出处。

    在我的iOS的MVC框架之控制层的构建(上)一文中介绍了一些控制层的构建方法,而这篇文章则继续对一些方法进行展开讨论。MVC被众多开发者所诟病的C层的膨胀,究其原因不外乎有如下几点:

    1. 所有视图的构建和布局代码都在控制器中完成。有很多同学不喜欢系统提供的Storyboard和XIB来构建视图,而是喜欢通过代码的形式来完成视图界面布局,并且通常这部分代码都集中在loadView或者viewDidLoad或者通过懒加载的形式分散在各处。通过代码来构建和布局视图的代码量有可能会超过您视图控制器总代码量的50%。

    2. 对服务端的请求,往往就是包装了一层非常薄的请求层,通常称之为APIService。
      这部分代码只是简单封装了对服务端URL的请求,同时通过一些报文转数据模型的第三方框架直接将报文转化为数据模型并通过异步回调的形式回吐给控制器或者视图。APIService的简单实现却增加了控制器的负荷,导致控制器除了要构建视图并且请求网络服务外还要担负非常多的一部分业务逻辑的实现。

    3. 对于一些复杂展示逻辑的功能界面没有进行合理拆解和有效设计导致所有代码都在一个视图控制器内完成,从而导致控制器膨胀臃肿。

    4. 在应用中最多使用的UITableView以及UITableViewCell中的数据更新的处理机制使用不恰当导致delegate中的方法实现异常的复杂,尤其是那些复杂的UITableViewCell的更新处理不得当导致代码混乱不堪。

    可以看出框架本身没有问题,问题在于使用的人不了解或者不恰当的设计思想导致问题出现了。当出现问题时我们首先应该反思的是自己哪里不对而不是去怪别人哪里不对。(这个鸡汤撒得真LOW!!) 怎么解决上面所说的导致C层膨胀的几个问题呢?这也是这篇文章所要重点介绍的。

    不同代码的构建时机

    控制器类是一个功能的调度总控室,而且他还通过模板方法的设计模式提供给了我们在控制器的生命周期内各阶段事件发生时的处理回调。比如控制器构建时(init)、

    视图构建时(loadView)、视图构建完成时(viewDidLoad)、视图将要呈现到窗口前(viewWillAppear)、视图已经呈现到窗口(viewDidAppear)、视图将要从窗口删除(viewWillDisappear)、视图已经从窗口删除(viewDidDisappear)、视图被销毁(viewDidUnload,这个方法在iOS6.0以后将不起作用了)、控制器被销毁(dealloc)。为了实现功能,我们可能需要在上述的某个地方添加对应的处理代码。如何添加代码?以及在上述的模板方法中添加什么样的代码?就非常的关键了。在这里面我想强调一点的是虽然控制器中拥有了一个view的根视图属性,但是控制器的生命周期一般要比根视图的生命周期要长,而且有可能会出现一个功能在不同场景下的视图呈现完全不一样,或者有可能会通过重新构建视图来实现一些换肤功能的场景。在iOS6以后的控制器中只提供了视图构建以及构建完成的模板方法,但却不再提供视图被销毁之前或者之后的模板方法,因此我们在loadView以及viewDidLoad中添加代码时就一定要考虑到这么一点,因为他不像其他的方法一样提供了互逆处理的机制。

    • 控制器初始化(init)
      如果你的业务模型对象的生命周期和控制器的生命周期一样,那么建议将业务模型对象的构建放在控制器的初始化代码中,当然前提是你的业务模型对象是一个轻量级的对象,如果你的业务模型对象的构建特别消耗时间那么不建议放在控制器的初始化中构建而是通过懒加载或者在某个触摸事件发生时再构建。如果你的控制器由多个子控制器组成,那么子控制器的初始化工作也在这里完成最佳。在控制器初始化时我们还可以初始化以及创建一些其他的轻量级的属性,这些属性或者变量的生命周期和控制器的生命周期一致。

    • 视图构建(loadView)
      如果你的视图是通过SB或者XIB来建立的,那么恭喜你,你可以省略这部分代码。如果你是通过代码来构建你的视图,那么你就有必要在这个地方添加你的视图构建和布局代码。你需要重载loadView的方法,并在最好在这里完成所有视图的构建和布局。如果你想复用默认的根视图作为自己的根视图那么你需要在构建你的其他子视图之前调用基类的loadView方法,而如果你想要完全构建自己的根视图以及子视图体系那么你就不必要调用基类的loadView方法。很多人都喜欢在viewDidLoad里面进行视图的构建,其实不是最佳的解决方案,因为根据字面意思viewDidLoad里面添加的应该是视图构建并加载完成后的一些处理逻辑。如何在loadView中更加优雅以及合理的构造界面布局代码,后面我将会给出一个具体解决方案。

    -(void)loadView
    {
       /*
       自定义根视图的构建,不需要调用基类的方法。你也可以直接在这里将UIScrollView或者UITableView作为根视图。
       这样就不必在默认的根视图上再建立滚动视图或者列表子视图了。
       */
        self.view = [[UIView alloc] initWithFrame: [UIScreen mainScreen].bounds];
    
       //...建立其他子视图。
    
    }
    • 事件绑定的代码(viewDidLoad)
      当视图构建完毕后系统会调用viewDidLoad。因此您应该在这里完成一些业务逻辑初始化的动作、业务模型服务接口的初始请求、一些控件的事件处理绑定的动作、视图的delegate以及dataSource的设置。也就是这里一般用来完成视图和控制器之间的关联处理以及控制器和业务模型的关联处理。在viewDidLoad中最适合做的就是实现视图和控制器之间的绑定以及控制器和业务模型之间的绑定操作。这里不建议进行视图的构建,以及一些涉及到整个控制器生命周期相关的处理。

    • 视图的呈现和消失(viewWill/DidAppear,viewWill/DidDisappear)
      视图的呈现和消失有可能会被反复调用。建议在这里完成定时器、通知观察者的添加和销毁处理。一般来说定时器和观察者都只是在界面被呈现时产生作用,而界面消失时则不处理,因此在这里添加定时器和通知观察者是最合适的。而且还有一个好处就是在这里实现定时器和观察者时不会产生循环引用而导致控制器不能被释放的问题发生。

    • 控制器被销毁(dealloc)
      控制器被销毁时表明控制器的生命周期已经完结了。一般情况下不需要添加特殊的代码,这里一再强调的就是:
      一定要在这里把各种控件视图中的delegate以及dataSource设置为nil!
      一定要在这里把各种控件视图中的delegate以及dataSource设置为nil!
      一定要在这里把各种控件视图中的delegate以及dataSource设置为nil!

    重要的事情说三遍!不管这些delegate是assign还是weak的。

    懒加载

    懒加载的目的是为了解决按需创建使用以及可选使用以及耗时创建的场景。在某种情况下使用懒加载可以加快展示的速度,懒加载可以将某些对象的创建时机延后。那么是不是要将所有的对象的创建都采用懒加载的形式进行创建?
    答案是否定的。 有不少同学都喜欢将控制器中的所有视图的创建和布局都通过懒加载的形式来完成,如下面的代码片段:

    @interface XXXViewController()
       @property(strong) UILabel *label;
       @property(strong) UITableView *tableView;
    @end
    
    @implementation XXXViewController
    
    -(UILabel*)label
    {
         if (_label == nil)
        {
              _label = [UILabel new];
              [self.view addSubview:_label];
             //有些同学会在这里添加附加代码比如布局相关的代码
        }
        return _label;
    }
    
    -(UITableView*)tableView
    {
         if (_tableView == nil)
        {
               _tableView = [UITableView new];
              [self.view addSubview:_tableView];
              _tableView.delegate = self;
             //有些同学会在这里添加附加代码比如布局相关的代码
        }
        return _tableView;
    }
    
    -(void)viewDidLoad
    {
        [super viewDidLoad];
    
        self.label.text = @"hello";
        [self.tableView reloadData];
    }
    
    @end

    看起来代码很简洁也很清晰,起码在viewDidLoad中是这样的。但是这里面却有可能存在着一些隐患:

    • 视图层次顺序被打乱和代码分散
      因为视图都是懒加载并且分散的,因此你不能从整体看出视图层次结构是如何的,以及排列的顺序是如何的。这就为我们的代码阅读以及调试和维护增加了困难。

    • 职责不明确
      懒加载的主要作用是延迟创建,但是上述的视图属性的重写却已经超出了单纯的创建的范畴了,除了创建视图之外还实现了视图添加到父视图的功能以及进行布局的功能,更有甚者还有可能实现其他更加复杂的逻辑。这样就会导致一个get属性的实现承载的功能过多,严重的超过了一个方法所应承担的责任。在使用时我们只是简单的将其当做一个读取属性来使用并且还有可能发生有些代码重复的问题。

    • 莫名的问题和崩溃
      懒加载视图使得我们的视图属性必须要设置为strong类型的,而且代码的实现是只创建一次。如果因为某些原因使得我们的控制器里面的所有视图都需要重新创建(比如换肤)时那么就有可能导致这个懒加载的视图不会再次被创建而产生界面上莫名其妙的问题。更有甚者因为在懒加载中实现过多的代码导致在某些地方访问属性时产生了崩溃。

    因此不建议对一个控制器里面的所有视图构建都采用懒加载模式,视图的构建和布局应该在loadView中进行统一处理。懒加载的方式不能滥用,尤其是视图的构建代码。我们应该只对那些可选存在的对象以及那些有可能会影响性能的对象采用懒加载的方式来进行构建,而不是所有的对象都采用懒加载的形式来创建。同时还需要注意的就是如果一定要采用懒加载来实现对象的构建时,在懒加载中的代码也应该尽量的简化,只需要实现创建部分的功能即可,而不要将一些非必要的逻辑代码放入到懒加载的实现处,越多的逻辑实现,就会对使用着产生越多的限制和不确定因素的发生。就以上面的例子来说使用者在调用self.label或者self.tableView时一般都只是将它们当做普通的属性来使用,而不会去考虑它们的内部还进行了如此多的设置和处理(比如完成布局和添加到父视图中去)。这样就可能会造成对这些属性的使用不当而造成灾难的后果。另外虽然你的视图的构建是通过懒加载的形式来完成的,但是如果你在比如viewDidLoad中大量的访问这些属性时一样的会产生视图的构建操作,这样其实和直接创建视图对象是一样的,并没有起到任何优化性能的作用,而且这样也是和懒加载的初衷是违背的。

    我们项目中的一个案例就是UITableView的创建使用的懒加载,里面除了创建UITableView的实例外还在里面设置了delegate的值以及其他代码逻辑。而这个UITableView又刚好是一个可选的显示视图。同时我们又在视图控制器的dealloc中对这个UITableView的delegate做了置为nil的处理。结果这段代码最终在线上出现了cra