公司業務不斷迭代擴張,項目的功能愈來愈多也愈來愈複雜,各個業務之間也不可避免的耦合愈來愈多,代碼也愈來愈臃腫,原來的模式已經沒法知足現有項目開發高複用、高可維護性的需求,目前業界解決業務多樣性複雜性比較好的一種架構思路就是組件化,將項目拆分紅各個模塊,這樣能很好的解決現有的代碼耦合度高、複用性不足的問題,也方便管理各個模塊。數組
前期調研了一些組件化的方案,大體概括爲三個方案url-block、protocol-class、target-action,通過權衡後,咱們最終選擇target-action這種方案,說到這裏確定會有人問了大家是基於什麼理由去定的這個方案的呢?因爲三個方案的優缺點所有描述篇幅太大,下面我主要基於調用方式、傳參模式給你們闡述下。bash
組件之間的調用其實就是調用方調用服務方的一個過程,這裏就涉及到一個問題調用方如何發現服務方的問題。微信
那麼問題來了,url-block、protocol-class每次啓動的時候都須要註冊一系列的映射關係到內存,隨着項目的愈來愈龐大,不可避免的會消耗掉更多的內存;業務的擴展變動不可避免的會涉及到服務映射關係的維護(增刪改),維護成本也會不斷增長,target-action則經過runtime徹底避免了相似的問題,內存開銷小,維護成本低。網絡
組件間調用,不可避免的會涉及到參數的傳遞。架構
備註:本文咱們會不斷提到一個概念叫runtime(其主要特性是消息傳遞,若是消息在對象中找不到,就進行轉發),若是對iOS runtime不是特別瞭解,能夠先搜索瞭解下,方便後面理解文章。app
target-action組件化方案分爲兩種調用方式,遠程調用和本地調用。模塊化
a. 遠程調用經過AppDelegate代理方法傳遞到當前應用,調用遠程接口並在內部作一些處理,處理完成後會在遠程接口內部調用本地接口,以實現本地調用爲遠程調用服務。工具
b. 本地調用由performTarget:action:params:
方法負責,但調用方通常不直接調用此方法,會經過一箇中間層Media層,Media層會提供明確參數和方法名的方法,在方法內部調用performTarget:
方法和參數的轉換。組件化
組件化完整的鏈路是調用方 => 中間件 => 服務方,這樣整個調用算是完成,下面從後二者的角色來闡述下大體的一個實現思路(調用方其實很簡單,字面意思你們都懂)。測試
一、中間件
TBJMediator(中間件)是基於CTMediator(target-action方案做者提供)的優化版本,基於CTMediator作了一些優化和容錯處理。首先中間件對外(調用方)暴露明確參數類型的方法,調用performTarget發現服務方對應的Target和Action,實現本地組件間的調用,實際是經過runtime(俗稱消息分發)發現服務方和服務方對應的方法。這裏你們能夠思考一個問題,每一個業務或者模塊全部的調用若是都寫在這個中間件中,幾十個甚至幾百上千個方法,勢必會對這個中間件的後期維護帶來極大的麻煩(埋坑),基於這樣的現實孕育而生了Category方案,根據每一個服務方業務,對應建立一個TBJMediator的Category(中間件分類),這樣每一個業務對外暴露的接口和這些Category一一對應,可是全部對外接口都根據業務分離。
二、服務方
服務方顧名思義服務的提供方,其實Target-Action這個方案名稱已經提早劇透了,每一個Target就是對應服務方提供的服務類,其中的每一個Action就是具體的某項服務。每一個組件能夠根據實際須要提供一個或者多個Target類,在Target類中聲明Action方法,TBJMediator經過runtime主動發現服務。
組件化的目的就是爲了下降耦合,可是項目中不可能不存在耦合,換句話說項目中各個業務都是有必定的關聯性,咱們要作的就是不斷下降沒必要要的耦合,讓項目變的架構清晰明瞭。爲了能優先完成整個組件化方案,咱們將拆分的維度適當放寬,剝離各個基礎組件和業務組件,並保證每一個組件的獨立性。
具體劃分出基礎組件、基礎業務模塊、業務模塊。
調用方在某處調用[[TBJMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]
向TBJMediator發起跨組件調用,TBJMediator根據得到的target和action信息,經過objective-C的Runtime轉化生成Target實例以及對應的Action,而後最終調用到目標業務。
調用方
// ViewController.h
#import "TBJMediator+TBJProdModul.h"
UIViewController *netLoanListVC = [TBJMediator getNetLoanListVCWithTypeId:typeId title:title];
[self.navigationController pushViewController:netLoanListVC animated:YES];
複製代碼
TBJMediator分類(中間件)
// TBJMediator+TBJProdModul.h
+ (UIViewController *)getNetLoanListVCWithTypeId:(NSString *)typeId title:(NSString *)title;
// TBJMediator+TBJProdModul.m
+ (UIViewController *)getNetLoanListVCWithTypeId:(NSString *)typeId title:(NSString *)title
{
id typeIdArg = NilObj(typeId);
id titleArg = NilObj(typeId);
return [[TBJMediator sharedInstance] performTarget:@"ProdModul" action:@"getNetLoanListVC" params:@{@"typeId": typeIdArg, @"title":titleArg} shouldCacheTarget:NO];
}
複製代碼
服務方
// Target_ProdModul.h
- (UIViewController *)getNetLoanListVC:(NSDictionary *)params;
複製代碼
上面代碼中調用方只須要依賴TBJMediator+TBJProdModul,進而達到了調用某個產品列表的目的,咱們解耦的目的達到了。固然咱們也能觀察到,如今的數據傳遞是經過字典,沒有用model傳遞,這樣作避免了直接依賴model,避免model暴露給全部組件,並且字典傳遞參數很靈活,能夠傳遞各類想要的數據類型。固然字典傳遞參數也不是沒有缺點,爲了調用方清晰,當參數個數比較多的時候,方法會看上去比較冗長,並且還須要特別注意參數的非空判斷。
一開始拆分的時候粒度要適中,粒度太細的話拆分很困難,俗話說拔出蘿蔔帶出泥,先將相對粗粒度的業務獨立的組件拆分出來,後續若是一個拆分完成的庫仍然比較臃腫的化,說明仍然存在細化拆分的餘地。
前期將項目組件大體梳理一遍,制定一個合理的拆分計劃,制定詳細的總體規劃可以將一些前期不合理的依賴、不合理的維度暴露出來,提高後續拆分的效率。
在拆分層級過程當中須要注意,上層不能對下層有依賴,下層中不能包含上層的業務邏輯。對於項目中的公共資源和代碼,儘可能下沉到下層中。
1.固然模塊化雖然有不少優勢,可是實際操做過程當中因爲CocoaPods上傳私有庫步驟繁瑣,若是每一個庫都是手動去上傳,就會比較費勁,仍是須要一些額外的腳本配合。
2.因爲涉及到打包編譯順序問題(CocoaPods維護的私有庫優先編譯),有些預編譯宏要格外注意,否則可能編譯後的代碼並非你想要的,可能編譯成了測試環境或者其餘測試環境的代碼。
3.另外每次上線以前app打包也必需要保證每一個模塊必須是最新的版本,相對單項目就沒有這個問題。
組件化目前也只是邁出了這一步,後期還有不少須要優化改進,也但願有更多的技術大咖能給出建議。
狄仁傑,銅板街 iOS 開發工程師,2013年12月加入團隊,目前主要負責 APP 端 iOS 平常開發。