在任何應(yīng)用當(dāng)中將界面做好都不是一件容易的事情。在一個(gè)小小的四邊形中呈現(xiàn)內(nèi)容以及互動(dòng)的結(jié)合看似容易,其實(shí)就算是在很小的應(yīng)用當(dāng)中也很容易寫(xiě)出混亂不堪的視圖代碼。在有很多工程師合作的復(fù)雜項(xiàng)目當(dāng)中,比如 Facebook 的新鮮事頁(yè)面,這些視圖的開(kāi)發(fā)和維護(hù)是有相當(dāng)難度的。
最近我一直在開(kāi)發(fā)一個(gè)叫做 Components 的庫(kù)來(lái)簡(jiǎn)化 iOS View 的開(kāi)發(fā)。它強(qiáng)調(diào)單項(xiàng)數(shù)據(jù)流動(dòng):從不可變的模型到不可變的”組件”,這些組件描述了視圖應(yīng)該如何被設(shè)置。這個(gè)庫(kù)從現(xiàn)在網(wǎng)絡(luò)開(kāi)發(fā)中很流行的 React Javascript 庫(kù) 中汲取了很多靈感。React 通過(guò)一個(gè)叫做 “虛擬 DOM” 的概念來(lái)抽象化對(duì) DOM 處理。同樣地,Components 會(huì)抽象化對(duì) UIView
層次的處理。
在這篇文章中我會(huì)著重說(shuō)明使用 Components 在 iOS 上來(lái)呈現(xiàn)視圖的一些好處,并且分享一些我學(xué)習(xí)到的經(jīng)驗(yàn)。相信在大家自己的應(yīng)用中也能夠用得上。
假設(shè)我們有四個(gè)子視圖而且我們想將它們垂直的分布,水平方向上使用全寬。經(jīng)典的辦法是去實(shí)現(xiàn) -layoutSubviews
和 -sizeThatFits:
這兩個(gè)函數(shù),這樣做需要 52 行代碼. 因?yàn)槠渲杏泻芏鄶?shù)學(xué)運(yùn)算,第一眼看上去不是很容易看出來(lái)是在豎直地?cái)[放視圖。在這兩個(gè)函數(shù)中有點(diǎn)重復(fù)的地方,所以在未來(lái)的修改中保持同步并不容易。
如果我們使用蘋(píng)果的自動(dòng)布局 API,我們可以獲得小小的改進(jìn):34行代碼.。同時(shí)數(shù)學(xué)運(yùn)算以及重復(fù)的代碼問(wèn)題亦可以解決!但是我們卻換來(lái)了另外一些問(wèn)題:自動(dòng)布局設(shè)置起來(lái)很困難,[^1] 調(diào)試起來(lái)也很費(fèi)力,[^2] 并且復(fù)雜的層次會(huì)讓運(yùn)行時(shí)性能打一些折扣。[^3]
^1 Interface Builder 簡(jiǎn)化了自動(dòng)布局,但是因?yàn)?XIBs 文件難以融合,你很難在大的團(tuán)隊(duì)里面使用它們。
^2 有很多關(guān)于如何調(diào)試自動(dòng)布局的文章和博客
^3 我們用自動(dòng)布局制作了一個(gè)很簡(jiǎn)單的新鮮事頁(yè)面,做到 60 幀每秒是非常的困難。
Components 從 CSS Flexbox specification 的布局系統(tǒng)中中吸取了靈感。我不會(huì)介紹太多的細(xì)節(jié),如想進(jìn)一步學(xué)習(xí)請(qǐng)參照 Mozilla 的高質(zhì)量教程 。因?yàn)?Flexbox 大幅簡(jiǎn)化了布局,相對(duì)應(yīng)的 Components 僅僅需要18行代碼。你也不需要使用任何數(shù)學(xué)運(yùn)算以及基于字符串的視覺(jué)格式語(yǔ)言。
用下面的代碼就可以依靠 Components 來(lái)做到同樣的垂直擺放視圖,對(duì)于不熟悉的人們來(lái)說(shuō),句型看上去可能會(huì)很奇怪 -- 稍后再來(lái)解釋?zhuān)?/p>
@implementation FBStoryComponent
+ (instancetype)newWithStory:(FBStory *)story
{
return [super newWithComponent:
[FBStackLayoutComponent
newWithView:{}
size:{}
style:{.alignItems = FBStackLayoutAlignItemsStretch}
children:{
{[FBHeaderComponent newWithStory:story]},
{[FBMessageComponent newWithStory:story]},
{[FBAttachmentComponent newWithStory:story]},
{[FBLikeBarComponent newWithStory:story]},
}]];
}
@end
沒(méi)錯(cuò),我們用的是 Objective-C++。聚合實(shí)例化給我們一個(gè)簡(jiǎn)明并且類(lèi)安全的方法來(lái)指明樣式結(jié)構(gòu)。以下是另外幾個(gè)有效的 style:
值:
style:{} // default values
style:{.justifyContent = FBStackLayoutJustifyContentCenter}
style:{
.direction = FBStackLayoutDirectionHorizontal,
.spacing = 10,
}
使用像 std:vector
和 std:unordered_map
這樣的標(biāo)準(zhǔn)庫(kù)中的容器比我們?cè)?Objective-C 中使用相對(duì)應(yīng)容器有更強(qiáng)的類(lèi)型安全性。我們同時(shí)能夠用棧來(lái)調(diào)用臨時(shí)視圖數(shù)據(jù)結(jié)構(gòu),提升性能。
Components 在句型風(fēng)格上還有另外一些有些奇怪的地方 (為了簡(jiǎn)介而使用 +newWith...
代替 -initWith...
,以及非常規(guī)的縮進(jìn)等),這要在更多的上下文中才解釋得通 --- 這個(gè)話(huà)題單獨(dú)可以再寫(xiě)一篇文章?,F(xiàn)在我們回到主題。
就算是全新的句型,也不難看懂我們擺放視圖的 Components 版本。用一個(gè)重要的原因是:它是聲明式的而不是命令式的。
大多數(shù)的 iOS 視圖代碼讀起來(lái)感覺(jué)像是一系列的指令:
_headerView
實(shí)例變量。而 Components 的代碼是聲明式的:
將這兩者的區(qū)別想象成給工人們列出所有材料和指示的清單,和僅僅給他們一張藍(lán)圖的區(qū)別。延伸一下這個(gè)比喻,一個(gè)建筑師不應(yīng)該在工地上四處奔走來(lái)告訴建筑工人如何去干他們的活 -- 這樣的話(huà)會(huì)太過(guò)于混亂。宣言性的技巧著重于什么需要被完成,而不是如何去完成它;結(jié)果是,你得以將精力集中在要解決的問(wèn)題上而不是實(shí)現(xiàn)細(xì)節(jié)上。
使用 Components 的時(shí)候,不用去操心本地變量和屬性。你不需要在創(chuàng)建視圖的地方,添加限制的地方和使用模型來(lái)配置視圖的地方來(lái)回跳躍。所有的事情就在你面前好好的放著。
我的建議是:永遠(yuǎn)傾向于聲明式風(fēng)格而不是命令式風(fēng)格,這樣一來(lái)代碼更易于讀懂,也更易于維護(hù)。
小小測(cè)驗(yàn):以下代碼是干什么的?
- (void)loadView {
self.view = [self newFeedView];
}
- (UIView *)newFeedView {
return [[FBFeedView alloc] init];
}
如果使用了繼承,那它可以是在做任何事情??赡?-newFeedView
在子類(lèi)中被重寫(xiě)了,返回了一個(gè)完全不同的視圖。又或許 -loadView
被重寫(xiě)去調(diào)用了一個(gè)不同的函數(shù)。在大規(guī)模的代碼庫(kù)中大量使用子類(lèi)會(huì)使得閱讀代碼和理解它們實(shí)際做了什么變得困難。[^4] 繼承產(chǎn)生的問(wèn)題在我們使用 Components 改寫(xiě)新鮮事頁(yè)面之前經(jīng)常發(fā)生,比如 FBHorizontalScrollerView
有很多子類(lèi)重寫(xiě)了不同的方法,這使得超類(lèi)難以閱讀和重構(gòu)。
^4 objc.io 在以前介紹過(guò)這個(gè)主題,這篇維基百科文章 也做了很好地介紹。
Components 永遠(yuǎn)都是被混合的,從來(lái)不會(huì)被繼承。將它們想象成小的基礎(chǔ)模塊,你可以將它們拼裝在一起組成非常棒的東西。
但是對(duì)混合的大量使用會(huì)造成非常深的層次,而深的 UIView 層次會(huì)將滑動(dòng)變得非常緩慢。有一點(diǎn)需要特別指明的是,其實(shí)是存在那種完全不需要為其創(chuàng)建視圖的 component 的。[^5] 在實(shí)踐中,大多數(shù)的組件是不需要視圖的。 就拿 FBStackLayoutComponent
來(lái)作例子;它將它的子視圖碼放在一起,但是它并不需要在層級(jí)中的一個(gè)視圖去執(zhí)行這項(xiàng)任務(wù)。
^5 相同地,在 React 中,也并非每一個(gè)組件都會(huì)創(chuàng)造一個(gè)相應(yīng)的 DOM 元素。
盡管新鮮事頁(yè)面的組件層次有好幾十層,但是得到的視圖層其實(shí)才有三層。我們獲取了所有混合帶來(lái)的好處卻沒(méi)有付出什么代價(jià)。
如果說(shuō)我從龐大的代碼庫(kù)中學(xué)到一樣?xùn)|西的話(huà),就是不要使用繼承!轉(zhuǎn)而使用混合或者其他的模式。
使用 UITableView
時(shí)的重要一步是 cell 的回收:少量的 UITableViewCell
實(shí)例會(huì)被反復(fù)地利用。這是實(shí)現(xiàn)驚人的滑動(dòng)速度得以實(shí)現(xiàn)的重要原因。
但是,要想在多工程師分享的代碼庫(kù)中妥當(dāng)?shù)鼗厥諒?fù)雜的 cells 并不容易。在開(kāi)始使用 Components 之前,我們?cè)砑右粋€(gè)功能來(lái)逐漸淡出一個(gè)故事的一部分界面,但是我們忘記了在回收時(shí)重設(shè) alpha
的值,這樣一來(lái)其他的故事也被隨機(jī)的淡化了!另一個(gè)例子,忘記妥善地重設(shè) hidden
屬性導(dǎo)致隨機(jī)地丟失或者覆蓋某些內(nèi)容。
如果使用 Components,你永遠(yuǎn)不需要擔(dān)心回收。庫(kù)會(huì)來(lái)很好地管理它。不同于寫(xiě)祈使性的代碼來(lái)正確地設(shè)置可能在任何狀態(tài)中所回收的視圖,你只需要指明一個(gè)視圖狀態(tài)即可。庫(kù)會(huì)計(jì)算出完成這項(xiàng)任務(wù)所需的最少步驟。
因?yàn)樗袑?duì)視圖的處理全都由 Components 的代碼完成,我們得以通過(guò)優(yōu)化一個(gè)算法來(lái)提升各個(gè)地方的速度。相較于修改 400 個(gè) UIView
子類(lèi)并心中默念:“這可是一個(gè)龐大的項(xiàng)目”來(lái)說(shuō),優(yōu)化一個(gè)地方并且處處受益要來(lái)的有意義的多。
比如說(shuō),我們加入了一個(gè)優(yōu)化來(lái)確保在重新設(shè)置視圖的時(shí)候,除非值確實(shí)被改變了,否則不去使用屬性的 setter (比如 -setText
)。盡管大多數(shù)的 setter 在值沒(méi)有變化的情況下還是非常有效率的,但我們還是在性能上得到了提升。另外一個(gè)優(yōu)化確保了只有在必要的情況下才重新排序視圖 (通過(guò)使用 -exchangeSubviewAtIndex:withSubviewAtIndex:
),因?yàn)檫@項(xiàng)操作相對(duì)來(lái)說(shuō)成本很高。
最好的部分是,這些優(yōu)化并不需要任何人去改變寫(xiě)代碼的方式。開(kāi)發(fā)者們能夠?qū)W⑼瓿扇蝿?wù)而不是了解高成本的操作并學(xué)會(huì)去避免他們 - 這是一個(gè)對(duì)整個(gè)團(tuán)隊(duì)來(lái)說(shuō)非常大的幫助。
沒(méi)有一個(gè)框架能夠解決所有的問(wèn)題,響應(yīng)式 (reactive) 的界面框架中一個(gè)有挑戰(zhàn)性的問(wèn)題是實(shí)現(xiàn)動(dòng)畫(huà)相較于使用傳統(tǒng)視圖框架要更困難一些。
響應(yīng)式的界面開(kāi)發(fā)鼓勵(lì)將狀態(tài)之間的切換明確化。舉個(gè)例子,有一個(gè)界面會(huì)刪減一部分文本內(nèi)容,但是允許用戶(hù)按一個(gè)按鈕來(lái)展開(kāi)并且查看全部的文本。這個(gè)可以輕易通過(guò)兩個(gè)狀態(tài)來(lái)做到:{Collapsed, Expanded}
。
但是如果你想把展開(kāi)文本做成動(dòng)畫(huà),或者讓用戶(hù)自己通過(guò)拖拽去精確地控制顯示多少文本內(nèi)容,那么就不可能只是用兩個(gè)狀態(tài)了。有數(shù)以百計(jì)的狀態(tài)對(duì)應(yīng)著動(dòng)畫(huà)中某個(gè)時(shí)刻有多少文本會(huì)被顯示出來(lái)。響應(yīng)式框架要求你在開(kāi)始的時(shí)候就把狀態(tài)變化安排好,正是因?yàn)檫@一點(diǎn)動(dòng)畫(huà)才變得如此困難。
我們開(kāi)發(fā)了兩種手段來(lái)管理 Components 中的動(dòng)畫(huà):
animationsFromPreviousComponent:
的 API 來(lái)宣言性地表達(dá)靜態(tài)的動(dòng)畫(huà)。比如說(shuō),一個(gè)組件可以指明在第一次出現(xiàn)的時(shí)候使用漸入的效果。UIKit
的威力。我們的設(shè)想是開(kāi)發(fā)強(qiáng)大的工具去用宣言性的代碼來(lái)寫(xiě)出簡(jiǎn)約的動(dòng)態(tài)動(dòng)畫(huà),我們只是還沒(méi)有完成這個(gè)計(jì)劃而已。
在 Facebook,我們最近宣布了 React Native,一個(gè)運(yùn)用 React Javascript 庫(kù)來(lái)處理本地應(yīng)用中 UIView
層次的框架,與網(wǎng)頁(yè)版不同,這個(gè)庫(kù)使管理的是 UIView
而非網(wǎng)頁(yè)中的 DOM
元素。要告訴大家的是,Components 庫(kù)并不是 React Native,而是一個(gè)單獨(dú)的項(xiàng)目,雖然這可能讓人有些驚訝。
它們的區(qū)別是什么?其實(shí)很簡(jiǎn)單:當(dāng)我們用 Components 重建新鮮事頁(yè)面的時(shí)候 React Native 還沒(méi)有被發(fā)明出來(lái)。在 Facebook, 每一個(gè)人都非常看好 React Native 的前景。并且已經(jīng)應(yīng)用在Mobile Ads Manager 和 Groups兩個(gè)應(yīng)用中使用了。
和所有框架一樣,也存在取舍;比如說(shuō),Components 選擇使用 Objective-C++ 是因?yàn)樗念?lèi)型安全性和性能,但是 React Native 對(duì) Javascript 的運(yùn)用讓在開(kāi)發(fā)環(huán)境下即時(shí)更新成為可能。這些項(xiàng)目經(jīng)常會(huì)分享一些推動(dòng)兩者共同進(jìn)步的創(chuàng)意。
那么用來(lái)驅(qū)動(dòng) Facebook Paper 應(yīng)用的 UI 框架 AsyncDisplayKit 呢?它增添了在后臺(tái)線程計(jì)算和渲染的能力,讓你無(wú)需面對(duì)使用 UIKit 主線程會(huì)遇到的麻煩。
從設(shè)計(jì)哲學(xué)的角度上來(lái)說(shuō),AsyncDisplayKit 和 UIKit 的關(guān)聯(lián)比和 React 要更強(qiáng)。不像 React,AsyncDisplayKit 沒(méi)有強(qiáng)調(diào)使用宣言性句法,混合以及不可變性。
像 AsyncDisplayKit 一樣,Components 在后臺(tái)線程進(jìn)行組件創(chuàng)造和分布 (這個(gè)很容易,因?yàn)槲覀兊哪P蛯?duì)象和組件本身全都是不可變的 - 不可能出現(xiàn)競(jìng)態(tài)條件!)
AsyncDisplayKit 能夠進(jìn)行復(fù)雜的手勢(shì)驅(qū)動(dòng)的動(dòng)畫(huà),這一點(diǎn)正是 Components 的弱項(xiàng)所在。這樣一來(lái)做選擇就很容易了:如果你在設(shè)計(jì)一個(gè)復(fù)雜的手勢(shì)驅(qū)動(dòng)的界面,AsyncDisplayKit 應(yīng)該是正確的選擇。如果你的界面看起來(lái)和 Facebook 的新鮮事頁(yè)面更類(lèi)似,那么 Components 是恰當(dāng)?shù)倪x擇。
Components 庫(kù)在所有的顯示大量信息的頁(yè)面都會(huì)用到 (新鮮事,時(shí)間軸,群,事件,頁(yè)面和搜索等等) 并且正在快速地在 Facebook 應(yīng)用的其他部分被應(yīng)用起來(lái)。用簡(jiǎn)潔,宣言性,可混合的組件是非常有趣的。
你可能覺(jué)得 Components 中的一些東西聽(tīng)起來(lái)很瘋狂。但是用點(diǎn)時(shí)間消化一下,你可能會(huì)自己挑戰(zhàn)之前的一些假設(shè),但是這些東西我們用著非常好而且對(duì)你們可能也有幫助。如果你想學(xué)習(xí)更多,可以看看這個(gè)演講,它深入地討論了 Components 的一些細(xì)節(jié)。為什么用 React? 的博文和它鏈接的資源都是非常好的參考。
我們非常想和社區(qū)分享 Components 背后的代碼,而且我們馬上要著手去做。如果你有想法要分享,隨時(shí)都可以聯(lián)系我 - 尤其是關(guān)于動(dòng)畫(huà)的想法!