寫界面可以說是每位移動(dòng)應(yīng)用開發(fā)者的基本功,也是一位合格移動(dòng)應(yīng)用開發(fā)者繞不過去的坎。但就如不是每一位開發(fā)者都能夠成為合格的開發(fā)者一樣,本人在不同的團(tuán)隊(duì)中發(fā)現(xiàn),甚少有人能夠編寫出合格的UI代碼;而非常奇怪的是,在很多的開發(fā)者論壇上看到我們移動(dòng)開發(fā)者更多關(guān)注于某個(gè)控件或者是動(dòng)畫,但卻很少看到深入剖析UI機(jī)制,指導(dǎo)UI開發(fā)的文章。
由于界面涉及到的方面實(shí)在過于廣泛,本文不可能事無巨細(xì),一一道來,所以本文先立足于
點(diǎn),深入剖析iOS
UI系統(tǒng)中不被重視卻非常重要的機(jī)制,幫助本文讀者對(duì)iOS的UI系統(tǒng)有整體了解;進(jìn)而以點(diǎn)帶面,拓展到UI邏輯設(shè)計(jì)和架構(gòu)設(shè)計(jì)模式的討論;最后讀文而有
所思有所得,設(shè)計(jì)開發(fā)出高效、易用、流暢的UI模塊。
本文章節(jié)如下:
基礎(chǔ)與本質(zhì):說明普遍意義上的UI系統(tǒng)的三大模塊,讓讀者從整體上對(duì)UI系統(tǒng)有清楚的認(rèn)識(shí)。
View:深入View的內(nèi)部機(jī)制,View與Layer之間的關(guān)系,以及Offscreen Render;
ViewController:講解ViewController在UI系統(tǒng)中所扮演的角色,以及UI架構(gòu)設(shè)計(jì)中ViewController運(yùn)用和實(shí)踐;
MVC、MVP、MVVM:簡(jiǎn)單分析三種主流的架構(gòu)設(shè)計(jì)模式及其異同,并簡(jiǎn)單提出了一些做架構(gòu)設(shè)計(jì)意見和想法;
總結(jié)。
各章節(jié)間沒有必然的聯(lián)系,讀者可以選擇感興趣章節(jié)閱讀。
1. 基礎(chǔ)與本質(zhì)
終端App開發(fā)區(qū)別于后端開發(fā)最大的不同,就是終端開發(fā)很大部分的邏輯是為用戶提供界面以供人機(jī)交互,即所謂的UI(User Interface)。所以所有的UI架構(gòu)主要關(guān)注三大模塊:界面布局管理,渲染及動(dòng)畫、事件響應(yīng);
1.1 布局管理
即在規(guī)定的坐標(biāo)系統(tǒng)上,按照一定的層級(jí)順序位置大小排布在容器內(nèi)。一個(gè)UI系統(tǒng)必然有個(gè)基于坐標(biāo)的布局管理系統(tǒng),不管是Windows、Sysbian,還是Andorid、iOS。好的布局管理機(jī)制直接影響界面邏輯實(shí)現(xiàn)的難易程度;
我
們現(xiàn)在日常接觸到的App的UI坐標(biāo)系統(tǒng)都是二維的,我們現(xiàn)在玩的3D游戲,受限于二維的展示屏幕,所以實(shí)質(zhì)上只是三維在二維上的映射投影。我們一直在往
更高的維度發(fā)展:全息影像、Hololens等等。在此可以設(shè)想下,未來我們構(gòu)建界面的布局管理很可能就是基于真實(shí)三維坐標(biāo)。
1.2 動(dòng)畫及渲染
UI之所以叫User Interface,就是因?yàn)閁I通過視覺上的展示,為用戶提供信息。這些信息的展示需要通過一系列復(fù)雜的計(jì)算,最后操作液晶體展示在顯示屏上,這一系列過程就是渲染和動(dòng)畫;
下圖就是應(yīng)用界面渲染到展示的流程:
引自WWDC2014 #419 Advanced Graphics and Animations for iOS Apps
這里不展開來講,推薦沒看過的同學(xué)都認(rèn)真觀看,能夠很好的理解渲染流程和界面優(yōu)化;
推薦資料:
WWDC2014 #419 Advanced Graphics and Animations for iOS Apps
Objc.io的文章:#Objc Issue 3:Views - Getting Pixels onto the Screen
1.3 事件響應(yīng)
UI
除了展示信息之外,還需要接收并響應(yīng)用戶的點(diǎn)擊、手勢(shì)、搖晃等事件,經(jīng)過一系列操作后更新展示信息,展示給用戶;正確及時(shí)地響應(yīng)用戶的操作并給予反饋,是
良好用戶體驗(yàn)的保證。為何Android設(shè)備普遍給人的感覺比iOS設(shè)備要卡,其中一個(gè)主要的原因是iOS系統(tǒng)將響應(yīng)用戶事件放在主線程的最高優(yōu)先級(jí)。
1.4 UI系統(tǒng)架構(gòu)
從整體理解了上述三個(gè)方面,你會(huì)對(duì)UI架構(gòu)有系統(tǒng)認(rèn)識(shí)。iOS中的UI系統(tǒng)架構(gòu)如下:
引自WWDC2014 #419 Advanced Graphics and Animations for iOS Apps
2. ?View
UIView
是UIKit中最基本控件,就如同NSObject基本上是Cocoa庫內(nèi)所有類的基類一樣,UIView也是UIKit中所有界面控件的基類。只要你愿
意,你甚至只用UIView就可以搭建你的App(不過iOS9做了約束,必須設(shè)置keyWindow的rootViewControler)。
一
般來說,熟練掌握常用的UIView子類控件(如UIButton, UIImageView,
UILabel等)就足以應(yīng)付90%的界面編碼需要。但想要編寫出高效、優(yōu)美的界面代碼,還需要更深入的了解。既然要深入,本文假設(shè)你對(duì)UIView已經(jīng)
有了初步的了解,至少使用寫過幾個(gè)完整的頁面;基于此設(shè)定下,本文討論聚焦于以下幾點(diǎn):
1) UIView 與 CALayer:討論UIView背后的CALayer,了解CALayer與UIView的關(guān)系及渲染流程;
2) Offscreen Render:闡述什么是Offscreen Render(離屏渲染),以及一些避免離屏渲染的方法;
3) UIResponser:討論UIView和UIViewController的父類UIResponser,分析iOS設(shè)備上的事件響應(yīng)鏈;
4) 設(shè)計(jì)與實(shí)踐:結(jié)合本人開發(fā)實(shí)踐經(jīng)驗(yàn),說明在UIView應(yīng)用中好的設(shè)計(jì)實(shí)踐規(guī)則;
參考:View Programming Guide for iOS
2.1 UIView 與 CALayer
我們應(yīng)該都知道每個(gè)UIView都包含了一個(gè)CALayer,就算你沒直接看過CALayer,應(yīng)該也使用過。比如給一個(gè)View切個(gè)圓角:view.layer.cornerRadius = 5.0f;;加個(gè)邊框:view.layer.borderWidth = 1.0f; view.layer.borderColor = [UIColor darkGrayColor].CGColor;,這里使用的layer就是CALayer。
CALayer
是QuartzCore庫內(nèi)的類,是iOS上最基本的繪制單元;而UIView只是CALyer之上的封裝,更準(zhǔn)確的來說,UIView是CALyer的
簡(jiǎn)版封裝,加上事件處理的集合類。事件處理我們下一節(jié)再討論,這里的簡(jiǎn)版封裝如何理解,為什么不直接使用CALayer?
首先,如上一段所述,CALayer是最基本的繪制單元,每一個(gè)UIView都有一個(gè)CALayer的變量(public var layer: CALayer { get }),UIView的渲染實(shí)質(zhì)就是這個(gè)layer的渲染。我們可以看看的類定義,里面有很多屬性(變量)及方法在中可以找到幾乎一模一樣的對(duì)應(yīng);如屬性變量frame、hidden,方法public func convertPoint(p: CGPoint, fromLayer l: CALayer?) -> CGPoint等;但也有更多的屬性方法是UIView所沒有的,這里就一一列舉了。我們可以看到UIView其實(shí)是把常用的接口(屬性和方法)暴露出來了,讓UIView更為易用。
其次,我們知道iOS平臺(tái)的Cocoa Touch是源于OS X平臺(tái)的Cocoa),是在Cocoa的基礎(chǔ)上添加了適用于移動(dòng)手機(jī)設(shè)備的手勢(shì)識(shí)別、動(dòng)畫等特性;但從底層實(shí)現(xiàn)上來說,Cocoa Touch與Cocoa共用一套底層的庫,其中就包括了QuartCore.framework;但QuartCore.framework一開始就是為OS X設(shè)計(jì)的,所以其中有部分特性是不適合做移動(dòng)設(shè)備開發(fā)的,比如最重要的坐標(biāo)系統(tǒng)。因此,我們也就不難理解為何UIView/NSView在CALayer上做了一層封裝。
以上,是UIView于CALayer的主要的關(guān)系。
2.2 Offscreen Render
當(dāng)
你尚在懵懂未知的開發(fā)初期,在寫UIScrollView及其子類(UITableView、UICollectionView)時(shí),一定會(huì)遇到滾動(dòng)不流
暢,經(jīng)常卡頓的情況;你認(rèn)真研究代碼,發(fā)現(xiàn)你邏輯代碼都放到了異步線程,主線程做的都是渲染界面的活,為什么會(huì)卡頓?然后你想老手尋求幫助,老手會(huì)讓你去
掉圓角、半透明和陰影之類,App又重回絲般順滑;你不知道為什么,問老手,他可能會(huì)很詳細(xì)跟你解釋一通,然后你一知半解地點(diǎn)點(diǎn)頭,腦中一片茫然;較好的
情況,也許你依稀記得這么一個(gè)詞:離屏渲染(Offscreen Render)。那到底什么是Offscreen
Render?為什么Offscreen Render會(huì)導(dǎo)致卡頓?
在第一章的1.2節(jié)中有提到渲染的流程圖,我們?cè)俑钊朦c(diǎn),先看看最基本的渲染通道流程:
引自WWDC2014 #419 Advanced Graphics and Animations for iOS Apps
注:iOS的GPU渲染機(jī)制是Tile-Based的,而Tile-Based?GPU也是現(xiàn)在移動(dòng)設(shè)備的主流;
我們?cè)賮砜纯葱枰狾ffscreen Render的渲染通道流程:
引自WWDC2014 #419 Advanced Graphics and Animations for iOS Apps
一
般情況下,OpenGL會(huì)將應(yīng)用提交到Render
Server的動(dòng)畫直接渲染顯示(基本的Tile-Based渲染流程),但對(duì)于一些復(fù)雜的圖像動(dòng)畫的渲染并不能直接渲染疊加顯示,而是需要根據(jù)
Command
Buffer分通道進(jìn)行渲染之后再組合,這一組合過程中,就有些渲染通道是不會(huì)直接顯示的;對(duì)比基本渲染通道流程和Masking渲染通道流程圖,我們可
以看到到Masking渲染需要更多渲染通道和合并的步驟;而這些沒有直接顯示在屏幕的上的通道(如上圖的 Pass 1 和 Pass
2)就是Offscreen Rendering Pass。
Offscreen Render為什么卡頓,從上圖我們就可以知道,Offscreen Render需要更多的渲染通道,而且不同的渲染通道間切換需要耗費(fèi)一定的時(shí)間,這個(gè)時(shí)間內(nèi)GPU會(huì)閑置,當(dāng)通道達(dá)到一定數(shù)量,對(duì)性能也會(huì)有較大的影響;
那哪些情況會(huì)Offscreen Render呢?
1)?drawRect
2)?layer.shouldRasterize?=?true;
3)?有mask或者是陰影(layer.masksToBounds,?layer.shadow*);
4)Text(UILabel,?CATextLayer,?Core?Text,?etc)
...
注:layer.cornerRadius,layer.borderWidth,layer.borderColor并不會(huì)Offscreen Render,因?yàn)檫@些不需要加入Mask。
還有更多與Offscreen Render以及動(dòng)畫圖形優(yōu)化相關(guān)的知識(shí),請(qǐng)認(rèn)真觀看WWDC。
參考:
WWDC2011 #121 Understanding UIKit Rendering
WWDC2014 #419 Advanced Graphics and Animations for iOS Apps
2.3 設(shè)計(jì)與實(shí)踐
以上幾節(jié),對(duì)View在開發(fā)過程中經(jīng)常遇到,但并不容易深入理解的概念進(jìn)行了討論。接下來,我想脫離View的具體概念,談?wù)劚救嗽赩iew設(shè)計(jì)和開發(fā)中的一些實(shí)踐經(jīng)驗(yàn);
2.3.1 精簡(jiǎn)扁平的View層次結(jié)構(gòu)
復(fù)雜的View層次結(jié)果不僅會(huì)影響渲染效率,而且也會(huì)造成代碼的臃腫,會(huì)造成不可預(yù)料的問題并且難以定位;怎么樣維護(hù)一個(gè)精簡(jiǎn)扁平的View層次結(jié)構(gòu)呢?原則如下:
1) 盡量使用系統(tǒng)原生的控件;
如
實(shí)現(xiàn)一個(gè)icon跟title上下布局的按鈕,很多人習(xí)慣是使用一個(gè)view包含了一個(gè)UIButton和一個(gè)UILabel。實(shí)際上更為推薦的方式是調(diào)
整UIButon的contentInset/titleInset/imageInset三個(gè)參數(shù)來達(dá)到這個(gè)效果,非常簡(jiǎn)單,并且上面title有
UIButton上的展示方式和特性;
又比如一個(gè)有著負(fù)責(zé)一點(diǎn)布局結(jié)構(gòu)的滾動(dòng)界面,有些開發(fā)者會(huì)覺得使用
UITableView/UICollectionView實(shí)現(xiàn)會(huì)比較復(fù)雜,有些效果可能沒辦法達(dá)到,就用他們的基類UIScrollView來實(shí)現(xiàn),自
己造了一大套的輪子,代碼可能也變得非常復(fù)雜;實(shí)際上根據(jù)我的經(jīng)驗(yàn),通過重寫或者是內(nèi)部屬性的調(diào)整是完全可以使用
UITableView/UICollectionView來達(dá)到這個(gè)效果,畢竟UITableView/UICollectionView是
UIScrollView的子類,功能不會(huì)減少,而會(huì)更加強(qiáng)大,并且我們還能利用已有的data
source和delegate機(jī)制,實(shí)現(xiàn)設(shè)計(jì)上的解耦。
其他常見的還有UINavigationBar、UITabBar、UIToolBar等等;
2) 合理添加/刪除動(dòng)態(tài)View;
有
些View是動(dòng)態(tài)的,就是偶爾顯示,偶爾隱藏。這類View有兩種處理方式:增刪,或者顯示/隱藏。沒有標(biāo)準(zhǔn)的答案,個(gè)人更推薦增刪的處理方式,即在有需
要的時(shí)候添加到對(duì)應(yīng)的ContainerView上,在不需要的時(shí)候?qū)⑵鋭h除。這樣即可以與懶加載結(jié)合在一起,而且也能避免兩個(gè)動(dòng)態(tài)View的相互影響,
比如TableFooterView,或者是錯(cuò)誤加載View。但這并不是唯一的方式,假如這個(gè)動(dòng)態(tài)View所在的View層級(jí)比較簡(jiǎn)單,并且需要?jiǎng)赢嬤M(jìn)
行動(dòng)態(tài)展示,則使用顯示/隱藏也是不錯(cuò)的處理方式。
2.3.2 通用控件;
每一個(gè)程序員都可以建
立自己的代碼庫,同理,每一個(gè)移動(dòng)開發(fā)程序員都可以建立自己的通用控件代碼庫。這個(gè)庫內(nèi)的控件,可以是你自己寫的,也可以是優(yōu)秀的第三方開源控件。建立控
件庫,除了能夠避免重新造輪子,大大提高我們的開發(fā)效率,還有更為重要的一點(diǎn):在運(yùn)用、改造、重構(gòu)中掌握接口設(shè)計(jì)解耦,甚至是架構(gòu)的知識(shí)和經(jīng)驗(yàn)。
每
個(gè)App的UI設(shè)計(jì)、交互、布局和配色往往千差萬別,但總脫離不出移動(dòng)App這一范疇,也就決定了在某些通用的控件交互上會(huì)保持一致性,以讓用戶依據(jù)自己
在移動(dòng)應(yīng)用上的使用經(jīng)驗(yàn)就能輕松快速上手使用,這就是App的移動(dòng)性。所以通用控件的適用場(chǎng)景往往是很“通用”的。比如下拉刷新、加載更多、Tab
Bar、提示Tips、加載錯(cuò)誤重新加載等等。在新的App或者功能模塊上運(yùn)用這些控件時(shí),你就會(huì)思考怎么讓控件更加通用,即不影響老的邏輯,又能夠適用
新的需求,這對(duì)于做界面的架構(gòu)設(shè)計(jì)是非常好的鍛煉。
2.3.3 ?合理運(yùn)用VC在替代View組合復(fù)雜界面;
在
界面開發(fā)過程中,我們常常會(huì)遇到復(fù)雜的界面,比如多頁界面、多種布局方式展示多業(yè)務(wù)的首頁等,但由于很大部分開發(fā)者已經(jīng)對(duì)“一屏就是一個(gè)VC”這一初學(xué)者
的習(xí)慣奉為教條,寫出一個(gè)龐然大View,再加上復(fù)雜的邏輯代碼,這一塊的代碼很可能就演變成了誰都不敢動(dòng)的禁區(qū)。一個(gè)VC可以管理多個(gè)VC,所以合理的
使用VC來替代View進(jìn)行復(fù)雜界面組合,不僅能夠?qū)?fù)雜界面切分成更小的粒度,邏輯代碼也同步合理劃分,便于維護(hù)和重構(gòu);而依托VC的機(jī)制,還能
View和數(shù)據(jù)的動(dòng)態(tài)加載管理。
下一章中關(guān)于輕VC的討論是這一節(jié)知識(shí)的拓展。
3. ?ViewController
上一節(jié)關(guān)于View的章節(jié)已討論了iOS界面機(jī)制,這一節(jié)則主要是來談?wù)勗趯懡缑孢^程中的設(shè)計(jì)問題和基本規(guī)范;
ViewController在iOS只是一個(gè)非常重要的概念,它是我們?cè)陂_發(fā)界面時(shí)最常打交道的模塊,其在一個(gè)App中所扮演的角色,View Controller Programming Guide for iOS中有清晰準(zhǔn)確的描述:
1) View Management:管理View;
2) Data Marshalling:管理數(shù)據(jù);
3) User Interactions:響應(yīng)用戶交互;
4) Resource Management:管理資源;
5) Adaptivity:適配不同的屏幕尺寸空間的變化;
可
以看到,ViewController有太多的事情要做,這也就導(dǎo)致了ViewController非常容易變得代碼膨脹、邏輯混亂等問題;依照我個(gè)人的
經(jīng)驗(yàn),一個(gè)ViewController類的有效代碼超過500行,這個(gè)ViewController就會(huì)變得難以維護(hù),但實(shí)際上在開發(fā)過程中,往往會(huì)遇
到上1K行,甚至2~3K行的ViewController類;當(dāng)一個(gè)ViewController類達(dá)到2~3K行,就意味著其他開發(fā)者接手這個(gè)模塊來
修改東西,已經(jīng)無法通過滾動(dòng)來定位代碼,只能通過搜索;
所以,在進(jìn)行界面開發(fā)時(shí),ViewController需要特別注意模塊設(shè)計(jì),將不同的模塊按照邏輯進(jìn)行一定的拆分,即解耦,又防止ViewController模塊的代碼膨脹。這就是輕VC的理念;
3.1 輕VC
輕VC是前兩年非常火的名詞,現(xiàn)在似乎已經(jīng)成為了一種業(yè)界規(guī)范或者是慣例。以我的經(jīng)驗(yàn)來看,一個(gè)VC的類,如果有效代碼超過了500行,則表示這個(gè)類看是變得臃腫而難以維護(hù);到達(dá)800行,基本上沒辦法通過滾動(dòng)來定位代碼,只能通過搜索,此時(shí)重構(gòu)已勢(shì)在必行;
關(guān)于輕VC,objc.io的開篇第一章#Issue 1 : Lighter View Controllers,足見這一理念的重要性。掌握輕VC的理念基本上是一個(gè)iOS開發(fā)者從初級(jí)邁向高級(jí)必備技能。#Issue 1 : Lighter View Controllers文中介紹了構(gòu)建輕VC幾種常見的方式:
1) 將數(shù)據(jù)源等復(fù)雜接口從VC中剝離;
2) 把業(yè)務(wù)邏輯代碼抽象到Model層;
3) 將復(fù)雜View抽象成獨(dú)立的類;
4) 使用VC的Containment的特點(diǎn),將一個(gè)VC中邏輯分離的界面模塊剝離成為多個(gè)子VC;
想要設(shè)計(jì)出合理而易于理解和維護(hù)的輕VC結(jié)構(gòu),需要掌握輕VC的知識(shí)并有一定實(shí)踐經(jīng)驗(yàn)。在以下情況下,可以考慮將一個(gè)VC設(shè)計(jì)或者重構(gòu)成更多模塊更多類的輕VC結(jié)構(gòu):
1) 如上所述,代碼超過500行時(shí);
2) VC內(nèi)的View的數(shù)據(jù)源來自多個(gè)不同的地方;
3) VC內(nèi)有多個(gè)復(fù)雜的View,需要展示數(shù)據(jù)實(shí)體類較為復(fù)雜;
總之,當(dāng)你感覺你的VC已經(jīng)變得臃腫,那么就可嘗試輕VC的實(shí)踐,實(shí)踐才有收獲。
3.2 VC的設(shè)計(jì)
相對(duì)于View關(guān)注于布局和展示,VC更關(guān)注設(shè)計(jì)和管理。本節(jié)以一個(gè)實(shí)例,來簡(jiǎn)單介紹在一個(gè)完整App中的VC設(shè)計(jì)。
先來看一個(gè)常見的UI結(jié)構(gòu)設(shè)計(jì)例子:
這
個(gè)圖應(yīng)該非常容易理解:最底部是一個(gè)側(cè)滑抽屜控件,該抽屜包含了App內(nèi)容展示的TabBarController和設(shè)置的
VC;TabBarController的子Item VC包含了相應(yīng)業(yè)務(wù)的List VC,點(diǎn)擊List
VC進(jìn)入到詳情View內(nèi);有些詳情VC是使用WebViewController來進(jìn)行內(nèi)容的展示。非常簡(jiǎn)單,不是么?接下來說明該設(shè)計(jì)的洞見:
1)Root ViewController,是整個(gè)App內(nèi)Window的根VC,這是一個(gè)生命周期與App相同的VC,即Window的RootViewController是唯一且一直存在的,需要切換場(chǎng)景則使用這個(gè)Root VC控制子VC切換來實(shí)現(xiàn)(常見于場(chǎng)景:需要進(jìn)行強(qiáng)登錄,即登錄之后才能使用的App,登錄成功后從登錄界面切換到主界面,則登錄VC和主界面VC都應(yīng)該是Root VC的子VC,受Root VC的控制來進(jìn)行切換)。這個(gè)RootViewController建議是一個(gè)UINavigationController,以此保證足夠擴(kuò)展性,并提供更為豐富的界面交互選擇。這個(gè)Root VC的生命周期與App一致,這樣一些突發(fā)的靈活分支界面可以很好的展示在Root VC上,如全局的Loading提示、OpenURL的分支調(diào)整等;
2)Main ViewController:主界面,是主要業(yè)務(wù)展示界面的根界面。該VC與RootVC功能上會(huì)很容易重合在一起,但需要注意的是,該VC并非一直存在,但切換到一些特定分支時(shí),該VC會(huì)從Root VC上remove掉,比如前面所說的強(qiáng)登錄App,登錄界面與主界面就會(huì)需要進(jìn)行切換。另外,該VC隔離了主要業(yè)務(wù)展示界面的VC與Root VC,便于App整體界面風(fēng)格的改版和重構(gòu)。比如現(xiàn)在上圖展示的是一個(gè)側(cè)滑抽屜+TabBar的組合,那到下個(gè)版本改版把側(cè)滑抽屜去掉,那么只需要使用TabBar替換到DrawerMenu VC在Main VC中的位置即可,而不會(huì)影響到RootVC中其他分支展示出來的界面(如Push等)。
3)TabBarItem ViewController:作為TabBar Controller的子Item VC,通常會(huì)設(shè)計(jì)為NavigationController,用以管理各TabBarItem內(nèi)的VC棧。注:如果需要在Push進(jìn)入二級(jí)界面(Detail VC)時(shí)隱藏TabBar,只需要設(shè)置二級(jí)VC的hidesBottomBarWhenPushed = true即可,如果想更加靈活的控制TabBar,例如進(jìn)到三級(jí)頁面的時(shí)候顯示出TabBar(這個(gè)場(chǎng)景應(yīng)該很少見),或者你的TabBar是自定義的,可以參考我寫的一個(gè)開源控件MZNavTab;
本節(jié)所示例的UI結(jié)構(gòu)是一個(gè)非常通用的UI結(jié)構(gòu),市面上除游戲外60%以上的App都是類似的UI交互(統(tǒng)計(jì)來源于個(gè)人手機(jī)),假如你的UI交互與此類似而你的UI結(jié)構(gòu)很混亂的話,不如嘗試下這個(gè)UI結(jié)構(gòu)設(shè)計(jì)。
4. MVC、MVP、MVVM
MVC:
MVP:
MVVM:
圖注:
虛線箭頭:表示兩者之間是非強(qiáng)依賴關(guān)系。如MVC圖,View與Model一般沒有直接聯(lián)系。
虛線矩形:表示該模塊在對(duì)應(yīng)架構(gòu)設(shè)計(jì)中的隱性存在。即一般性架構(gòu)中并沒有這個(gè)角色,但立足于iOS這個(gè)平臺(tái),這又是不可或缺的一部分;
本文并不打算將MVC、MVP、MVVM這個(gè)幾個(gè)通用架構(gòu)設(shè)計(jì)模式的概念統(tǒng)統(tǒng)在這里敘述一遍,上面三個(gè)圖基本上能夠很明白地對(duì)比出三者之間的差異。也許與你在網(wǎng)上看到的不盡相同,這是因?yàn)橐陨先龍D更立足于iOS平臺(tái)。
4.1 MVC
我們最初看到的MVC設(shè)計(jì)模式圖可能是這樣的:
引
自[MSDN#ASP.NET - Single-Page Applications: Build Modern, Responsive Web
Apps with
ASP.NET(https://msdn.microsoft.com/en-us/magazine/dn463786.aspx)
而蘋果官方給的MVC的設(shè)計(jì)模式圖卻是這樣的:
到底哪一副圖才是真正的MVC?我的答案只能是:都是。
MVC從施樂帕克實(shí)驗(yàn)室提出至今,已經(jīng)應(yīng)用到各種應(yīng)用開發(fā)領(lǐng)域中:Web App可以用MVC,iOS/Android/Windows客戶端應(yīng)用也用MVC,Web前端也在用MVC,等等;這些幾乎涵蓋了我們常見的開發(fā)領(lǐng)域,所以MVC其實(shí)已經(jīng)超越了他原本最初的設(shè)計(jì),基于所有涉及展示的應(yīng)用都能套上MVC,只不過不同的平臺(tái)在設(shè)計(jì)上略有差別。而MVP和MVVM,也不過是MVC的衍生變種,除這兩者之外,還有我們沒怎么見過的HMVC、MVA等。
4.2 Model Layer
在討論MVP和MVVM之前,我想先明確一個(gè)經(jīng)常被誤解的概念:Model。由于Model這個(gè)詞太通用化,如數(shù)據(jù)Model,數(shù)據(jù)庫Model,這就導(dǎo)致了Model這一概念理解差異化,簡(jiǎn)單的說,就是被玩壞。拋開其他,我們來看看常見的定義:
Wikipedia的定義:
The?central?component?of?MVC,?the?model,?captures?the?behavior?of?the?application?in?terms?of?its?problem?domain,?independent?of?the?user?interface.[11]?The?model?directly?manages?the?data,?logic?and?rules?of?the?application.
MSDN(https://msdn.microsoft.com/en-us/library/ff649643.aspx)中的定義:
Model.?The?model?manages?the?behavior?and?data?of?the?application?domain,?responds?to?requests?for?information?about?its?state?(usually?from?the?view),?and?responds?to?instructions?to?change?state?(usually?from?the?controller).
上面兩個(gè)定義基本一致:Model,管理應(yīng)用的行為和數(shù)據(jù)。
再來看看Apple官方文檔Model-View-Controller的定義:
Model?Objects
Model?objects?encapsulate?the?data?specific?to?an?application?and?define?the?logic?and?computation?that?manipulate?and?process?that?data.?For?example,?a?model?object?might?represent?a?character?in?a?game?or?a?contact?in?an?address?book.?A?model?object?can?have?to-one?and?to-many?relationships?with?other?model?objects,?and?so?sometimes?the?model?layer?of?an?application?effectively?is?one?or?more?object?graphs.?Much?of?the?data?that?is?part?of?the?persistent?state?of?the?application?(whether?that?persistent?state?is?stored?in?files?or?databases)?should?reside?in?the?model?objects?after?the?data?is?loaded?into?the?application.?Because?model?objects?represent?knowledge?and?expertise?related?to?a?specific?problem?domain,?they?can?be?reused?in?similar?problem?domains.?Ideally,?a?model?object?should?have?no?explicit?connection?to?the?view?objects?that?present?its?data?and?allow?users?to?edit?that?data—it?should?not?be?concerned?with?user-interface?and?presentation?issues.
Communication:?User?actions?in?the?view?layer?that?create?or?modify?data?are?communicated?through?a?controller?object?and?result?in?the?creation?or?updating?of?a?model?object.?When?a?model?object?changes?(for?example,?new?data?is?received?over?a?network?connection),?it?notifies?a?controller?object,?which?updates?the?appropriate?view?objects.
雖然Apple的官方文檔是定義ModelObjects,但它的含義還是封裝數(shù)據(jù)以及管理數(shù)據(jù)相關(guān)的邏輯計(jì)算;
所以這里需要明確的一個(gè)概念是:在MVC的設(shè)計(jì)模式中,Model是一個(gè)Layer,而不只是一個(gè)數(shù)據(jù)模型(Data Model)類。總體來說,Model Layer 包含了數(shù)據(jù)模型,以及管理這些數(shù)據(jù)相關(guān)的邏輯計(jì)算,如本地?cái)?shù)據(jù)變化、數(shù)據(jù)緩存、從網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)等業(yè)務(wù)邏輯。關(guān)于這個(gè)問題,還可以參考這篇文章:《iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案》。但有一點(diǎn)需要說明:該文章更傾向于從Model Object上思考Model的定義,因?yàn)槔锩娴年P(guān)于Model的示例是從數(shù)據(jù)模型中擴(kuò)展出業(yè)務(wù)接口;而本人則更傾向于從Model Layer來思考Model,即Model并不限于數(shù)據(jù)模型,可以是數(shù)據(jù)管理類(各種Manager)、請(qǐng)求隊(duì)列管理等等。
4.3 MVP VS MVVM
上一節(jié)關(guān)于Model Layer中推薦的文章《iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案》對(duì)MVC和MVVM都做了非常詳細(xì)的討論,是一篇非常不錯(cuò)的文章,推薦各位閱讀,那么本節(jié)就來說說MVP,以及我為什么更傾向于選擇MVP作為App架構(gòu)設(shè)計(jì)中的設(shè)計(jì)框架。
回顧下在本章一開始祭出的MVP以及MVVM兩張圖,兩者之間有什么不同?
MVVM的VM(View Model)到V(View),比MVP的P(Presenter)到V(View),多了數(shù)據(jù)綁定。也就是
MVP:是MVC的變種,其中Model和View的定義與MVC的一致,不同點(diǎn)在于:MVC的Controller是管理一組Model與View之間交互邏輯,是一個(gè)管理者;而Presenter(展示者)則是Model于View之間的連接者,針對(duì)特定模塊的View提供對(duì)應(yīng)的格式化的Model數(shù)據(jù),將View中的行為反饋到Model中。所以MVC中的Controller一般會(huì)管理一個(gè)或多個(gè)Model和一個(gè)或多個(gè)View,而Presenter則是 M-P-V 一對(duì)一,有更細(xì)的粒度和更好的解耦。
從MVP的定義,你會(huì)發(fā)現(xiàn)MVP與MVVM極其相似,Presenter與View
Model扮演的角色基本沒有差別,除了前面所說到綁定機(jī)制。但綁定機(jī)制既有很明顯的強(qiáng)大優(yōu)點(diǎn)——自動(dòng)連接View和Model,也有很明顯的缺點(diǎn)——更
高的耦合度,更復(fù)雜的代碼邏輯;但讓人感嘆命運(yùn)無常的是:MVVM隨著ReativeCocoa而在iOS平臺(tái)炙手可熱,而iOS平臺(tái)上甚少有人提及的
MVP,在Android平臺(tái)卻幾乎成了標(biāo)準(zhǔn)(Android5.0引入了數(shù)據(jù)綁定支持,MVVM會(huì)在Android平臺(tái)有新的發(fā)展)。
我
為什么傾向于MVP?不過是相比于MVVM雙向綁定的便利,我更希望我的App設(shè)計(jì)中有更強(qiáng)的靈活性和擴(kuò)展性。沒有完美的架構(gòu)設(shè)計(jì)模式,只有適用于你的
App業(yè)務(wù)場(chǎng)景和團(tuán)隊(duì)的設(shè)計(jì)模式。比如數(shù)據(jù)邏輯并不復(fù)雜、更注重視覺展示的應(yīng)用,原始的MVC往往是最優(yōu)解。所有的MVC衍生出的變種,無非是為了
Solve The Problem。
4.4 架構(gòu)設(shè)計(jì)模式應(yīng)用
無論MVC、MVP還是
MVVM,都是指導(dǎo)我們進(jìn)行架構(gòu)設(shè)計(jì)的模式,并非可以生搬硬套的;而且在實(shí)際的應(yīng)用中,對(duì)于這些設(shè)計(jì)模式總會(huì)有不同的理解,并且需要根據(jù)項(xiàng)目需求進(jìn)行必要
的調(diào)整;更為重要的是在我們App的架構(gòu)設(shè)計(jì)中,處理好Model-View-Controller之間的關(guān)系只是基礎(chǔ),最主要的挑戰(zhàn)來自于復(fù)雜的業(yè)務(wù)邏
輯和場(chǎng)景,這才是體現(xiàn)一個(gè)架構(gòu)師能力所在。
唐巧前不久寫的一篇文章《被誤解的MVC和被神化的MVVM》對(duì)MVC和MVVM的實(shí)踐的討論應(yīng)該是體現(xiàn)了現(xiàn)在移動(dòng)端主流架構(gòu)思想,其中對(duì)網(wǎng)絡(luò)請(qǐng)求層、ViewModel 層、Service 層、Storage 層等其它類的提取設(shè)計(jì),才決定了一個(gè)App架構(gòu)設(shè)計(jì)的優(yōu)劣。
對(duì)于架構(gòu)設(shè)計(jì),我準(zhǔn)備在下一篇文章,結(jié)合本人在iOS/Android兩端的設(shè)計(jì)經(jīng)驗(yàn),做個(gè)深入的討論,并給出自己的設(shè)計(jì)范例,供各位討論參考。這里先拋出幾個(gè)在架構(gòu)設(shè)計(jì)中最常思考的點(diǎn),作為下一篇文章的引子:
1) 架構(gòu)是為了解耦,越松的耦合就代表越多的份層,但人的思維總是更愿意接受直線思維,怎么解決這個(gè)矛盾?
2) 在一個(gè)App中,統(tǒng)一(一致)的架構(gòu)設(shè)計(jì)能夠讓邏輯代碼更健壯,更有利于團(tuán)隊(duì)成員間的溝通和項(xiàng)目維護(hù),但如何解決其和靈活性之間的矛盾?
3) 架構(gòu)設(shè)計(jì)是否只包含邏輯分層?需要設(shè)計(jì)數(shù)據(jù)流和多線程么?
4) 設(shè)計(jì)模式中的幾大原則;
5 總結(jié)
以
上四個(gè)章節(jié),先從UI整體出發(fā),到剖析UIView幾點(diǎn)重要機(jī)制,接著討論怎么用好VC這個(gè)UI中重要的管理角色,最后則漫談了MVC/MVVM/MVP
幾個(gè)架構(gòu)設(shè)計(jì)模式的異同和實(shí)踐應(yīng)用,想通過以點(diǎn)帶面,讓我們?cè)陉P(guān)注了具體實(shí)現(xiàn)之后,能夠脫離出來,從俯視下我們App開發(fā)更為整體核心的部分。
參考閱讀:
WWDC2011 #121 Understanding UIKit Rendering
WWDC2014 #419 Advanced Graphics and Animations for iOS Apps
iOS Developer Library#View and Window Architecture
iOS Developer Library#UIKit Framework
iOS Developer Library#Getting Started with Graphics & Animation