前言
公司業務不斷迭代擴張,專案的功能越來越多也越來越複雜,各個業務之間也不可避免的耦合越來越多,程式碼也越來越臃腫,原來的模式已經無法滿足現有專案開發高複用、高可維護性的需求,目前業界解決業務多樣性複雜性比較好的一種架構思路就是元件化,將專案拆分成各個模組,這樣能很好的解決現有的程式碼耦合度高、複用性不足的問題,也方便管理各個模組。
技術選型
前期調研了一些元件化的方案,大致歸納為三個方案url-block、protocol-class、target-action,經過權衡後,我們最終選擇target-action這種方案,說到這裡肯定會有人問了你們是基於什麼理由去定的這個方案的呢?由於三個方案的優缺點全部描述篇幅太大,下面我主要基於呼叫方式、傳參模式給大家闡述下。
呼叫方式
元件之間的呼叫其實就是呼叫方呼叫服務方的一個過程,這裡就涉及到一個問題呼叫方如何發現服務方的問題。
- url-block是通過url來發現服務,服務方在應用啟動時候優先註冊一系列url和對應的block到記憶體中;
- protocol-class其實是基於url-block的一種擴充套件補充的元件化方案,服務方也需要在應用啟動的時候優先將class和protocol做一個對映存放到記憶體中;
- target-action基於的是runtime,不涉及到任何註冊對映關係的問題。
那麼問題來了,url-block、protocol-class每次啟動的時候都需要註冊一系列的對映關係到記憶體,隨著專案的越來越龐大,不可避免的會消耗掉更多的記憶體;業務的擴充套件變更不可避免的會涉及到服務對映關係的維護(增刪改),維護成本也會不斷增加,target-action則通過runtime完全避免了類似的問題,記憶體開銷小,維護成本低。
傳參模式
元件間呼叫,不可避免的會涉及到引數的傳遞。
- url-block是基於url,這樣弊端就暴露無遺了,url傳遞的引數限制性很強,就舉個簡單的列子,分享模組的分享圖片問題,圖片之類的引數url傳遞不了,再比如字典、陣列等,這樣url傳遞這些引數顯然不合適也不方便;
- protocol-class正是由於url-block傳參的弊端才孕育而生的擴充套件方法,這裡雖然能解決傳遞複雜引數的問題,但是記憶體增加和難維護的問題依舊存在;
- target-action方案提出了去model化傳參,因為如果傳遞的是對應的model,因為對應的model一般都是和對應的業務或者模組掛鉤的,這樣元件之間本質上還是沒有獨立,沒有達到去耦合的目的,最終這個方案呼叫方和服務方之間是通過字典來進行傳參,這樣做具備字典傳參的靈活性、多樣性,也具備了url-block方案不具備傳遞複雜引數的能力,引數去model化和呼叫runtime也徹底斬斷了服務方和中介軟體之間的依賴,真正意義上實現了元件化。
備註:本文我們會不斷提到一個概念叫runtime(其主要特性是訊息傳遞,如果訊息在物件中找不到,就進行轉發),如果對iOS runtime不是特別瞭解,可以先搜尋瞭解下,方便後面理解文章。
target-action元件化方案
方案架構
target-action元件化方案分為兩種呼叫方式,遠端呼叫和本地呼叫。
a. 遠端呼叫通過AppDelegate代理方法傳遞到當前應用,呼叫遠端介面並在內部做一些處理,處理完成後會在遠端介面內部呼叫本地介面,以實現本地呼叫為遠端呼叫服務。
b. 本地呼叫由performTarget:action:params:
方法負責,但呼叫方一般不直接呼叫此方法,會通過一箇中間層Media層,Media層會提供明確引數和方法名的方法,在方法內部呼叫performTarget:
方法和引數的轉換。
方案思路
元件化完整的鏈路是呼叫方 => 中介軟體 => 服務方,這樣整個呼叫算是完成,下面從後兩者的角色來闡述下大致的一個實現思路(呼叫方其實很簡單,字面意思大家都懂)。
1、中介軟體
TBJMediator(中介軟體)是基於CTMediator(target-action方案作者提供)的優化版本,基於CTMediator做了一些優化和容錯處理。首先中介軟體對外(呼叫方)暴露明確引數型別的方法,呼叫performTarget發現服務方對應的Target和Action,實現本地元件間的呼叫,實際是通過runtime(俗稱訊息分發)發現服務方和服務方對應的方法。這裡大家可以思考一個問題,每個業務或者模組所有的呼叫如果都寫在這個中介軟體中,幾十個甚至幾百上千個方法,勢必會對這個中介軟體的後期維護帶來極大的麻煩(埋坑),基於這樣的現實孕育而生了Category方案,根據每個服務方業務,對應建立一個TBJMediator的Category(中介軟體分類),這樣每個業務對外暴露的介面和這些Category一一對應,但是所有對外介面都根據業務分離。
2、服務方
服務方顧名思義服務的提供方,其實Target-Action這個方案名稱已經提前劇透了,每個Target就是對應服務方提供的服務類,其中的每個Action就是具體的某項服務。每個元件可以根據實際需要提供一個或者多個Target類,在Target類中宣告Action方法,TBJMediator通過runtime主動發現服務。
具體實施
元件化的目的就是為了降低耦合,但是專案中不可能不存在耦合,換句話說專案中各個業務都是有一定的關聯性,我們要做的就是不斷降低不必要的耦合,讓專案變的架構清晰明瞭。為了能優先完成整個元件化方案,我們將拆分的維度適當放寬,剝離各個基礎元件和業務元件,並保證每個元件的獨立性。
具體劃分出基礎元件、基礎業務模組、業務模組。
- 基礎元件主要包含業務完全無關的一些UI控制元件、UI工廠類、基礎工具類、網路請求等;
- 基礎業務模組包含分享模組、外掛模組、以及基礎服務模組等;
- 業務模組主要包含產品模組、使用者模組等。
技術實現(產品模組為例)
呼叫方在某處呼叫[[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.合理的拆分粒度
一開始拆分的時候粒度要適中,粒度太細的話拆分很困難,俗話說拔出蘿蔔帶出泥,先將相對粗粒度的業務獨立的元件拆分出來,後續如果一個拆分完成的庫仍然比較臃腫的化,說明仍然存在細化拆分的餘地。
2.制定拆分計劃
前期將專案元件大致梳理一遍,制定一個合理的拆分計劃,制定詳細的整體規劃能夠將一些前期不合理的依賴、不合理的維度暴露出來,提升後續拆分的效率。
3.拆分原則
在拆分層級過程中需要注意,上層不能對下層有依賴,下層中不能包含上層的業務邏輯。對於專案中的公共資源和程式碼,儘量下沉到下層中。
模組化後相比單專案的一些缺點
1.當然模組化雖然有很多優點,但是實際操作過程中由於CocoaPods上傳私有庫步驟繁瑣,如果每個庫都是手動去上傳,就會比較費勁,還是需要一些額外的指令碼配合。
2.由於涉及到打包編譯順序問題(CocoaPods維護的私有庫優先編譯),有些預編譯巨集要格外注意,不然可能編譯後的程式碼並不是你想要的,可能編譯成了測試環境或者其他測試環境的程式碼。
3.另外每次上線之前app打包也必須要保證每個模組必須是最新的版本,相對單專案就沒有這個問題。
元件化目前也只是邁出了這一步,後期還有很多需要優化改進,也希望有更多的技術大咖能給出建議。
作者簡介
狄仁傑,銅板街 iOS 開發工程師,2013年12月加入團隊,目前主要負責 APP 端 iOS 日常開發。