UIViewController瘦身
BC架構探索之路
做iOS也有些年頭了,最近把專案核心模組的架構重新設計了一番,這裡做一些記錄。
首先,我們要對基礎的設計模式有一定的認知。這些基礎的設計模式,便是 MVC 、 MVVM 、 VIPER 。
MVC、MVVM
關於 MVC ,斯坦福的 Paul 老頭有一張經典的圖示,相信大部分iOSer都看過:

mvc.png
當有多個模組時,我們需要有多個 MVC 互相配合:

MVCs working together.png
可以看到,多個模組之間的互動都是通過 Controller 層。以上就是 MVC 的概覽,那麼 MVVM 是什麼樣的呢?
MVVM 是 Model-View-ViewModel 的縮寫。其實在 MVC 的基礎上再稍進一步,把 Controller 與 View 之間的資料傳遞過程獨立出來,封裝成一個模組,叫做 ViewModel ,這就成了 MVVM 了。在 MVVM 的基礎上,通常還會使用雙向繫結技術,使得 View 和 ViewModel 之間可以自動同步。
VIPER

viper.png
VIPER ,全稱 View-Interactor-Presenter-Entity-Router 。這是另一種細分 MVC 而得到的架構。從上圖可以看到, VIPER 實際上是將 MVC 中的 Controller 細化為了三個模組,即 Presenter、Interactor、Router 。 Entity 負責資料持久化, Interactor 負責業務相關的邏輯計算等, Presenter 則負責將業務資料傳遞給 View ,也負責處理 View 的事件。大部分 View 的事件是交由邏輯側 interactor 處理,在 interactor 處理完後會觸發必要的 UI 重新整理。跳轉相關的 View 事件則交由 Router 處理。
可以看到, VIPER 和 MVVM 並不矛盾,我們可以在 MVVM 的基礎上繼續細化得到 VIPER , ViewModel 相關的邏輯放在 Presenter 中即可。
同樣,當有多個模組時,我們需要有多個 VIPER 互相配合。
縱覽
可以看到傳統架構的進化過程: MVC -> MVVM -> VIPER 。這是一個對架構不斷細化的過程。在工程實踐中,我們的業務採用什麼架構,需要根據業務的形態和頻繁變動的模組而定。
不知大家有沒有發現,以上所述的架構解決的是單個業務模組內的職責劃分問題,並沒有解決如何將多個業務模組組合在一起的問題。即多個 MVC 或者 多個 VIPER 之間如何配合?實踐中我們發現:
- 通過對 MVC 的進一步細分,可以從單個業務模組的角度上緩解 MVC 中 Controller 中心化所導致的 massive view controller 的問題,但對於有眾多業務模組的 Controller 來說, massive view controller 依然得不到解決,即中心化的 Controller 需要做大量膠水層的工作,管理各個子 Controller 。
- 用好傳統架構,可以保證單個業務模組內的程式碼的可複用性,但並不能避免業務之間的互相影響。簡單說,就是修改業務 A 的 bug 時,可能會給業務 B 引入 bug 。
- ...
歸根結底,就是因為沒有一種更為巨集觀的組合模組的架構體系。正是為了解決如何將多個業務模組組合在一起的問題,我設計了一套 BC 的架構體系。
BC
BC ,全稱 BusinessController ,是一種為解決業務模組耦合和管理問題而生的架構體系。
為了表明 BC 的思想和實踐效果,這裡我以 UIViewController 的瘦身為例進行闡述。眾所周知, iOS 開發最讓人頭痛的問題之一就是 UIViewController 的程式碼過於龐大,難以維護。更有網友戲謔稱 MVC 為 massive view controller 。
Massive View Controller
iOS 系統預設以 UIViewController 扮演 Controller 的角色,推出一個介面就是 push 一個 UIViewController 。因此作為一個介面的總管, UIViewController 管理著各個子模組,也包攬了眾多的邊界模糊的工作。每當我們需要新增一個業務功能,首先就要找到對應的 UIViewController ,再在其中進行編碼,如下述程式碼所示:
@interface ViewController () @property (nonatomic, assign) BOOL A_LogicFlag; @property (nonatomic, assign) BOOL B_LogicFlag; ... (keep adding flags) @property (nonatomic, strong) A_ControllerClass *A_Controller; @property (nonatomic, strong) B_ControllerClass *B_Controller; ... (keep adding modules) @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.A_Controller = [A_ControllerClass new]; [self.view addSubview:self.A_Controller.view]; __weak typeof(self) weakSelf = self; [self.A_Controller sendRequestOnCompletion:^(BOOL success){ weakSelf.A_LogicFlag = YES; }]; self.B_Controller = [B_ControllerClass new]; self.B_Controller.delegate = self.A_Controller; ... (keep adding code) } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; __weak typeof(self) weakSelf = self; [self.B_Controller sendRequestOnCompletion:^(BOOL success){ weakSelf.B_LogicFlag = YES; }]; ... (keep adding code) } @end
以上程式碼已經把每一個業務邏輯封裝為一個個模組,然後在 UIViewController 中管理和維繫各個業務模組間的關係,這是我們日常工作中最常見的程式碼。很明顯,隨著業務模組的不斷增加,整個 UIViewController 的程式碼量將會無上限的增加。並且各個業務都在這個 UIViewController 中修改程式碼,很容易互相引入bug,產生耦合。
如果有細心的讀者,會發現這其中還有時序問題。怎麼講?假設現在我們有一個模組 C ,我們想要做一個小改動:將 A 模組的初始化時機放在 C 模組的資料請求返回成功後。這是個很簡單的改動,只需將 A 模組的初始化工作放入 C 模組的資料請求返回的 completion block 裡:
- (void)viewDidLoad { [super viewDidLoad]; self.C_Controller = [C_ControllerClass new]; __weak typeof(self) weakSelf = self; [self.C_Controller sendRequestOnCompletion:^(BOOL success){ weakSelf.A_Controller = [A_ControllerClass new]; [weakSelf.view addSubview:weakSelf.A_Controller.view]; [weakSelf.A_Controller sendRequestOnCompletion:^(BOOL success){ weakSelf.A_LogicFlag = YES; }]; }]; self.B_Controller = [B_ControllerClass new]; self.B_Controller.delegate = self.A_Controller; ... (keep adding code) }
若不仔細看看,難以發現以上程式碼已經有了 bug 。因為我們延遲了 A_Controller 的初始化,所以在 B_Controller 設定 delegate 時,寫入的 A_Controller 是 nil 。這就是時序依賴, B_Controller 在設定 delegate 時,要求 A_Controller 已經完成了初始化。看似這種時序問題在所難免,其實不然。在 BC 架構中,我將描述一種解決該時序問題的方案。
另外,由於 coder 在 VC 中有著極高的自由度,所以當 coder 在做一些小特性時,會直接把程式碼寫在 VC 中。大家為省事不再去為小功能獨立建立模組,這樣 VC 中的程式碼會更加混亂不堪。
- 無限增長的程式碼量
- 魚龍混雜的耦合關係
- 複雜的時序問題
- 過度自由引入的混亂
- ...
讓我們來看看 BC 的架構體系如何來解決這些問題。
BC 實現
我們讓 UIViewController 只負責持有和維護一個業務模組( businessController )的陣列,其並不關心陣列中每個業務模組的具體實現。我們定義一個 businessController 的基類,或者協議。這裡我們以協議為例,定義協議 BusinessController
。
// Define.h @protocol BusinessController <NSObject> @end // ViewController.h @interface ViewController : UIViewController @property (nonatomic, strong) NSMutableArray<id<BusinessController>> *businessControllers; @end
首先,我們希望能夠將 View Controller 的狀態事件通知給 Business Controller ,而 Business Controller 可以選擇性的實現這些事件。所以我們先定義一個協議 ViewControllerEvents
。因為是可選擇性實現,所以為 optional 。
// Define.h @protocol ViewControllerEvents <NSObject> @optional - (void)jx_viewDidLoad; - (void)jx_viewWillAppear; - (void)jx_viewDidAppear; - (void)jx_viewWillDisappear; - (void)jx_viewDidDisappear; // ... 其它主框架的事件也可放在這裡 @end
然後使 BusinessController
遵循 ViewControllerEvents
協議,這樣在 BusinessController 就有了監聽 VC 事件的能力,並且可以自動補全這些方法名。
// Define.h @protocol BusinessController <ViewControllerEvents> @required // 建立一個vc的弱引用,用於訪問vc @property (nonatomic, weak) ViewController *viewController; @end
接著, VC 需要向業務模組傳送這些狀態事件。以 viewWillAppear
為例,
// ViewController.m - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self.businessControllers enumerateObjectsUsingBlock:^(id<BusinessController>_Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if ([obj respondsToSelector:@selector(jx_viewWillAppear)]) { [obj jx_viewWillAppear]; } }]; }
現在,當我們需要新增一個模組 A 時,只需使其遵循 BusinessController
協議,一切就像在一個全新的 VC 中編碼一樣,十分清爽。
// A_ControllerClass.m - (void)jx_viewWillAppear { // do some logic request or other business logics ... }
最後,我們只需在 VC 中新增各個業務模組,讓整個流程跑通:
// ViewController.m - (void)viewDidLoad { [super viewDidLoad]; [self addBusinessControllers:@[[A_ControllerClass new], [B_ControllerClass new], [C_ControllerClass new], ...]]; }
至此, VC 中的程式碼就被我們劃分為了許許多多的模組。可是, 業務模組之間,是需要通訊的 ,那我們又如何解決這個通訊問題呢?我們最容易想到的是兩種常規的通訊方式—— NSNotification 和 delegate 。
首先, NSNotification 是不合適的。這是一種全域性通知,整個 APP 都會收到。我們希望的結果是, ViewController
例項一中的模組 A 給模組 B 發訊息時,不會發送到 ViewController
例項二中的模組 B 去。
那我們就用 delegate 吧?—— NO! 第一,使用 delegate 我們需要不斷的去維護那些物件之間的 delegate 關係(即在 VC 中編寫 delegate 的依賴關係, A.delegate = B
),這也會引入 Massive View Controller 中提到的時序問題。第二,若是模組 A 的代理事件模組 B 和模組 C 都需要監聽,我們還需要將 delegate 做成陣列。咦,真夠噁心。
所以,我們能否找到一種更好的方式來解決通訊問題呢?
這裡我提供的解決方案是使用 OC 的訊息轉發特性(對訊息轉發不太瞭解的同學,可以學習一下《Effective Objective-C 2.0》中訊息轉發的章節)。首先我們建立一個訊息中心 CommunicationCenter
,一個訊息協議 BusinessControllerConversation
。讓訊息中心遵循訊息協議,但其內部不實現任何方法,其只做轉發,將訊息轉發給每一個實現了該訊息的業務模組( BC )。接收訊息的 BC 也遵循 BusinessControllerConversation
協議。
// Define.h @protocol BusinessControllerConversation <NSObject> @end // Define.h @protocol BusinessController <ViewControllerEvents, BusinessControllerConversation> @required // 建立一個vc的弱引用,用於訪問vc @property (nonatomic, weak) ViewController *viewController; @end // CommunicationCenter.m @interface CommunicationCenter : NSObject <BusinessControllerConversation> // 建立一個vc的弱引用,用於訪問vc @property (nonatomic, weak) ViewController *viewController; @end // CommunicationCenter.m - (void)forwardInvocation:(NSInvocation *)anInvocation { SEL selector = anInvocation.selector; [self.viewController.businessControllers enumerateObjectsUsingBlock:^(id<BusinessController> _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if ([obj respondsToSelector:selector]) { [anInvocation invokeWithTarget:obj]; } }]; }
接著,我們在 VC 中建立並持有一個訊息中心。
// ViewController.h @property (nonatomic, strong) CommunicationCenter *communicationCenter; // ViewController.m _communicationCenter = [CommunicationCenter new];
這樣,當我們的業務模組之間需要通訊時,將訊息定義在 BusinessControllerConversation
中,然後直接向訊息中心傳送訊息即可。例如當前頁面的重新整理按鈕被點選了,但管理重新整理按鈕的模組並不管當前頁面有哪些模組需要重新整理,它只管將該訊息拋到訊息中心。而需要重新整理的業務模組,則實現該訊息即可。
// Define.h @protocol BusinessControllerConversation <NSObject> @optional - (void)msg_refreshButtonClicked; @end // B_ControllerClass.m - (void)refreshBtnClicked { [self.viewController.communicationCenter msg_refreshBtnClicked]; } // A_ControllerClass.m - (void)msg_refreshBtnClicked { // do some business logic ... }
由此,我們實現了單個VC中,模組之間一對多的互相通訊。這裡值得注意的是,模組 A 和模組 B 的耦合度幾乎降至最低。因為 A 和 B 之間互相都不知道對方,不需要設定對方為 delegate ,也不會有建立依賴的時序問題。 BC 都全部面向訊息程式設計,即面向協議程式設計。
這就是使用 CommunicationCenter 進行統一轉發的通訊方式所帶來的極大好處:訊息傳送方不需要關心誰接收訊息,其只管通知一下某事件發生了。訊息接收方也不需要關心誰傳送的訊息,其只管接收訊息做出反應。這樣使業務模組間的耦合性降至最低。
不難發現,只要是業務模組 BC 所需要的事件,我們都可以通過 CommunicationCenter
進行轉發。所以我們讓 CommunicationCenter
和 BusinessController
遵循 ViewControllerEvents
協議,這樣 ViewController
中的狀態事件,我們直接拋給 CommunicationCenter
即可。狀態事件會經過 CommunicationCenter
路由至業務 BC 。
// ViewController.m - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self.communicationCenter jx_viewWillAppear]; }
在採用 BC 的架構之後,所有的模組都需要建立 BC ,再也沒有隨意散落在 VC 中的程式碼。
至此,我們實現了將 VC 中的業務模組逐一打散,各自為營,也支援業務模組之間的靈活通訊。其程式碼量無限增長的問題、程式碼糅雜在一起魚龍混雜的問題等,都得到了解決。
BC 與傳統架構

BC_Overview.png
BC 設計模式的通訊結構如上圖所示( Owner 即文中的 ViewController )。 Owner 將主流程事件發至訊息中心,由訊息中心路由至各個 Module 。而各個 Module 之間也通過訊息中心轉發至其他 Module 。
可以看到 BC 和傳統的 MVC , MVVM , VIPER 的關係不是互斥的,是並存的。從 MVC 到 MVVM 到 VIPER 是對架構的不斷細化。而 BC 則是提供了一種劃分模組的機制。即一個 Module 可以是 Model ,可以是 View ,也可以是包含了 MVC 的一個完整的模組。在使用 MVC , MVVM , VIPER 等設計模式時,我們可以同時使用 BC 來幫助我們組織各個模組。通過 BC ,我們將根據不同架構設計的不同模組有機的結合了起來。