關於軟件架構模式(確切的說是一種軟件編碼規範或者軟件開發模式),這幾年罵戰不斷。爭論的焦點主要是在MVC、MVVM、MVP哪一種架構最好,哪一種架構纔是最牛逼的、擴展性更強的、可維護性更高的。筆者不才,在實際項目中不多用過MVP架構,對於MVP的掌握也是隻停留在寫寫Demo階段。本篇文章主要着重介紹下MVVM架構在真實項目當中的應用,以及拋開RAC,咱們如何本身動手寫一個View和ViewModel之間的綁定框架。html
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、ViewModel以及Model間的清晰的持有關係,因此在三個模塊間的數據流轉有了很好的控制。
這裏給你們推薦一篇博文猿題庫iOS客戶端架構設計,其架構圖以下。
猿題庫的架構本質上不是MVC也不是MVVM,它是兩種架構演進的一種架構模式。博文中對於MVC和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開發能夠是兩我的同時進行)。
ReativeCocoa相信你們並不陌生,這個函數響應式框架在Github中已經有將近2w star 。RAC是個很是優秀的框架,它能夠獨立於MVVM而存在。 若是隻是把它理解成MVVM中View和ViewModel Binder角色的話,那就有點大材小用了。 本文不會對RAC進行展開分析,感興趣的能夠自行實踐一下。
RAC特色:
總結:RAC是一種編程思惟的改變,因此其缺點很明顯,學習成本很大!!!
具體RAC的使用,能夠參考官方文檔,自行實踐一下,這裏再也不展開。
經過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(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屬性時,如何讓一個函數支持傳輸不一樣的參數類型?
筆者借鑑了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.
文章首發GitHub github.com/Lobster-Kin…