說說MVVM

寫在前面

關於軟件架構模式(確切的說是一種軟件編碼規範或者軟件開發模式),這幾年罵戰不斷。爭論的焦點主要是在MVC、MVVM、MVP哪一種架構最好,哪一種架構纔是最牛逼的、擴展性更強的、可維護性更高的。筆者不才,在實際項目中不多用過MVP架構,對於MVP的掌握也是隻停留在寫寫Demo階段。本篇文章主要着重介紹下MVVM架構在真實項目當中的應用,以及拋開RAC,咱們如何本身動手寫一個View和ViewModel之間的綁定框架。html

MVVM掃盲

MVVM(Model–View–Viewmodel)是一種軟件架構模式。ios

MVVM有助於將圖形用戶界面的開發與業務邏輯或後端邏輯(數據模型)的開發分離開來,這是經過置標語言或GUI代碼實現的。MVVM的視圖模型是一個值轉換器, 這意味着視圖模型負責從模型中暴露(轉換)數據對象,以便輕鬆管理和呈現對象。在這方面,視圖模型比視圖作得更多,而且處理大部分視圖的顯示邏輯。 視圖模型能夠實現中介者模式,組織對視圖所支持的用例集的後端邏輯的訪問。git

MVVM是馬丁·福勒的PM(Presentation Model)設計模式的變體。 MVVM以相同的方式抽象出視圖的狀態和行爲,但PM以不依賴於特定用戶界面平臺的方式抽象出視圖(建立了視圖模型)。 MVVM和PM都來自MVC模式。github

MVVM由微軟架構師Ken Cooper和Ted Peters開發,經過利用WPF(微軟.NET圖形系統)和Silverlight(WPF的互聯網應用派生品)的特性來簡化用戶界面的事件驅動程序設計。 微軟的WPF和Silverlight架構師之一John Gossman於2005年在他的博客上發表了MVVM。數據庫

MVVM也被稱爲model-view-binder,特別是在不涉及.NET平臺的實現中。ZK(Java寫的一個Web應用框架)和KnockoutJS(一個JavaScript庫)使用model-view-binder。編程

以上內容均來自維基百科。MVVM wikipedia後端

簡單的講,MVVM是MVC的改進版。咱們都知道MVC軟件架構模式是蘋果推薦的開發模式。設計模式

MVC中的M就是單純的從網絡獲取回來的數據模型,V指的咱們的視圖界面,而C就是咱們的ViewController。bash

在其中,ViewController負責View和Model之間調度,View發生交互事件會經過target-action或者delegate方式回調給ViewController,與此同時ViewController還要承擔把Model經過KVO、Notification方式傳來的數據傳輸給View用於展現的責任。 隨着業務愈來愈複雜,視圖交互越複雜,致使Controller愈來愈臃腫,負重前行。髒活累活都它幹了,到頭來還一點不討好。福報修多了的結果就是,不行了就重構你,重構不了就換掉你。 😅服務器

來一張斯坦福老頭經典的MVC架構圖。

因此爲了解決這個問題,MVVM就閃亮登場了。他把View和Contrller都放在了View層(至關於把Controller一部分邏輯抽離了出來),Model層依然是服務端返回的數據模型。而ViewModel充當了一個UI適配器的角色,也就是說View中每一個UI元素都應該在ViewModel找到與之對應的屬性。除此以外,從Controller抽離出來的與UI有關的邏輯都放在了ViewModel中,這樣就減輕了Controller的負擔。

我簡單的畫了下MVVM的架構圖。

從以上的架構圖中,咱們能夠很清晰的梳理出各自的分工。

  • 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客戶端架構設計,其架構圖以下。

猿題庫的架構本質上不是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 規則

文章首發GitHub github.com/Lobster-Kin…

相關文章
相關標籤/搜索