zoukankan      html  css  js  c++  java
  • MVVM

    MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。

    MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点
    1. 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
    2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
    3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xaml代码。
    4. 可测试。界面素来是比较难于测试的,测试可以针对ViewModel来写。

    他把View和Contrller都放在了View层(相当于把Controller一部分逻辑抽离了出来),Model层依然是服务端返回的数据模型。而ViewModel充当了一个UI适配器的角色,也就是说View中每个UI元素都应该在ViewModel找到与之对应的属性。除此之外,从Controller抽离出来的与UI有关的逻辑都放在了ViewModel中,这样就减轻了Controller的负担。

    我简单的画了下MVVM的架构图。

     
    mvvm-arch.png

    从以上的架构图中,我们可以很清晰的梳理出各自的分工。

    • View层:视图展示。包含UIView以及UIViewController,View层是可以持有ViewModel的。
    • ViewModel层:视图适配器。暴露属性与View元素显示内容或者元素状态一一对应。一般情况下ViewModel暴露的属性建议是readOnly的,至于为什么,我们在实战中会去解释。还有一点,ViewModel层是可以持有Model的。
    • Model层:数据模型与持久化抽象模型。数据模型很好理解,就是从服务器拉回来的JSON数据。而持久化抽象模型暂时放在Model层,是因为MVVM诞生之初就没有对这块进行很细致的描述。按照经验,我们通常把数据库、文件操作封装成Model,并对外提供操作接口。(有些公司把数据存取操作单拎出来一层,称之为DataAdapter层,所以在业内会有很多MVVM的变种,但其本质上都是MVVM)。
    • Binder:MVVM的灵魂。可惜在MVVM这几个英文单词中并没有它的一席之地,它的最主要作用是在View和ViewModel之间做了双向数据绑定。如果MVVM没有Binder,那么它与MVC的差异不是很大。

    我们发现,正是因为View、ViewModel以及Model间的清晰的持有关系,所以在三个模块间的数据流转有了很好的控制。

    这里给大家推荐一篇博文猿题库iOS客户端架构设计,其架构图如下。

     
    猿题库Arch.png

    猿题库的架构本质上不是MVC也不是MVVM,它是两种架构演进的一种架构模式。博文中对于MVC和MVVM的优缺点做了简单的介绍。

    • MVC缺点:Massive View Controller,也就是胖VC。
    • MVVM缺点:1.学习成本高。2.DEBUG困难。

    但博文中关于MVVM的阐述有两处笔者不太赞同。

    • MVVM绝不等于RAC,所以MVVM并不存在DEBUG难的问题。
    • MVVM正是因为跟RAC不对等,所以博文中“MVVM一个首要的缺点是,MVVM的学习成本和开发成本都很高”这句话也是不成立的。

    MVVM架构本身并不复杂,而且不用RAC我们依然可以通过KVO、类KVO的方式来帮我们实现View和ViewModel绑定器功能。

    关于猿题库iOS客户端架构设计是否合理,因为笔者不了解其具体业务,所以不能妄下结论。但是有一点可以肯定的是,MVVM ≠ RAC

    一年一度的QA环节来了。

    Q:View和ViewModel之间是否一定要解耦?
    A:View持有ViewModel,ViewModel不能持有View(即ViewModel不能依赖UIKit中任何东西)。说明白了吧? 解耦是有一定成本的,不管是通过Category或者中间件,消息链条都会无形之中变长,会有一定的DEBUG成本。

    Q:为什么ViewModel不能持有View?
    A:这个很好理解啊兄dei,主要有两方面原因:1.ViewModel可测性,即单元测试方便进行。2.团队人员可分离开发(View和ViewModel开发可以是两个人同时进行)。

    MVVM结合RAC

    ReativeCocoa相信大家并不陌生,这个函数响应式框架在Github中已经有将近2w star 。RAC是个非常优秀的框架,它可以独立于MVVM而存在。如果只是把它理解成MVVM中View和ViewModel Binder角色的话,那就有点大材小用了。本文不会对RAC进行展开分析,感兴趣的可以自行实践一下。

    RAC特点:

    • 语法怪异,杂交种。(函数式+响应式编程组合)
    • 万物皆可盘。(事件信号RACSignal贯穿整个框架)
    • 把离散的函数调用撺成一坨。(个人感觉跟Promise很像)

    总结:RAC是一种编程思维的改变,所以其缺点很明显,学习成本很大!!!

    具体RAC的使用,可以参考官方文档,自行实践一下,这里不再展开。

    MVVM结合非RAC(IQDataBinding

    通过MVVM扫盲部分,我们了解到,Binder在MVVM中扮演了View和ViewModel数据通信者的角色。

    了解过Android开发的同学都知道,Java有个好东西,那就是注解(Annotation)。在开发Android App的时候,可以在XML中通过注解的方式标记View和ViewModel的绑定关系。编译器在编译过程中,会自动生成XML和ViewModel的绑定类(Binder)。

    注解功能很强大,但是不幸的是,我们iOS(Objective-C)没有!!!Swift有没有注解笔者不太清楚,有知道的童鞋可以告诉我一下。

    接下来我们将一步步实现一个View和ViewModel双向绑定的框架。

    方案一:“躺爽法”

    名次解释:所谓“躺爽法”(实在想不出用什么词描述这种最基础的方法了)和KVO,是相对于ViewModel >>> View而言的。

    1.ViewModel >>> View:View不需要关心ViewModel属性的改变,View只需要提供更新视图的接口即可,ViewModel属性改变之后调用View提供的API更新视图。所以View这里没有做过多的事情,一切都是被动触发,所以我称作是“躺爽法”。

    2.View >>> ViewModel:用户操作视图,比如一个开关按钮,这时候要同步给ViewModel。我们知道View是可以持有ViewModel的,所以在View中我们可以直接拿到ViewModel指针,进而通过ViewModel暴露的方法而更新值。

    高能预警:这种最基础的方法,实际上是MVC!!!他本身没有解决“Massive View Controller”问题。也就是说为了ViewModel中不依赖于View,必须通过Controller中转,依然会有一堆胶水代码。所以这种解决方案并不是MVVM!!!不是故意给大家挖坑,只是意在提醒大家,阅读文章的时候要举一反三,更不要被一些脏乱差的文章混淆视听。

    方案一:KVO

    1.ViewModel >>> View:ViewModel属性改变之后,通知View进行视图布局。这种最熟悉不过,通过KVO即可实现。

    2.View >>> ViewModel:用户操作视图,通过ViewModel暴露的更新方法而更新值(设置属性值时要避开触发KVO监听,否则会出现死循环)。

    Talk is cheap,show me the code!
    我们以大家最熟悉的Cell举例子。
    ViewModel

    //
    //  IQMVVMDemoViewModel.h
    //
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface IQMVVMDemoViewModel : NSObject
    
    @property (nonatomic, copy, readonly) NSString *userName;
    @property (nonatomic, copy, readonly) NSString *userPwd;
    
    + (IQMVVMDemoViewModel *)demoViewWithName:(NSString *)userName withPwd:(NSString *)userPwd;
    - (void)updateViewModelWithName:(NSString *)userName withPwd:(NSString *)userPwd;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    //
    //  IQMVVMDemoViewModel.m
    //  
    
    #import "IQMVVMDemoViewModel.h"
    
    @interface IQMVVMDemoViewModel ()
    
    @property (nonatomic, copy, readwrite) NSString *userName;
    @property (nonatomic, copy, readwrite) NSString *userPwd;
    
    @end
    
    @implementation IQMVVMDemoViewModel
    
    + (IQMVVMDemoViewModel *)demoViewWithName:(NSString *)userName withPwd:(NSString *)userPwd {
        IQMVVMDemoViewModel *viewModel = [[IQMVVMDemoViewModel alloc]init];
        viewModel.userName  = userName;
        viewModel.userPwd   = userPwd;
        return viewModel;
    }
    
    - (void)updateViewModelWithName:(NSString *)userName withPwd:(NSString *)userPwd {
        _userName   = userName;
        _userPwd    = userPwd;
    }
    
    @end
    

    View

    //
    //  IQMVVMDemoView.h
    //  
    #import <UIKit/UIKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @class IQMVVMDemoViewModel;
    
    @interface IQMVVMDemoView : UITableViewCell
    
    - (void)updateViewWithViewModel:(IQMVVMDemoViewModel *)viewModel;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    //
    //  IQMVVMDemoView.m
    //  
    
    #import "IQMVVMDemoView.h"
    #import "IQMVVMDemoViewModel.h"
    
    @interface IQMVVMDemoView ()<UITextFieldDelegate>
    
    @property (nonatomic, strong) UITextField *userNameField;
    @property (nonatomic, strong) UITextField *userPwdField;
    @property (nonatomic, strong) IQMVVMDemoViewModel *viewModel;
    
    @end
    
    @implementation IQMVVMDemoView
    
    #pragma mark--Life Cycle--
    - (void)dealloc {
        [self.viewModel removeObserver:self forKeyPath:@"userName"];
        [self.viewModel removeObserver:self forKeyPath:@"userPwd"];
    }
    
    - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
        if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
            [self setupSubviews];
        }
        return self;
    }
    
    #pragma Public & Private Methods--
    - (void)setupSubviews {
        [self.contentView addSubview:self.userNameField];
        [self.contentView addSubview:self.userPwdField];
        /*
         这里做布局,不写了啊
         */
    }
    
    - (void)updateViewWithViewModel:(IQMVVMDemoViewModel *)viewModel {
        self.viewModel = viewModel;
        [self.viewModel addObserver:self forKeyPath:@"userName" options:NSKeyValueObservingOptionNew context:NULL];
        [self.viewModel addObserver:self forKeyPath:@"userPwd" options:NSKeyValueObservingOptionNew context:NULL];
    }
    
    #pragma mark--Delegates & KVO--
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        if ([keyPath isEqualToString:@"userName"]) {
            self.userNameField.text = change[NSKeyValueChangeNewKey];
        } else if([keyPath isEqualToString:@"userPwd"]) {
            self.userPwdField.text = change[NSKeyValueChangeNewKey];
        }
    }
    
    - (void)textFieldDidEndEditing:(UITextField *)textField {
        /*更新ViewModel*/
        if (textField == self.userNameField) {
            self.userNameField.text = textField.text;
        } else {
            self.userPwdField.text = textField.text;
        }
        [self.viewModel updateViewModelWithName:self.userNameField.text withPwd:self.userPwdField.text];
    }
    
    #pragma mark--Getters & Setters--
    - (UITextField *)userNameField {
        if (!_userNameField) {
            _userNameField = [[UITextField alloc]init];
            _userNameField.delegate = self;
        }
        return _userNameField;
    }
    
    - (UITextField *)userPwdField {
        if (!_userPwdField) {
            _userPwdField = [[UITextField alloc]init];
            _userPwdField.delegate = self;
        }
        return _userPwdField;
    }
    
    
    @end
    
    

    至此,我们大致把View和ViewModel之间数据通信方式给理清了。但是大家都知道KVO存在各种问题,而且每次监听一个属性都要写大量的代码(注册、移除、收到监听的处理)。所以方案一存在以下问题:

    • 直接使用KVO方式,每次都要写大量的注册、移除等代码,没有做到自动移除。
    • 如果没有移除监听可能直接导致Crash,使用姿势不方便。

    方案二:类KVO(IQDataBinding)

    名词解释:之所以称之为类KVO,是因为方案二本质上是通过KVO来实现的。不过IQDataBinding实现了自动移除,且支持函数式、链式调用,使用姿势比较优雅。

    空说无凭,我们来看看IQDataBinding如何使用

    Controller

    /*引入NSObject+IQDataBinding头文件*/
    - (void)configData {
        self.contentModel = [[ContentModel alloc]init];
        self.contentModel.title = @"lobster";
        self.contentModel.content = @"123456";
        
        /*View和ViewModel之间绑定*/
        [self.contentView bindModel:self.contentModel];
        
    }
    
    

    View

    /*ViewModel >>> View*/
    - (void)setUpSubviews {
        
    
        [self addSubview:self.loginTextField];
        [self addSubview:self.pwdTextField];
        
        self.loginTextField.frame = CGRectMake(0, 0, self.bounds.size.width, 30);
        self.pwdTextField.frame = CGRectMake(0, 40, self.bounds.size.width, 30);
        
        /*绑定ViewModel中title和content属性,发生改变自动触发View更新操作*/
        __weak typeof(self)weakSelf = self;
        self.bind(@"title",^(id value){
            weakSelf.loginTextField.text = value;
        }).bind(@"content",^(id value){
            weakSelf.pwdTextField.text = value;
        });
        
    }
        
    
    /*View >>> ViewModel*/
    - (BOOL)textFieldShouldReturn:(UITextField *)textField {
        [textField resignFirstResponder];
        if (textField.text) {
            /*函数式调用*/
            self.update(@"content",textField.text).update(@"title",@"lobster");
        }
        return YES;
    }
    

    IQDataBinding踩坑记:

    • View更新ViewModel属性时,如何让一个函数支持传输不同的参数类型?
    • View更新ViewModel时,如何避免触发KVO而导致死循环?
    • 如何自动移除KVO?

    View更新ViewModel属性时,如何让一个函数支持传输不同的参数类型?

    笔者借鉴了Masonry框架的解决方案,通过宏定义+不定参数解决了传输不同参数类型的问题。感兴趣的可以了解下Masonry中_MASBoxValue这个函数。

    View更新ViewModel时,如何避免触发KVO而导致死循环?

    很显然,通过setValue:forKey:函数会触发KVO回调,所以我的解决方案是获取到IVar,直接设置实例变量的值。但是object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value) 函数,只接收id类型的值。Stack Overflow查询之后,发现可以通过函数类型强转的方式来解决。

    如何自动移除KVO?

    这个问题就比较简单了,为了监控View的dealloc函数调用时机,我们可以通过Hook的方式,但是Hook不太推荐。尤其使用类似于Aspects(通过消息转发来实现,代价很高)进行Hook时,对于那种一秒钟调用超过1000次的业务场景会严重影响性能。所以我采用的方案是,通过给View添加一个关联对象来解决。因为我们知道对象释放时会先释放成员变量,然后再释放关联对象,所以我们可以在关联对象的dealloc方法里对观察者进行自动移除。

     /*给view添加一个关联对象IQWatchDog,IQWatchDog职责如下
         1.存储@{绑定的Key,回调Block}对应关系。
         2.根据@{绑定的Key,回调Block}中的Key,进行KVO监听。
         3.监听view Dealloc事件,自动移除KVO监听。
         */
        IQWatchDog *viewAssociatedModel = objc_getAssociatedObject(self, &kViewAssociatedModelKey);
        if (!viewAssociatedModel) {
            viewAssociatedModel = [[IQWatchDog alloc]init];
            viewAssociatedModel.target = model;
            objc_setAssociatedObject(self, &kViewAssociatedModelKey, viewAssociatedModel, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
    
    @interface IQWatchDog : NSObject
    
    @property (nonatomic, weak) id target;
    @property (nonatomic, strong) NSMutableDictionary *keyPathsAndCallBacks;
    
    @end
    
    @implementation IQWatchDog
    
    - (void)dealloc {
        [self.keyPathsAndCallBacks enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            [self.target removeObserver:self forKeyPath:key];
        }];
    }
    
    - (void)observeKeyPath:(NSString *)keyPath callBack:(observerCallBack)callBack {
        NSAssert(keyPath.length, @"keyPath不合法");
        /*加载默认值*/
        id value = [self.target valueForKeyPath:keyPath];
        if (value) {
            callBack(value);
        }
        /*添加观察者*/
        [self.keyPathsAndCallBacks setObject:callBack forKey:keyPath];
        [self.target addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        observerCallBack callBack = self.keyPathsAndCallBacks[keyPath];
        if (callBack) {
            callBack(change[NSKeyValueChangeNewKey]);
        }
    }
    
    - (void)removeAllObservers {
        [self.keyPathsAndCallBacks enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            [self.target removeObserver:self forKeyPath:key];
        }];
    }
    
    - (NSMutableDictionary *)keyPathsAndCallBacks {
        if (!_keyPathsAndCallBacks) {
            _keyPathsAndCallBacks = [NSMutableDictionary dictionary];
        }
        return _keyPathsAndCallBacks;
    }
    
    @end
    
    

    再回忆下对象的释放过程

    /*对象在释放时,最终都会走到这个函数*/
    void *objc_destructInstance(id obj) 
    {
        if (obj) {
            // Read all of the flags at once for performance.
            bool cxx = obj->hasCxxDtor();
            bool assoc = obj->hasAssociatedObjects();
    
            // This order is important.
            if (cxx) object_cxxDestruct(obj);/*如果有成员变量,则先释放成员变量*/
            if (assoc) _object_remove_assocations(obj);/*如果有关联对象,则释放关联对象*/
            obj->clearDeallocating();/*清除SideTable中weak引用表,并把指向该对象的指针置为nil*/
        }
    
        return obj;
    }
    

    GitHub地址:IQDataBinding,一个View和ViewModel双向绑定的框架

    除此之外,再推荐一个比较好用的框架:KVOController Simple, modern, thread-safe key-value observing for iOS and OS X.

    对于开发者的建议

    • 不管是新、老团队,还是新、老项目,我都强烈建议大家尝试MVVM架构,再次强调的是:MVVM ≠ RAC
    • 对于团队成员众多,项目遗留问题多的团队来说,我建议大家尝试MVVM+KVO+数据存取放到Model层的架构方案。
    • 不可否认的是,RAC是个特别优秀的框架,但是落地比较难,尤其在中国。
    • 不管哪种架构方式,执行落地都不是一件容易的事情。对于MVVM,我建议采用分步走的策略,即新功能用MVVM开发,老旧代码分步重构。而且要引入一些手段对代码进行静态检查,然后一步步把MVVM落到实处。再推荐点干货使用 OCLint 自定义 MVVM 规则

    参考:https://www.jianshu.com/p/0ae3c0d830e5

  • 相关阅读:
    招隐-古琴曲-山中鸣琴,万籁声沉沉,何泠泠!
    因循苟且逸豫而无为,可以侥幸一时,而不可以旷日持久。——王安石
    模糊理论在图像处理中的应用
    铁关-中国首都警官合唱团-歌词
    听着总感觉莫名熟悉的音乐汇总
    石鼓歌-韩愈
    唐长安城
    唐长安的信仰——读书笔记
    Eclipse安装java web插件
    Java调用MySql数据库函数
  • 原文地址:https://www.cnblogs.com/XZY30/p/14455082.html
Copyright © 2011-2022 走看看