iOS 應用架構談:view 層的組織和調用方案(一)

iOS應用架構談 view層的組織和調用方案

iOS應用架構談 網絡層設計方案

iOS應用架構談 動態部署方案

iOS應用架構談 本地持久化方案

前言

《iOS應用架構談 開篇》出來之后,很多人來催我趕緊出第二篇。這一篇文章出得相當艱難,因為公司里的破事兒特別多,我自己又有點私事兒,以至于能用來寫博客的時間不夠充分。

現在好啦,第二篇出來了。

當我們開始設計View層的架構時,往往是這個App還沒有開始開發,或者這個App已經發過幾個版本了,然后此時需要做非常徹底的重構。

一般也就是這兩種時機會去做View層架構,基于這個時機的特殊性,我們在這時候必須清楚認識到:View層的架構一旦實現或定型,在App發版后可修改的余地就已經非常之小了。因為它跟業務關聯最為緊密,所以哪怕稍微動一點點,它所引發的蝴蝶效應都不見得是業務方能夠hold住的。這樣的情況,就要求我們在實現這個架構時,代碼必須得改得勤快,不能偷懶。也必須抱著充分的自我懷疑態度,做決策時要拿捏好尺度。

View層的架構非常之重要,在我看來,這部分架構是這系列文章涉及4個方面最重要的一部分,沒有之一。為什么這么說?

View層架構是影響業務方迭代周期的因素之一

產品經理產生需求的速度會非常快,尤其是公司此時仍處于創業初期,在規模稍大的公司里面,產品經理也喜歡挖大坑來在leader面前刷存在感,比如阿里。這就導致業務工程師任務非常繁重。正常情況下讓產品經理砍需求是不太可能的,因此作為架構師,在架構里有一些可做可不做的事情,最好還是能做就做掉,不要偷懶。這可以幫業務方減負,編寫代碼的時候也能更加關注業務。

我跟一些朋友交流的時候,他們都會或多或少地抱怨自己的團隊迭代速度不夠快,或者說,迭代速度不合理地慢。我認為迭代速度不是想提就能提的,迭代速度的影響因素有很多,一期PRD里的任務量和任務復雜度都會影響迭代周期能達到什么樣的程度。拋開這些外在的不談,從內在可能導致迭代周期達不到合理的速度的原因來看,其中有一個原因很有可能就是View層架構沒有做好,讓業務工程師完成一個不算復雜的需求時,需要處理太多額外的事情。當然,開會多,工程師水平爛也屬于迭代速度提不上去的內部原因,但這個不屬于本文討論范圍。還有, 加班不是優化迭代周期的正確方式 ,嗯。

一般來說,一個不夠好的View層架構,主要原因有以下五種:

代碼混亂不規范
過多繼承導致的復雜依賴關系
模塊化程度不夠高,組件粒度不夠細
橫向依賴
架構設計失去傳承
這五個地方會影響業務工程師實現需求的效率,進而拖慢迭代周期。View架構的其他缺陷也會或多或少地產生影響,但在我看來這里五個是比較重要的影響因素。如果大家覺得還有什么因素比這四個更高的,可以在評論區提出來我補上去。

對于第五點我想做一下強調:架構的設計是一定需要有傳承的,有傳承的架構從整體上看會非常協調。但實際情況有可能是一個人走了,另一個頂上,即便任務交接得再完整,都不可避免不同的人有不同的架構思路,從而導致整個架構的流暢程度受到影響。要解決這個問題,一方面要盡量避免單點問題,讓架構師做架構的時候再帶一個人。另一方面,架構要設計得盡量簡單,平緩接手人的學習曲線。我離開安居客的時候,做過保證: 凡是從我手里出來的代碼,終身保修 。所以不要想著離職了就什么事兒都不管了,這不光是職業素養問題,還有一個是你對你的代碼是否足夠自信的問題。傳承性對于View層架構非常重要,因為它距離業務最近,改動余地最小。

所以當各位CTO、技術總監、TeamLeader們覺得迭代周期不夠快時,你可以先不忙著急吼吼地去招新人,《人月神話》早就說過加人不能完全解決問題。這時候如果你可以回過頭來看一下是不是View層架構不合理,把這個弄好也是優化迭代周期的手段之一。

嗯,至于本系列其他三項的架構方案對于迭代周期的影響程度,我認為都不如View層架構方案對迭代周期的影響高,所以這是我認為View層架構是最重要的其中一個理由。

View層架構是最貼近業務的底層架構

View層架構雖然也算底層,但還沒那么底層,它跟業務的對接面最廣,影響業務層代碼的程度也最深。在所有的底層都牽一發的時候,在View架構上牽一發導致業務層動全身的面積最大。

所以View架構在所有架構中一旦定型,可修改的空間就最小,我們在一開始考慮View相關架構時,不光要實現功能,還要考慮更多規范上的東西。制定規范的目的一方面是防止業務工程師的代碼腐蝕View架構,另一方面也是為了能夠有所傳承。按照規范來,總還是不那么容易出差池的。

還有就是,架構師一開始考慮的東西也會有很多,不可能在第一版就把它們全部實現,對于一個尚未發版的App來說,第一版架構往往是最小完整功能集,那么在第二版第三版的發展過程中,架構的迭代任務就很有可能不只是你一個人的事情了,相信你一個人也不見得能搞定全部。所以你要跟你的合作者們有所約定。另外,第一版出去之后,業務工程師在使用過程中也會產生很多修改意見,哪些意見是合理的,哪些意見是不合理的,也要通過事先約定的規范來進行篩選,最終決定如何采納。

規范也不是一成不變的,什么時候槍斃意見,什么時候改規范,這就要靠各位的技術和經驗了。

以上就是前言。

這篇文章講什么?

View代碼結構的規定

關于view的布局

何時使用storyboard,何時使用nib,何時使用代碼寫View

是否有必要讓業務方統一派生ViewController?

方便View布局的小工具

MVC、MVVM、MVCS、VIPER

本門心法

跨業務時View的處理

留給評論區各種補

總結

View代碼結構的規定

架構師不是寫SDK出來交付業務方使用就沒事兒了的,每家公司一定都有一套代碼規范,架構師的職責也包括定義代碼規范。按照道理來講,定代碼規范應該是屬于通識,放在這里講的原因只是因為我這邊需要為View添加一個規范。

制定代碼規范嚴格來講不屬于View層架構的事情,但它對View層架構未來的影響會比較大,也是屬于架構師在設計View層架構時需要考慮的事情。制定View層規范的重要性在于:

提高業務方View層的可讀性可維護性
防止業務代碼對架構產生腐蝕
確保傳承
保持架構發展的方向不輕易被不合理的意見所左右
在這一節里面我不打算從頭開始定義一套規范,蘋果有一套 Coding Guidelines ,當我們定代碼結構或規范的時候,首先一定要符合這個規范。

然后,相信大家各自公司里面也都有一套自己的規范,具體怎么個規范法其實也是根據各位架構師的經驗而定,我這邊只是建議各位在各自規范的基礎上再加上下面這一點。

viewController的代碼應該差不多是這樣:
注:原文此圖片已經查看不了

要點如下:

所有的屬性都使用getter和setter

不要在viewDidLoad里面初始化你的view然后再add,這樣代碼就很難看。在viewDidload里面只做addSubview的事情,然后在viewWillAppear里面做布局的事情( 勘誤1 ),最后在viewDidAppear里面做Notification的監聽之類的事情。至于屬性的初始化,則交給getter去做。

比如這樣:

<pre>#pragma mark - life cycle

  • (void)viewDidLoad
    {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:self.firstTableView];
    [self.view addSubview:self.secondTableView];
    [self.view addSubview:self.firstFilterLabel];
    [self.view addSubview:self.secondFilterLabel];
    [self.view addSubview:self.cleanButton];
    [self.view addSubview:self.originImageView];
    [self.view addSubview:self.processedImageView];
    [self.view addSubview:self.activityIndicator];
    [self.view addSubview:self.takeImageButton];
    }
  • (void)viewWillAppear:(BOOL)animated
    {
    [super viewWillAppear:animated];
    CGFloat width = (self.view.width - 30) / 2.0f;
    self.originImageView.size = CGSizeMake(width, width);
    [self.originImageView topInContainer:70 shouldResize:NO];
    [self.originImageView leftInContainer:10 shouldResize:NO];
    self.processedImageView.size = CGSizeMake(width, width);
    [self.processedImageView right:10 FromView:self.originImageView];
    [self.processedImageView topEqualToView:self.originImageView];
    CGFloat labelWidth = self.view.width - 100;
    self.firstFilterLabel.size = CGSizeMake(labelWidth, 20);
    [self.firstFilterLabel leftInContainer:10 shouldResize:NO];
    [self.firstFilterLabel top:10 FromView:self.originImageView];
    ... ...
    }</pre>
    這樣即便在屬性非常多的情況下,還是能夠保持代碼整齊,view的初始化都交給getter去做了??傊褪潜M量不要出現以下的情況:

<pre>- (void)viewDidLoad
{
[super viewDidLoad];
self.textLabel = [[UILabel alloc] init];
self.textLabel.textColor = [UIColor blackColor];
self.textLabel ... ...
self.textLabel ... ...
self.textLabel ... ...
[self.view addSubview:self.textLabel];
}</pre>
這種做法就不夠干凈,都扔到getter里面去就好了。關于這個做法,在唐巧的技術博客里面有 一篇文章 和我所提倡的做法不同,這個我會放在后面詳細論述。

getter和setter全部都放在最后

因為一個ViewController很有可能會有非常多的view,就像上面給出的代碼樣例一樣,如果getter和setter寫在前面,就會把主要邏輯扯到后面去,其他人看的時候就要先劃過一長串getter和setter,這樣不太好。然后要求業務工程師寫代碼的時候按照順序來分配代碼塊的位置,先是 life cycle ,然后是 Delegate方法實現 ,然后是 event response ,然后才是 getters and setters 。這樣后來者閱讀代碼時就能省力很多。

每一個delegate都把對應的protocol名字帶上,delegate方法不要到處亂寫,寫到一塊區域里面去

比如UITableViewDelegate的方法集就老老實實寫上 #pragma mark - UITableViewDelegate 。這樣有個好處就是,當其他人閱讀一個他并不熟悉的Delegate實現方法時,他只要按住command然后去點這個protocol名字,Xcode就能夠立刻跳轉到對應這個Delegate的protocol定義的那部分代碼去,就省得他到處找了。

event response專門開一個代碼區域

所有button、gestureRecognizer的響應事件都放在這個區域里面,不要到處亂放。

關于private methods,正常情況下ViewController里面不應該寫

不是delegate方法的,不是event response方法的,不是life cycle方法的,就是private method了。對的,正常情況下ViewController里面一般是不會存在private methods的,這個private methods一般是用于日期換算、圖片裁剪啥的這種小功能。這種小功能要么把它寫成一個category,要么把他做成一個模塊,哪怕這個模塊只有一個函數也行。

ViewController基本上是大部分業務的載體,本身代碼已經相當復雜,所以跟業務關聯不大的東西能不放在ViewController里面就不要放。另外一點,這個private method的功能這時候只是你用得到,但是將來說不定別的地方也會用到,一開始就獨立出來,有利于將來的代碼復用。

為什么要這樣要求?

我見過無數ViewController,代碼布局亂得一塌糊涂,這里一個delegate那里一個getter,然后ViewController的代碼一般都死長死長的,看了就讓人頭疼。

定義好這個規范,就能使得ViewController條理清晰,業務方程序員很能夠區分哪些放在ViewController里面比較合適,哪些不合適。另外,也可以提高代碼的可維護性和可讀性。

關于View的布局

業務工程師在寫View的時候一定逃不掉的就是這個命題。用Frame也好用Autolayout也好,如果沒有精心設計過,布局部分一定慘不忍睹。

直接使用CGRectMake的話可讀性很差,光看那幾個數字,也無法知道view和view之間的位置關系。用Autolayout可讀性稍微好點兒,但生成Constraint的長度實在太長,代碼觀感不太好。

Autolayout這邊可以考慮使用Masonry,代碼的可讀性就能好很多。如果還有使用Frame的,可以考慮一下使用 這個項目 。

這個項目里面提供了Frame相關的方便方法( UIView+LayoutMethods ),里面的方法也基本涵蓋了所有布局的需求,可讀性非常好,使用它之后基本可以和CGRectMake說再見了。因為天貓在最近才切換到支持iOS6,所以之前天貓都是用Frame布局的,在天貓App中,首頁,范兒部分頁面的布局就使用了這些方法。使用這些方便方法能起到事半功倍的效果。

這個項目也提供了Autolayout方案下生產Constraints的方便方法( UIView+AEBHandyAutoLayout ),可讀性比原生好很多。我當時在寫這系列方法的時候還不知道有Masonry。知道有Masonry之后我特地去看了一下,發現Masonry功能果然強大。不過這系列方法雖然沒有Masonry那么強大,但是也夠用了。當時安居客iPad版App全部都是Autolayout來做的View布局,就是使用的這個項目里面的方法??勺x性很好。

讓業務工程師使用良好的工具來做View的布局,能提高他們的工作效率,也能減少bug發生的幾率。架構師不光要關心那些高大上的內容,也要多給業務工程師提供方便易用的小工具,才能發揮架構師的價值。

何時使用storyboard,何時使用nib,何時使用代碼寫View

這個問題唐巧的博客里 這篇文章 也提到過,我的意見和他是基本一致的。

在這里我還想補充一些內容:

具有一定規模的團隊化iOS開發(10人以上)有以下幾個特點:

同一份代碼文件的作者會有很多,不同作者同時修改同一份代碼的情況也不少見。因此,使用Git進行代碼版本管理時出現Conflict的幾率也比較大。
需求變化非常頻繁,產品經理一時一個主意,為了完成需求而針對現有代碼進行微調的情況,以及針對現有代碼的 部分復用 的情況也比較多。
復雜界面元素、復雜動畫場景的開發任務比較多。
如果這三個特點你一看就明白了,下面的解釋就可以不用看了。如果你針對我的傾向愿意進一步討論的,可以先看我下面的解釋,看完再說。

同一份代碼文件的作者會有很多,不同作者同時修改同一份代碼的情況也不少見。因此,使用Git進行代碼版本管理時出現Conflict的幾率也比較大。

iOS開發過程中,會遇到最蛋疼的兩種Conflict一個是 project.pbxproj ,另外一個就是 StoryBoard 或 XIB 。因為這些文件的內容的可讀性非常差,雖然蘋果在XCode5(現在我有點不確定是不是這個版本了)中對StoryBoard的文件描述方式做了一定的優化,但只是把可讀性從 非常差 提升為 很差 。

然而在StoryBoard中往往包含了多個頁面,這些頁面基本上不太可能都由一個人去完成,如果另一個人在做StoryBoard的操作的時候,出于某些目的動了一下不屬于他的那個頁面,比如為了美觀調整了一下位置。然后另外一個人也因為要添加一個頁面,而在Storyboard中調整了一下某個其他頁面的位置。那么針對這個情況我除了說個 呵呵 以外,我就只能說: 祝你好運。 看清楚哦,這還沒動具體的頁頁面內容呢。

但如果使用代碼繪制View,Conflict一樣會發生,但是這種Conflict就好解很多了,你懂的。

需求變化非常頻繁,產品經理一時一個主意,為了完成需求而針對現有代碼進行微調的情況,以及針對現有代碼的 部分復用 的情況也比較多。

我覺得產品經理一時一個主意不是他的錯,他說不定也是被逼的,比如誰都會來摻和一下產品的設計,公司里的所有人,上至CEO,下至基層員工都有可能對產品設計評頭論足,只要他個人有個地方用得不爽(極大可能是個人喜好)然后又正好跟產品經理比較熟悉能夠搭得上話,都會提出各種意見。產品經理躲不起也惹不起,有時也是沒辦法,嗯。

但落實到工程師這邊來,這種情況就很蛋疼。因為這種改變有時候不光是UI,UI所對應的邏輯也有要改的可能,工程師就會兩邊文件都改,你原來link的那個view現在不link了,然后你的outlet對應也要刪掉,這兩部分只要有一個沒做,編譯通過之后跑一下App,一會兒就crash了。看起來這不是什么大事兒,但很影響心情。

另外,如果出現部分的代碼復用,比如說某頁面下某個View也希望放在另外一個頁面里,相關的操作就不是復制粘貼這么簡單了,你還得重新link一遍。也很影響心情。

復雜界面元素,復雜動畫交互場景的開發任務比較多。

要是想在基于StoryBoard的項目中做一個動畫,很煩。做幾個復雜界面元素,也很煩。有的時候我們掛Custom View上去,其實在StoryBoard里面看來就是一個空白View。然后另外一點就是,當你的layout出現問題需要調整的時候,還是挺難找到問題所在的,尤其是在復雜界面元素的情況下。

所以在針對View層這邊的要求時,我也是建議不要用StoryBoard。實現簡單的東西,用Code一樣簡單,實現復雜的東西,Code比StoryBoard更簡單。所以我更加提倡用code去畫view而不是storyboard。

是否有必要讓業務方統一派生ViewController

有的時候我們出于記錄用戶操作行為數據的需要,或者統一配置頁面的目的,會從UIViewController里面派生一個自己的ViewController,來執行一些通用邏輯。比如天貓客戶端要求所有的ViewController都要繼承自TMViewController。這個統一的父類里面針對一個ViewController的所有生命周期都做了一些設置,至于這里都有哪些設置對于本篇文章來說并不重要。在這里我想討論的是,在設計View架構時,如果為了能夠達到統一設置或執行統一邏輯的目的,使用派生的手段是有必要的嗎?

我覺得沒有必要,為什么沒有必要?

使用派生比不使用派生更容易增加業務方的使用成本
不使用派生手段一樣也能達到統一設置的目的
這兩條原因是我認為沒有必要使用派生手段的理由,如果兩條理由你都心領神會,那么下面的就可以不用看了。如果你還有點疑惑,請看下面我來詳細講一下原因。

為什么使用了派生,業務方的使用成本會提升?

其實不光是業務方的使用成本,架構的維護成本也會上升。那么具體的成本都來自于哪里呢?

集成成本

這里講的集成成本是這樣的:如果業務方自己開了一個獨立demo,快速完成了某個獨立流程,現在他想把這個現有流程集合進去。那么問題就來了,他需要把所有獨立的UIViewController改變成TMViewController。那為什么不是一開始就立刻使用TMViewController呢?因為要想引入TMViewController,就要引入整個天貓App所有的業務線,所有的基礎庫,因為這個父類里面涉及很多天貓環境才有的內容,所謂拔出蘿卜帶出泥,你要是想簡單繼承一下就能搞定的事情,搭環境就要搞半天,然后這個小Demo才能跑得起來。

對于業務層存在的所有父類來說,它們是很容易跟項目中的其他代碼糾纏不清的,這使得業務方開發時遇到一個兩難問題: 要么把所有依賴全部搞定,然后基于App環境(比如天貓)下開發Demo , 要么就是自己Demo寫好之后,按照環境要求改代碼 。這里的兩難問題都會帶來成本,都會影響業務方的迭代進度。

我不確定各位所在公司是否會有這樣的情況,但我可以在這里給大家舉一個我在阿里的真實的例子:我最近在開發某濾鏡Demo和相關頁面流程,最終是要合并到天貓這個App里面去的。使用天貓環境進行開發的話,pod install完所有依賴差不多需要10分鐘,然后打開workspace之后,差不多要再等待1分鐘讓xcode做好索引,然后才能正式開始工作。在這里要感謝一下則平,因為他在此基礎上做了很多優化,使得這個1分鐘已經比原來的時間短很多了。但如果天貓環境有更新,你就要再重復一次上面的流程,否則 就很有可能編譯不過。

拜托,我只是想做個Demo而已,不想搞那么復雜。

上手接受成本

新來的業務工程師有的時候不見得都記得每一個ViewController都必須要派生自TMViewController而不是直接的UIViewController。新來的工程師他不能直接按照蘋果原生的做法去做事情,他需要額外學習,比如說:所有的ViewController都必須繼承自TMViewController。

架構的維護難度

盡可能少地使用繼承能提高項目的可維護性,具體內容我在《 跳出面向對象思想(一) 繼承 》里面說了,在這里我想偷懶不想把那篇文章里說過的東西再說一遍。

其實對于業務方來說,主要還是第一個集成成本比較蛋疼,因為這是長痛,每次要做點什么事情都會遇到。第二點倒還好,短痛。第三點跟業務工程師沒啥關系。

那么如果不使用派生,我們應該使用什么手段?

我的建議是使用AOP。

在架構師實現具體的方案之前,必須要想清楚幾個問題,然后才能決定采用哪種方案。是哪幾個問題?

方案的效果,和最終要達到的目的是什么?
在自己的知識體系里面,是否具備實現這個方案的能力?
在業界已有的開源組件里面,是否有可以直接拿來用的輪子?
這三個問題按照順序一一解答之后,具體方案就能出來了。

我們先看第一個問題: 方案的效果,和最終要達到的目的是什么?

方案的效果應該是:

業務方可以不用通過繼承的方法,然后框架能夠做到對ViewController的統一配置。
業務方即使脫離框架環境,不需要修改任何代碼也能夠跑完代碼。業務方的ViewController一旦丟入框架環境,不需要修改任何代碼,框架就能夠起到它應該起的作用。
其實就是要實現 不通過業務代碼上對框架的主動迎合,使得業務能夠被框架感知 這樣的功能。細化下來就是兩個問題,框架要能夠攔截到ViewController的生命周期,另一個問題就是,攔截的定義時機。

對于方法攔截,很容易想到 Method Swizzling ,那么我們可以寫一個實例,在App啟動的時候添加針對UIViewController的方法攔截,這是一種做法。還有另一種做法就是,使用NSObject的load函數,在應用啟動時自動監聽。使用后者的好處在于,這個模塊只要被項目包含,就能夠發揮作用,不需要在項目里面添加任何代碼。

然后另外一個要考慮的事情就是,原有的TMViewController(所謂的父類)也是會提供額外方法方便子類使用的, Method Swizzling 只支持針對現有方法的操作,拓展方法的話,嗯,當然是用 Category 啦。

我本人不贊成Category的過度使用,但鑒于Category是最典型的化繼承為組合的手段,在這個場景下還是適合使用的。還有的就是,關于 Method Swizzling 手段實現方法攔截,業界也已經有了現成的開源庫: Aspects ,我們可以直接拿來使用。

我這邊有個非常非常小的Demo可以放出來給大家,這個 Demo 只是一個點睛之筆,有一些話我也寫在這個Demo里面了,各位架構師們你們可以基于各自公司App的需求去拓展。

這個Demo不包含Category,畢竟Category還是得你們自己去寫啊~然后這套方案能夠完成原來通過派生手段所有可以完成的任務,但同時又允許業務方不必添加任何代碼,直接使用原生的UIViewController。

然后另外要提醒的是,這方案的目的是消除不必要的繼承,雖然不限定于UIViewController,但它也是有適用范圍的,在適用繼承的地方,還是要老老實實使用繼承。比如你有一個數據模型,是由基本模型派生出的一整套模型,那么這個時候還是老老實實使用繼承。至于拿捏何時使用繼承,相信各位架構師一定能夠處理好,或者你也可以參考我前面提到的那篇文章來控制拿捏的尺度。

未完結,下一篇:

iOS 應用架構談:view 層的組織和調用方案(二)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容